A simple scripting language in C++
Ferenc Szontágh
2025-04-19 ea9af87ba4399d094180f06be59c878d864a17e0
implemented array, added some new builtin functions
4 files modified
1 files renamed
9 files added
4 files deleted
1186 ■■■■■ changed files
src/Interpreter/ArrayAccessExpressionNode.hpp 55 ●●●●● patch | view | raw | blame | history
src/Interpreter/ExpressionBuilder.hpp 10 ●●●●● patch | view | raw | blame | history
src/Modules/BuiltIn/ArrayModule.hpp 44 ●●●●● patch | view | raw | blame | history
src/Modules/BuiltIn/FileModule.hpp 4 ●●●● patch | view | raw | blame | history
src/Modules/BuiltIn/JsonModule.hpp 328 ●●●●● patch | view | raw | blame | history
src/Modules/BuiltIn/PrintModule.hpp 44 ●●●●● patch | view | raw | blame | history
src/Modules/BuiltIn/StringModule.hpp 101 ●●●●● patch | view | raw | blame | history
src/Modules/BuiltIn/VariableHelpersModule.hpp 48 ●●●●● patch | view | raw | blame | history
src/Modules/JsonModule.hpp 239 ●●●●● patch | view | raw | blame | history
src/Modules/PrintModule.hpp 29 ●●●●● patch | view | raw | blame | history
src/Modules/PrintNlModule.hpp 31 ●●●●● patch | view | raw | blame | history
src/Modules/TypeofModule.hpp 45 ●●●●● patch | view | raw | blame | history
src/Parser/Parser.cpp 155 ●●●● patch | view | raw | blame | history
src/VoidScript.hpp 26 ●●●●● patch | view | raw | blame | history
test_scripts/array.vs 18 ●●●● patch | view | raw | blame | history
test_scripts/test_array.vs 2 ●●●●● patch | view | raw | blame | history
test_scripts/test_string.vs 3 ●●●●● patch | view | raw | blame | history
test_scripts/test_substr.vs 4 ●●●● patch | view | raw | blame | history
src/Interpreter/ArrayAccessExpressionNode.hpp
New file
@@ -0,0 +1,55 @@
 #ifndef INTERPRETER_ARRAY_ACCESS_EXPRESSION_NODE_HPP
 #define INTERPRETER_ARRAY_ACCESS_EXPRESSION_NODE_HPP
 #include "ExpressionNode.hpp"
 #include "Symbols/Value.hpp"
 #include <stdexcept>
 #include <string>
 namespace Interpreter {
 /**
  * @brief Expression node for dynamic array/object indexing: expr[index]
  */
 class ArrayAccessExpressionNode : public ExpressionNode {
   private:
    std::unique_ptr<ExpressionNode> arrayExpr_;
    std::unique_ptr<ExpressionNode> indexExpr_;
   public:
    ArrayAccessExpressionNode(std::unique_ptr<ExpressionNode> arrayExpr,
                              std::unique_ptr<ExpressionNode> indexExpr)
        : arrayExpr_(std::move(arrayExpr)), indexExpr_(std::move(indexExpr)) {}
    Symbols::Value evaluate(Interpreter &interpreter) const override {
        // Evaluate the container (object or array)
        Symbols::Value container = arrayExpr_->evaluate(interpreter);
        if (container.getType() != Symbols::Variables::Type::OBJECT) {
            throw std::runtime_error("Attempted to index non-array");
        }
        const auto & map = std::get<Symbols::Value::ObjectMap>(container.get());
        // Evaluate the index
        Symbols::Value idxVal = indexExpr_->evaluate(interpreter);
        std::string key;
        if (idxVal.getType() == Symbols::Variables::Type::INTEGER) {
            key = std::to_string(idxVal.get<int>());
        } else if (idxVal.getType() == Symbols::Variables::Type::STRING) {
            key = idxVal.get<std::string>();
        } else {
            throw std::runtime_error("Array index must be integer or string");
        }
        auto it = map.find(key);
        if (it == map.end()) {
            throw std::runtime_error("Index not found: " + key);
        }
        return it->second;
    }
    std::string toString() const override {
        return arrayExpr_->toString() + "[" + indexExpr_->toString() + "]";
    }
};
} // namespace Interpreter
 #endif // INTERPRETER_ARRAY_ACCESS_EXPRESSION_NODE_HPP
src/Interpreter/ExpressionBuilder.hpp
@@ -11,6 +11,7 @@
#include "Interpreter/UnaryExpressionNode.hpp"  // <-- új include
#include "Interpreter/CallExpressionNode.hpp"
#include "Interpreter/MemberExpressionNode.hpp"
#include "Interpreter/ArrayAccessExpressionNode.hpp"
#include "Interpreter/ObjectExpressionNode.hpp"
#include "Interpreter/ObjectExpressionNode.hpp"
#include "Parser/ParsedExpression.hpp"
@@ -29,10 +30,16 @@
        case Kind::Binary:
            {
                // Array/object dynamic indexing: operator []
                if (expr->op == "[]") {
                    auto arrExpr = buildExpressionFromParsed(expr->lhs);
                    auto idxExpr = buildExpressionFromParsed(expr->rhs);
                    return std::make_unique<Interpreter::ArrayAccessExpressionNode>(std::move(arrExpr), std::move(idxExpr));
                }
                // Member access for object properties: '->'
                if (expr->op == "->") {
                    auto objExpr = buildExpressionFromParsed(expr->lhs);
                    std::string propName;
                    // RHS parsed expression should be a literal string or variable parser node
                    if (expr->rhs->kind == ParsedExpression::Kind::Literal &&
                        expr->rhs->value.getType() == Symbols::Variables::Type::STRING) {
                        propName = expr->rhs->value.get<std::string>();
@@ -43,6 +50,7 @@
                    }
                    return std::make_unique<Interpreter::MemberExpressionNode>(std::move(objExpr), propName);
                }
                // Default binary operator
                auto lhs = buildExpressionFromParsed(expr->lhs);
                auto rhs = buildExpressionFromParsed(expr->rhs);
                return std::make_unique<Interpreter::BinaryExpressionNode>(std::move(lhs), expr->op, std::move(rhs));
src/Modules/BuiltIn/ArrayModule.hpp
New file
@@ -0,0 +1,44 @@
// ArrayModule.hpp
#ifndef MODULES_ARRAYMODULE_HPP
#define MODULES_ARRAYMODULE_HPP
#include <stdexcept>
#include <string>
#include <vector>
#include "Modules/BaseModule.hpp"
#include "Modules/ModuleManager.hpp"
#include "Symbols/Value.hpp"
#include "Symbols/VariableTypes.hpp"
namespace Modules {
/**
 * @brief Module providing a sizeof() function for array variables.
 * Usage:
 *   sizeof($array)   -> returns number of elements in the array
 */
class ArrayModule : public BaseModule {
  public:
    void registerModule() override {
        auto & mgr = ModuleManager::instance();
        mgr.registerFunction("sizeof", [](const std::vector<Symbols::Value> & args) {
            using namespace Symbols;
            if (args.size() != 1) {
                throw std::runtime_error("sizeof expects exactly one argument");
            }
            const auto & val  = args[0];
            auto   type = val.getType();
            // Only allow array types (OBJECT)
            if (type == Variables::Type::OBJECT) {
                const auto & map = std::get<Value::ObjectMap>(val.get());
                return Value(static_cast<int>(map.size()));
            }
            throw std::runtime_error("sizeof expects an array variable");
        });
    }
};
}  // namespace Modules
#endif  // MODULES_ARRAYMODULE_HPP
src/Modules/BuiltIn/FileModule.hpp
File was renamed from src/Modules/FileModule.hpp
@@ -8,8 +8,8 @@
#include <stdexcept>
#include <vector>
#include <string>
#include "BaseModule.hpp"
#include "ModuleManager.hpp"
#include "Modules/BaseModule.hpp"
#include "Modules/ModuleManager.hpp"
#include "Symbols/Value.hpp"
namespace Modules {
src/Modules/BuiltIn/JsonModule.hpp
New file
@@ -0,0 +1,328 @@
// JsonModule.hpp
#ifndef MODULES_JSONMODULE_HPP
#define MODULES_JSONMODULE_HPP
#include <cctype>
#include <stdexcept>
#include <string>
#include <variant>
#include "Modules/BaseModule.hpp"
#include "Modules/ModuleManager.hpp"
#include "Symbols/Value.hpp"
#include "Symbols/VariableTypes.hpp"
namespace Modules {
/**
 * @brief Module providing JSON encode/decode functions.
 *   json_encode(value) -> string
 *   json_decode(string) -> object/value
 */
class JsonModule : public BaseModule {
  public:
    void registerModule() override {
        auto & mgr = ModuleManager::instance();
        // json_encode: serialize a Value to JSON string
        mgr.registerFunction("json_encode", [](const std::vector<Symbols::Value> & args) {
            using namespace Symbols;
            if (args.size() != 1) {
                throw std::runtime_error("json_encode expects 1 argument");
            }
            // forward to encoder
            std::function<std::string(const Value &)> encode;
            encode = [&](const Value & v) -> std::string {
                const auto & var = v.get();
                return std::visit(
                    [&](auto && x) -> std::string {
                        using T = std::decay_t<decltype(x)>;
                        if constexpr (std::is_same_v<T, bool>) {
                            return x ? "true" : "false";
                        } else if constexpr (std::is_same_v<T, int> || std::is_same_v<T, double> ||
                                             std::is_same_v<T, float>) {
                            return std::to_string(x);
                        } else if constexpr (std::is_same_v<T, std::string>) {
                            // escape string
                            std::string out = "\"";
                            for (char c : x) {
                                switch (c) {
                                    case '"':
                                        out += "\\\"";
                                        break;
                                    case '\\':
                                        out += "\\\\";
                                        break;
                                    case '\b':
                                        out += "\\b";
                                        break;
                                    case '\f':
                                        out += "\\f";
                                        break;
                                    case '\n':
                                        out += "\\n";
                                        break;
                                    case '\r':
                                        out += "\\r";
                                        break;
                                    case '\t':
                                        out += "\\t";
                                        break;
                                    default:
                                        if (static_cast<unsigned char>(c) < 0x20) {
                                            // control character
                                            char buf[7];
                                            std::snprintf(buf, sizeof(buf), "\\u%04x", c);
                                            out += buf;
                                        } else {
                                            out += c;
                                        }
                                }
                            }
                            out += "\"";
                            return out;
                        } else if constexpr (std::is_same_v<T, Value::ObjectMap>) {
                            std::string out   = "{";
                            bool        first = true;
                            for (const auto & kv : x) {
                                if (!first) {
                                    out += ",";
                                }
                                first = false;
                                // key
                                out += '"';
                                // escape key string
                                for (char c : kv.first) {
                                    switch (c) {
                                        case '"':
                                            out += "\\\"";
                                            break;
                                        case '\\':
                                            out += "\\\\";
                                            break;
                                        case '\b':
                                            out += "\\b";
                                            break;
                                        case '\f':
                                            out += "\\f";
                                            break;
                                        case '\n':
                                            out += "\\n";
                                            break;
                                        case '\r':
                                            out += "\\r";
                                            break;
                                        case '\t':
                                            out += "\\t";
                                            break;
                                        default:
                                            if (static_cast<unsigned char>(c) < 0x20) {
                                                char buf[7];
                                                std::snprintf(buf, sizeof(buf), "\\u%04x", c);
                                                out += buf;
                                            } else {
                                                out += c;
                                            }
                                    }
                                }
                                out += '"';
                                out += ':';
                                out += encode(kv.second);
                            }
                            out += "}";
                            return out;
                        } else {
                            return "null";
                        }
                    },
                    var);
            };
            std::string result = encode(args[0]);
            return Symbols::Value(result);
        });
        // json_decode: parse JSON string to Value (object/value)
        mgr.registerFunction("json_decode", [](const std::vector<Symbols::Value> & args) {
            using namespace Symbols;
            if (args.size() != 1) {
                throw std::runtime_error("json_decode expects 1 argument");
            }
            if (args[0].getType() != Variables::Type::STRING) {
                throw std::runtime_error("json_decode expects a JSON string");
            }
            const std::string s = args[0].get<std::string>();
            struct Parser {
                const std::string & s;
                size_t              pos = 0;
                Parser(const std::string & str) : s(str), pos(0) {}
                void skip() {
                    while (pos < s.size() && std::isspace(static_cast<unsigned char>(s[pos]))) {
                        pos++;
                    }
                }
                std::string parseString() {
                    skip();
                    if (s[pos] != '"') {
                        throw std::runtime_error("Invalid JSON string");
                    }
                    pos++;
                    std::string out;
                    while (pos < s.size()) {
                        char c = s[pos++];
                        if (c == '"') {
                            break;
                        }
                        if (c == '\\') {
                            if (pos >= s.size()) {
                                break;
                            }
                            char e = s[pos++];
                            switch (e) {
                                case '"':
                                    out += '"';
                                    break;
                                case '\\':
                                    out += '\\';
                                    break;
                                case '/':
                                    out += '/';
                                    break;
                                case 'b':
                                    out += '\b';
                                    break;
                                case 'f':
                                    out += '\f';
                                    break;
                                case 'n':
                                    out += '\n';
                                    break;
                                case 'r':
                                    out += '\r';
                                    break;
                                case 't':
                                    out += '\t';
                                    break;
                                default:
                                    out += e;
                                    break;
                            }
                        } else {
                            out += c;
                        }
                    }
                    return out;
                }
                Value parseNumber() {
                    skip();
                    size_t start = pos;
                    if (s[pos] == '-') {
                        pos++;
                    }
                    while (pos < s.size() && std::isdigit(static_cast<unsigned char>(s[pos]))) {
                        pos++;
                    }
                    bool isDouble = false;
                    if (pos < s.size() && s[pos] == '.') {
                        isDouble = true;
                        pos++;
                        while (pos < s.size() && std::isdigit(static_cast<unsigned char>(s[pos]))) {
                            pos++;
                        }
                    }
                    std::string num = s.substr(start, pos - start);
                    try {
                        if (isDouble) {
                            return Value(std::stod(num));
                        }
                        return Value(std::stoi(num));
                    } catch (...) {
                        throw std::runtime_error("Invalid JSON number: " + num);
                    }
                }
                Value parseBool() {
                    skip();
                    if (s.compare(pos, 4, "true") == 0) {
                        pos += 4;
                        return Value(true);
                    }
                    if (s.compare(pos, 5, "false") == 0) {
                        pos += 5;
                        return Value(false);
                    }
                    throw std::runtime_error("Invalid JSON boolean");
                }
                Value parseNull() {
                    skip();
                    if (s.compare(pos, 4, "null") == 0) {
                        pos += 4;
                        return Value::makeNull();
                    }
                    throw std::runtime_error("Invalid JSON null");
                }
                Value parseObject() {
                    skip();
                    if (s[pos] != '{') {
                        throw std::runtime_error("Invalid JSON object");
                    }
                    pos++;
                    skip();
                    Value::ObjectMap obj;
                    if (s[pos] == '}') {
                        pos++;
                        return Value(obj);
                    }
                    while (pos < s.size()) {
                        skip();
                        std::string key = parseString();
                        skip();
                        if (s[pos] != ':') {
                            throw std::runtime_error("Expected ':' in object");
                        }
                        pos++;
                        skip();
                        Value val = parseValue();
                        obj.emplace(key, val);
                        skip();
                        if (s[pos] == ',') {
                            pos++;
                            continue;
                        }
                        if (s[pos] == '}') {
                            pos++;
                            break;
                        }
                        throw std::runtime_error("Expected ',' or '}' in object");
                    }
                    return Value(obj);
                }
                Value parseValue() {
                    skip();
                    if (pos >= s.size()) {
                        throw std::runtime_error("Empty JSON");
                    }
                    char c = s[pos];
                    if (c == '{') {
                        return parseObject();
                    }
                    if (c == '"') {
                        std::string str = parseString();
                        return Value(str);
                    }
                    if (c == 't' || c == 'f') {
                        return parseBool();
                    }
                    if (c == 'n') {
                        return parseNull();
                    }
                    if (c == '-' || std::isdigit(static_cast<unsigned char>(c))) {
                        return parseNumber();
                    }
                    throw std::runtime_error(std::string("Invalid JSON value at pos ") + std::to_string(pos));
                }
            } parser(s);
            Value result = parser.parseValue();
            return result;
        });
    }
};
}  // namespace Modules
#endif  // MODULES_JSONMODULE_HPP
src/Modules/BuiltIn/PrintModule.hpp
New file
@@ -0,0 +1,44 @@
// PrintModule.hpp
#ifndef MODULES_PRINTMODULE_HPP
#define MODULES_PRINTMODULE_HPP
#include <iostream>
#include "Modules/BaseModule.hpp"
#include "Modules/ModuleManager.hpp"
#include "Symbols/Value.hpp"
namespace Modules {
/**
 * @brief Module that provides a built-in print function.
 */
class PrintModule : public BaseModule {
  public:
    void registerModule() override {
        auto & mgr = ModuleManager::instance();
        mgr.registerFunction("print", [](const std::vector<Symbols::Value> & args) {
            for (const auto & v : args) {
                std::cout << Symbols::Value::to_string(v);
            }
            return Symbols::Value();
        });
        mgr.registerFunction("printnl", [](const std::vector<Symbols::Value> & args) {
            for (const auto & v : args) {
                std::cout << Symbols::Value::to_string(v);
            }
            std::cout << "\n";
            return Symbols::Value();
        });
        mgr.registerFunction("error", [](const std::vector<Symbols::Value> & args) {
            for (const auto & v : args) {
                std::cerr << Symbols::Value::to_string(v);
            }
            std::cerr << "\n";
            return Symbols::Value();
        });
    }
};
}  // namespace Modules
#endif  // MODULES_PRINTMODULE_HPP
src/Modules/BuiltIn/StringModule.hpp
New file
@@ -0,0 +1,101 @@
// StringModule.hpp
#ifndef MODULES_STRINGMODULE_HPP
#define MODULES_STRINGMODULE_HPP
#include <string>
#include <vector>
#include <stdexcept>
#include "Modules/BaseModule.hpp"
#include "Modules/ModuleManager.hpp"
#include "Symbols/Value.hpp"
#include "Symbols/VariableTypes.hpp"
namespace Modules {
/**
 * @brief Module providing string helper functions.
 * Functions:
 *   string_length(string $in) -> length of the string
 *   string_replace(string $in, string $from, string $to, bool $replace_all)
 *   string_substr(string $in, int from, int length) -> substring
 */
class StringModule : public BaseModule {
 public:
    void registerModule() override {
        auto &mgr = ModuleManager::instance();
        // string_length
        mgr.registerFunction("string_length", [](const std::vector<Symbols::Value> &args) {
            using namespace Symbols;
            if (args.size() != 1) {
                throw std::runtime_error("string_length expects exactly one argument");
            }
            if (args[0].getType() != Variables::Type::STRING) {
                throw std::runtime_error("string_length expects a string argument");
            }
            const std::string &s = args[0].get<std::string>();
            return Value(static_cast<int>(s.size()));
        });
        // string_replace
        mgr.registerFunction("string_replace", [](const std::vector<Symbols::Value> &args) {
            using namespace Symbols;
            if (args.size() != 4) {
                throw std::runtime_error("string_replace expects 4 arguments");
            }
            if (args[0].getType() != Variables::Type::STRING ||
                args[1].getType() != Variables::Type::STRING ||
                args[2].getType() != Variables::Type::STRING ||
                args[3].getType() != Variables::Type::BOOLEAN) {
                throw std::runtime_error("string_replace argument types must be (string, string, string, boolean)");
            }
            std::string in = args[0].get<std::string>();
            const std::string &from = args[1].get<std::string>();
            const std::string &to = args[2].get<std::string>();
            bool replace_all = args[3].get<bool>();
            if (from.empty()) {
                throw std::runtime_error("string_replace: 'from' cannot be empty");
            }
            size_t pos = 0;
            if (replace_all) {
                while ((pos = in.find(from, pos)) != std::string::npos) {
                    in.replace(pos, from.length(), to);
                    pos += to.length();
                }
            } else {
                pos = in.find(from);
                if (pos != std::string::npos) {
                    in.replace(pos, from.length(), to);
                }
            }
            return Value(in);
        });
        // string_substr
        mgr.registerFunction("string_substr", [](const std::vector<Symbols::Value> &args) {
            using namespace Symbols;
            if (args.size() != 3) {
                throw std::runtime_error("string_substr expects 3 arguments");
            }
            if (args[0].getType() != Variables::Type::STRING ||
                args[1].getType() != Variables::Type::INTEGER ||
                args[2].getType() != Variables::Type::INTEGER) {
                throw std::runtime_error("string_substr argument types must be (string, int, int)");
            }
            const std::string &s = args[0].get<std::string>();
            int from = args[1].get<int>();
            int length = args[2].get<int>();
            if (from < 0 || length < 0) {
                throw std::runtime_error("string_substr: 'from' and 'length' must be non-negative");
            }
            size_t pos = static_cast<size_t>(from);
            if (pos > s.size()) {
                throw std::runtime_error("string_substr: 'from' out of range");
            }
            size_t len = static_cast<size_t>(length);
            std::string sub = s.substr(pos, len);
            return Value(sub);
        });
    }
};
} // namespace Modules
#endif // MODULES_STRINGMODULE_HPP
src/Modules/BuiltIn/VariableHelpersModule.hpp
New file
@@ -0,0 +1,48 @@
// VariableHelpersModule.hpp
#ifndef MODULES_VARIABLEHELPERSMODULE_HPP
#define MODULES_VARIABLEHELPERSMODULE_HPP
#include <stdexcept>
#include <string>
#include <vector>
#include "Modules/BaseModule.hpp"
#include "Modules/ModuleManager.hpp"
#include "Symbols/Value.hpp"
#include "Symbols/VariableTypes.hpp"
namespace Modules {
/**
 * @brief Module providing helper functions for variables.
 * Currently supports:
 *   typeof($var)            -> returns string name of type
 *   typeof($var, "int")   -> returns bool if type matches
 */
class VariableHelpersModule : public BaseModule {
  public:
    void registerModule() override {
        auto & mgr = ModuleManager::instance();
        mgr.registerFunction("typeof", [](const std::vector<Symbols::Value> & args) {
            using namespace Symbols;
            if (args.size() == 1) {
                auto t = args[0].getType();
                return Value(Variables::TypeToString(t));
            }
            if (args.size() == 2) {
                auto        t    = args[0].getType();
                std::string name = Variables::TypeToString(t);
                if (args[1].getType() != Variables::Type::STRING) {
                    throw std::runtime_error("Second argument to typeof must be string");
                }
                bool match = (name == args[1].get<std::string>());
                return Value(match);
            }
            throw std::runtime_error("typeof expects 1 or 2 arguments");
        });
    }
};
}  // namespace Modules
#endif  // MODULES_VARIABLEHELPERSMODULE_HPP
src/Modules/JsonModule.hpp
File was deleted
src/Modules/PrintModule.hpp
File was deleted
src/Modules/PrintNlModule.hpp
File was deleted
src/Modules/TypeofModule.hpp
File was deleted
src/Parser/Parser.cpp
@@ -55,8 +55,15 @@
void Parser::parseVariableDefinition() {
    Symbols::Variables::Type var_type = parseType();
    Lexer::Tokens::Token id_token = expect(Lexer::Tokens::Type::VARIABLE_IDENTIFIER);
    std::string          var_name = id_token.value;
    // Variable name: allow names with or without leading '$'
    Lexer::Tokens::Token id_token;
    if (currentToken().type == Lexer::Tokens::Type::VARIABLE_IDENTIFIER ||
        currentToken().type == Lexer::Tokens::Type::IDENTIFIER) {
        id_token = consumeToken();
    } else {
        reportError("Expected variable name", currentToken());
    }
    std::string var_name = id_token.value;
    if (!var_name.empty() && var_name[0] == '$') {
        var_name = var_name.substr(1);
@@ -154,23 +161,39 @@
std::unique_ptr<Interpreter::StatementNode> Parser::parseForStatementNode() {
    auto forToken = expect(Lexer::Tokens::Type::KEYWORD, "for");
    expect(Lexer::Tokens::Type::PUNCTUATION, "(");
    Symbols::Variables::Type keyType = parseType();
    auto                     keyTok  = expect(Lexer::Tokens::Type::VARIABLE_IDENTIFIER);
    std::string              keyName = keyTok.value;
    if (!keyName.empty() && keyName[0] == '$') {
        keyName = keyName.substr(1);
    // Parse element type and variable name
    Symbols::Variables::Type elemType = parseType();
    auto firstTok = expect(Lexer::Tokens::Type::VARIABLE_IDENTIFIER);
    std::string firstName = firstTok.value;
    if (!firstName.empty() && firstName[0] == '$') {
        firstName = firstName.substr(1);
    }
    expect(Lexer::Tokens::Type::PUNCTUATION, ",");
    if (!(currentToken().type == Lexer::Tokens::Type::IDENTIFIER && currentToken().value == "auto")) {
        reportError("Expected 'auto' in for-in loop");
    // Determine loop form: key,value or simple element loop
    std::string keyName, valName;
    Symbols::Variables::Type keyType;
    if (match(Lexer::Tokens::Type::PUNCTUATION, ",")) {
        // Key, value syntax
        keyType = elemType;
        keyName = firstName;
        // Expect 'auto' for value variable
        if (!(currentToken().type == Lexer::Tokens::Type::IDENTIFIER && currentToken().value == "auto")) {
            reportError("Expected 'auto' in for-in loop");
        }
        consumeToken();
        auto valTok = expect(Lexer::Tokens::Type::VARIABLE_IDENTIFIER);
        valName = valTok.value;
        if (!valName.empty() && valName[0] == '$') {
            valName = valName.substr(1);
        }
        expect(Lexer::Tokens::Type::PUNCTUATION, ":");
    } else if (match(Lexer::Tokens::Type::PUNCTUATION, ":")) {
        // Simple element loop
        keyType = elemType;
        keyName = firstName;
        valName = firstName;
    } else {
        reportError("Expected ',' or ':' in for-in loop");
    }
    consumeToken();
    auto        valTok  = expect(Lexer::Tokens::Type::VARIABLE_IDENTIFIER);
    std::string valName = valTok.value;
    if (!valName.empty() && valName[0] == '$') {
        valName = valName.substr(1);
    }
    expect(Lexer::Tokens::Type::PUNCTUATION, ":");
    auto iterableExpr = parseParsedExpression(Symbols::Variables::Type::NULL_TYPE);
    expect(Lexer::Tokens::Type::PUNCTUATION, ")");
    expect(Lexer::Tokens::Type::PUNCTUATION, "{");
@@ -401,25 +424,39 @@
    // 'for'
    auto forToken = expect(Lexer::Tokens::Type::KEYWORD, "for");
    expect(Lexer::Tokens::Type::PUNCTUATION, "(");
    // Parse key type and name
    Symbols::Variables::Type keyType = parseType();
    auto                     keyTok  = expect(Lexer::Tokens::Type::VARIABLE_IDENTIFIER);
    std::string              keyName = keyTok.value;
    if (!keyName.empty() && keyName[0] == '$') {
        keyName = keyName.substr(1);
    // Parse element type and variable name
    Symbols::Variables::Type elemType = parseType();
    auto firstTok = expect(Lexer::Tokens::Type::VARIABLE_IDENTIFIER);
    std::string firstName = firstTok.value;
    if (!firstName.empty() && firstName[0] == '$') {
        firstName = firstName.substr(1);
    }
    expect(Lexer::Tokens::Type::PUNCTUATION, ",");
    // Parse 'auto' keyword for value
    if (!(currentToken().type == Lexer::Tokens::Type::IDENTIFIER && currentToken().value == "auto")) {
        reportError("Expected 'auto' in for-in loop");
    // Determine loop form: key,value or simple element loop
    std::string keyName, valName;
    Symbols::Variables::Type keyType;
    if (match(Lexer::Tokens::Type::PUNCTUATION, ",")) {
        // Key, value syntax
        keyType = elemType;
        keyName = firstName;
        // Expect 'auto' for value variable
        if (!(currentToken().type == Lexer::Tokens::Type::IDENTIFIER && currentToken().value == "auto")) {
            reportError("Expected 'auto' in for-in loop");
        }
        consumeToken();
        auto valTok = expect(Lexer::Tokens::Type::VARIABLE_IDENTIFIER);
        valName = valTok.value;
        if (!valName.empty() && valName[0] == '$') {
            valName = valName.substr(1);
        }
        expect(Lexer::Tokens::Type::PUNCTUATION, ":");
    } else if (match(Lexer::Tokens::Type::PUNCTUATION, ":")) {
        // Simple element loop
        keyType = elemType;
        keyName = firstName;
        valName = firstName;
    } else {
        reportError("Expected ',' or ':' in for-in loop");
    }
    consumeToken();
    auto        valTok  = expect(Lexer::Tokens::Type::VARIABLE_IDENTIFIER);
    std::string valName = valTok.value;
    if (!valName.empty() && valName[0] == '$') {
        valName = valName.substr(1);
    }
    expect(Lexer::Tokens::Type::PUNCTUATION, ":");
    // Parse iterable expression
    auto iterableExpr = parseParsedExpression(Symbols::Variables::Type::NULL_TYPE);
    expect(Lexer::Tokens::Type::PUNCTUATION, ")");
@@ -535,9 +572,52 @@
    }
    bool expect_unary = true;
    // Track if at start of expression (to distinguish array literal vs indexing)
    bool atStart = true;
    while (true) {
        auto token = currentToken();
        // Array literal (at start) or dynamic indexing (postfix)
        if (token.type == Lexer::Tokens::Type::PUNCTUATION && token.value == "[") {
            if (atStart) {
                // Parse array literal as object with numeric keys
                consumeToken(); // consume '['
                std::vector<std::pair<std::string, ParsedExpressionPtr>> members;
                size_t idx = 0;
                // Elements until ']'
                if (!(currentToken().type == Lexer::Tokens::Type::PUNCTUATION && currentToken().value == "]")) {
                    while (true) {
                        auto elem = parseParsedExpression(Symbols::Variables::Type::NULL_TYPE);
                        members.emplace_back(std::to_string(idx++), std::move(elem));
                        if (match(Lexer::Tokens::Type::PUNCTUATION, ",")) {
                            continue;
                        }
                        break;
                    }
                }
                expect(Lexer::Tokens::Type::PUNCTUATION, "]");
                // Build as object literal
                output_queue.push_back(ParsedExpression::makeObject(std::move(members)));
                expect_unary = false;
                atStart = false;
                continue;
            } else {
                // Parse dynamic array/object indexing: lhs[index]
                consumeToken(); // consume '['
                auto indexExpr = parseParsedExpression(Symbols::Variables::Type::NULL_TYPE);
                expect(Lexer::Tokens::Type::PUNCTUATION, "]");
                if (output_queue.empty()) {
                    reportError("Missing array/object for indexing");
                }
                auto lhsExpr = std::move(output_queue.back());
                output_queue.pop_back();
                auto accessExpr = ParsedExpression::makeBinary("[]", std::move(lhsExpr), std::move(indexExpr));
                output_queue.push_back(std::move(accessExpr));
                expect_unary = false;
                atStart = false;
                continue;
            }
        }
        // Object literal: { key: value, ... }
        if (token.type == Lexer::Tokens::Type::PUNCTUATION && token.value == "{") {
            // Consume '{'
@@ -737,8 +817,10 @@
                    Parser::reportError("Invalid type", token, "literal or variable");
                }
            }
            // Consume operand and mark that expression start has passed
            consumeToken();
            expect_unary = false;
            atStart = false;
        } else {
            break;
        }
@@ -853,7 +935,12 @@
    // Direct lookup for type keyword
    auto         it    = Parser::variable_types.find(token.type);
    if (it != Parser::variable_types.end()) {
        // Base type
        consumeToken();
        // Array type syntax: baseType[] -> treat as OBJECT/array map
        if (match(Lexer::Tokens::Type::PUNCTUATION, "[") && match(Lexer::Tokens::Type::PUNCTUATION, "]")) {
            return Symbols::Variables::Type::OBJECT;
        }
        return it->second;
    }
    reportError("Expected type keyword (string, int, double, float)");
src/VoidScript.hpp
@@ -6,12 +6,18 @@
#include "Interpreter/Interpreter.hpp"
#include "Lexer/Lexer.hpp"
#include "Modules/BuiltIn/PrintModule.hpp"
#include "Modules/ModuleManager.hpp"
#include "Modules/PrintNlModule.hpp"
#include "Modules/PrintModule.hpp"
#include "Modules/TypeofModule.hpp"
#include "Modules/FileModule.hpp"
#include "Modules/JsonModule.hpp"
// Variable helper functions (typeof)
#include "Modules/BuiltIn/VariableHelpersModule.hpp"
// String helper functions
#include "Modules/BuiltIn/StringModule.hpp"
// Array helper functions (sizeof)
#include "Modules/BuiltIn/ArrayModule.hpp"
// File I/O
#include "Modules/BuiltIn/FileModule.hpp"
// JSON encode/decode
#include "Modules/BuiltIn/JsonModule.hpp"
#include "Parser/Parser.hpp"
class VoidScript {
@@ -55,10 +61,14 @@
        lexer(std::make_shared<Lexer::Lexer>()),
        parser(std::make_shared<Parser::Parser>()) {
        // Register built-in modules (print, etc.)
        // print functions
        Modules::ModuleManager::instance().addModule(std::make_unique<Modules::PrintModule>());
        Modules::ModuleManager::instance().addModule(std::make_unique<Modules::PrintNlModule>());
        // typeof() builtin
        Modules::ModuleManager::instance().addModule(std::make_unique<Modules::TypeofModule>());
        // variable helpers (typeof)
        Modules::ModuleManager::instance().addModule(std::make_unique<Modules::VariableHelpersModule>());
        // string helper functions
        Modules::ModuleManager::instance().addModule(std::make_unique<Modules::StringModule>());
        // array helper functions (sizeof)
        Modules::ModuleManager::instance().addModule(std::make_unique<Modules::ArrayModule>());
        // file I/O builtin
        Modules::ModuleManager::instance().addModule(std::make_unique<Modules::FileModule>());
        // JSON encode/decode builtin
test_scripts/array.vs
@@ -1,8 +1,20 @@
array[string] $array = ["cat", "dog", "girafe"];
string[] $array = ["cat", "dog", "girafe"];
array[int] $intArray = [10,11,0,1,2,44,2];
int[] $intArray = [10,11,0,1,2,44,2];
array[int] $emptyIntArray = [];
int[] $emptyIntArray = [];
printnl("First key: ", $array[0]);
int index = 0;
for (string $value : $array) {
    printnl("$value =",$value, " ?= $array[$index] =", $array[$index]);
    $index = $index + 1;
}
int $size = sizeof($array);
printnl("The size of the $array: ", $size);
test_scripts/test_array.vs
New file
@@ -0,0 +1,2 @@
int[] $arr = [0,1,2,3];
printnl(sizeof($arr));
test_scripts/test_string.vs
New file
@@ -0,0 +1,3 @@
string $s = "hello";
printnl(sizeof($s));
printnl(sizeof("abc"));
test_scripts/test_substr.vs
New file
@@ -0,0 +1,4 @@
string $s = "hello_world";
printnl(string_substr($s, 6, 5));
printnl(string_substr($s, 0, 5));
printnl(string_substr($s, 0, 50));