A simple scripting language in C++
Ferenc Szontágh
2025-04-18 d092df5264f6e48f9d59650c092b8297382b1316
some built-in io module
7 files modified
4 files added
357 ■■■■■ changed files
src/Interpreter/CallStatementNode.hpp 98 ●●●● patch | view | raw | blame | history
src/Interpreter/ConditionalStatementNode.hpp 28 ●●●●● patch | view | raw | blame | history
src/Interpreter/DeclareFunctionStatementNode.hpp 15 ●●●●● patch | view | raw | blame | history
src/Interpreter/DeclareVariableStatementNode.hpp 38 ●●●● patch | view | raw | blame | history
src/Interpreter/ForStatementNode.hpp 68 ●●●● patch | view | raw | blame | history
src/Modules/BaseModule.hpp 3 ●●●●● patch | view | raw | blame | history
src/Modules/FileModule.hpp 93 ●●●●● patch | view | raw | blame | history
src/VoidScript.hpp 3 ●●●●● patch | view | raw | blame | history
temp_test_io.txt 1 ●●●● patch | view | raw | blame | history
temp_test_io2.txt 1 ●●●● patch | view | raw | blame | history
test_scripts/file_io.vs 9 ●●●●● patch | view | raw | blame | history
src/Interpreter/CallStatementNode.hpp
@@ -34,60 +34,54 @@
        args_(std::move(args)) {}
    void interpret(Interpreter & interpreter) const override {
        using namespace Symbols;
        // Evaluate argument expressions
        std::vector<Value> argValues;
        argValues.reserve(args_.size());
        for (const auto & expr : args_) {
            argValues.push_back(expr->evaluate(interpreter));
        }
        // Handle built-in function callbacks
        {
            auto &mgr = Modules::ModuleManager::instance();
            if (mgr.hasFunction(functionName_)) {
                mgr.callFunction(functionName_, argValues);
                return;
        try {
            using namespace Symbols;
            std::vector<Value> argValues;
            argValues.reserve(args_.size());
            for (const auto & expr : args_) {
                argValues.push_back(expr->evaluate(interpreter));
            }
            {
                auto &mgr = Modules::ModuleManager::instance();
                if (mgr.hasFunction(functionName_)) {
                    mgr.callFunction(functionName_, argValues);
                    return;
                }
            }
            SymbolContainer * sc        = SymbolContainer::instance();
            const std::string currentNs = sc->currentScopeName();
            const std::string fnSymNs   = currentNs + ".functions";
            auto              sym       = sc->get(fnSymNs, functionName_);
            if (!sym || sym->getKind() != Kind::Function) {
                throw Exception("Function not found: " + functionName_, filename_, line_, column_);
            }
            auto funcSym = std::static_pointer_cast<FunctionSymbol>(sym);
            const auto & params = funcSym->parameters();
            if (params.size() != argValues.size()) {
                throw Exception(
                    "Function '" + functionName_ + "' expects " + std::to_string(params.size()) +
                    " args, got " + std::to_string(argValues.size()),
                    filename_, line_, column_);
            }
            const std::string fnOpNs = currentNs + "." + functionName_;
            sc->enter(fnOpNs);
            for (size_t i = 0; i < params.size(); ++i) {
                const auto &  p = params[i];
                const Value & v = argValues[i];
                auto varSym = SymbolFactory::createVariable(p.name, v, fnOpNs);
                sc->add(varSym);
            }
            auto ops = Operations::Container::instance()->getAll(fnOpNs);
            auto it  = ops.begin();
            for (; it != ops.end(); ++it) {
                interpreter.runOperation(*(*it));
            }
            sc->enterPreviousScope();
        } catch (const Exception &) {
            throw;
        } catch (const std::exception &e) {
            throw Exception(e.what(), filename_, line_, column_);
        }
        // Lookup function symbol in functions namespace
        SymbolContainer * sc        = SymbolContainer::instance();
        const std::string currentNs = sc->currentScopeName();
        const std::string fnSymNs   = currentNs + ".functions";
        auto              sym       = sc->get(fnSymNs, functionName_);
        if (!sym || sym->getKind() != Kind::Function) {
            throw Exception("Function not found: " + functionName_, filename_, line_, column_);
        }
        auto funcSym = std::static_pointer_cast<FunctionSymbol>(sym);
        // Check parameter count
        const auto & params = funcSym->parameters();
        if (params.size() != argValues.size()) {
            throw Exception(
                "Function '" + functionName_ + "' expects " + std::to_string(params.size()) +
                " args, got " + std::to_string(argValues.size()),
                filename_, line_, column_);
        }
        // Enter function scope to bind parameters and execute body
        const std::string fnOpNs = currentNs + "." + functionName_;
        sc->enter(fnOpNs);
        // Bind parameters as local variables
        for (size_t i = 0; i < params.size(); ++i) {
            const auto &  p = params[i];
            const Value & v = argValues[i];
            auto varSym = SymbolFactory::createVariable(p.name, v, fnOpNs);
            sc->add(varSym);
        }
        // Execute function body operations
        auto ops = Operations::Container::instance()->getAll(fnOpNs);
        auto it  = ops.begin();
        for (; it != ops.end(); ++it) {
            interpreter.runOperation(*(*it));
        }
        // Exit function scope
        sc->enterPreviousScope();
    }
    std::string toString() const override {
src/Interpreter/ConditionalStatementNode.hpp
@@ -33,18 +33,22 @@
         elseBranch_(std::move(elseBranch)) {}
     void interpret(class Interpreter & interpreter) const override {
         // Evaluate condition
         auto val = condition_->evaluate(interpreter);
         bool cond = false;
         if (val.getType() == Symbols::Variables::Type::BOOLEAN) {
             cond = val.get<bool>();
        } else {
            throw Exception("Condition did not evaluate to boolean", filename_, line_, column_);
        }
         // Execute appropriate branch
         const auto & branch = cond ? thenBranch_ : elseBranch_;
         for (const auto & stmt : branch) {
             stmt->interpret(interpreter);
         try {
             auto val = condition_->evaluate(interpreter);
             bool cond = false;
             if (val.getType() == Symbols::Variables::Type::BOOLEAN) {
                 cond = val.get<bool>();
             } else {
                 throw Exception("Condition did not evaluate to boolean", filename_, line_, column_);
             }
             const auto & branch = cond ? thenBranch_ : elseBranch_;
             for (const auto & stmt : branch) {
                 stmt->interpret(interpreter);
             }
         } catch (const Exception &) {
             throw;
         } catch (const std::exception &e) {
             throw Exception(e.what(), filename_, line_, column_);
         }
     }
src/Interpreter/DeclareFunctionStatementNode.hpp
@@ -37,12 +37,17 @@
        ns(ns) {}
    void interpret(Interpreter & /*interpreter*/) const override {
        //Symbols::Value value = expression_->evaluate(interpreter);
        if (Symbols::SymbolContainer::instance()->exists(functionName_)) {
            throw Exception("Function already declared: " + functionName_, filename_, line_, column_);
        try {
            if (Symbols::SymbolContainer::instance()->exists(functionName_)) {
                throw Exception("Function already declared: " + functionName_, filename_, line_, column_);
            }
            const auto func = Symbols::SymbolFactory::createFunction(functionName_, ns, params_, "", returnType_);
            Symbols::SymbolContainer::instance()->add(func);
        } catch (const Exception &) {
            throw;
        } catch (const std::exception &e) {
            throw Exception(e.what(), filename_, line_, column_);
        }
        const auto func = Symbols::SymbolFactory::createFunction(functionName_, ns, params_, "", returnType_);
        Symbols::SymbolContainer::instance()->add(func);
    }
    std::string toString() const override {
src/Interpreter/DeclareVariableStatementNode.hpp
@@ -33,25 +33,27 @@
        ns(ns) {}
    void interpret(Interpreter & interpreter) const override {
        // Evaluate the expression and enforce declared type matches actual value type
        Symbols::Value value = expression_->evaluate(interpreter);
        // Check for duplicate declaration
        if (Symbols::SymbolContainer::instance()->exists(variableName_)) {
            throw Exception("Variable already declared: " + variableName_, filename_, line_, column_);
        try {
            Symbols::Value value = expression_->evaluate(interpreter);
            if (Symbols::SymbolContainer::instance()->exists(variableName_)) {
                throw Exception("Variable already declared: " + variableName_, filename_, line_, column_);
            }
            if (value.getType() != variableType_) {
                using namespace Symbols::Variables;
                std::string expected = TypeToString(variableType_);
                std::string actual   = TypeToString(value.getType());
                throw Exception(
                    "Type mismatch for variable '" + variableName_ +
                    "': expected '" + expected + "' but got '" + actual + "'",
                    filename_, line_, column_);
            }
            const auto variable = Symbols::SymbolFactory::createVariable(variableName_, value, ns, variableType_);
            Symbols::SymbolContainer::instance()->add(variable);
        } catch (const Exception &) {
            throw;
        } catch (const std::exception &e) {
            throw Exception(e.what(), filename_, line_, column_);
        }
        // Enforce type correctness: the evaluated value must match the declared type
        if (value.getType() != variableType_) {
            using namespace Symbols::Variables;
            std::string expected = TypeToString(variableType_);
            std::string actual   = TypeToString(value.getType());
            throw Exception(
                "Type mismatch for variable '" + variableName_ +
                "': expected '" + expected + "' but got '" + actual + "'",
                filename_, line_, column_);
        }
        // Create and add the variable symbol
        const auto variable = Symbols::SymbolFactory::createVariable(variableName_, value, ns, variableType_);
        Symbols::SymbolContainer::instance()->add(variable);
    }
    std::string toString() const override {
src/Interpreter/ForStatementNode.hpp
@@ -43,42 +43,42 @@
        body_(std::move(body)) {}
    void interpret(Interpreter & interpreter) const override {
        using namespace Symbols;
        // Evaluate iterable expression
        auto iterableVal = iterableExpr_->evaluate(interpreter);
        if (iterableVal.getType() != Variables::Type::OBJECT) {
            throw Exception("For-in loop applied to non-object", filename_, line_, column_);
        }
        // Access underlying object map
        const auto & objMap = std::get<Value::ObjectMap>(iterableVal.get());
        auto * symContainer = SymbolContainer::instance();
        const std::string base_ns = symContainer->currentScopeName();
        const std::string var_ns  = base_ns + ".variables";
        // Iterate through object entries
        for (const auto & entry : objMap) {
            // Key binding
            const std::string & key = entry.first;
            Value keyVal(key);
            if (!symContainer->exists(keyName_, var_ns)) {
                auto sym = SymbolFactory::createVariable(keyName_, keyVal, base_ns);
                symContainer->add(sym);
            } else {
                auto sym = symContainer->get(var_ns, keyName_);
                sym->setValue(keyVal);
        try {
            using namespace Symbols;
            auto iterableVal = iterableExpr_->evaluate(interpreter);
            if (iterableVal.getType() != Variables::Type::OBJECT) {
                throw Exception("For-in loop applied to non-object", filename_, line_, column_);
            }
            // Value binding
            Value valVal = entry.second;
            if (!symContainer->exists(valueName_, var_ns)) {
                auto sym = SymbolFactory::createVariable(valueName_, valVal, base_ns);
                symContainer->add(sym);
            } else {
                auto sym = symContainer->get(var_ns, valueName_);
                sym->setValue(valVal);
            const auto & objMap = std::get<Value::ObjectMap>(iterableVal.get());
            auto * symContainer = SymbolContainer::instance();
            const std::string base_ns = symContainer->currentScopeName();
            const std::string var_ns  = base_ns + ".variables";
            for (const auto & entry : objMap) {
                const std::string & key = entry.first;
                Value keyVal(key);
                if (!symContainer->exists(keyName_, var_ns)) {
                    auto sym = SymbolFactory::createVariable(keyName_, keyVal, base_ns);
                    symContainer->add(sym);
                } else {
                    auto sym = symContainer->get(var_ns, keyName_);
                    sym->setValue(keyVal);
                }
                Value valVal = entry.second;
                if (!symContainer->exists(valueName_, var_ns)) {
                    auto sym = SymbolFactory::createVariable(valueName_, valVal, base_ns);
                    symContainer->add(sym);
                } else {
                    auto sym = symContainer->get(var_ns, valueName_);
                    sym->setValue(valVal);
                }
                for (const auto & stmt : body_) {
                    stmt->interpret(interpreter);
                }
            }
            // Execute loop body
            for (const auto & stmt : body_) {
                stmt->interpret(interpreter);
            }
        } catch (const Exception &) {
            throw;
        } catch (const std::exception &e) {
            throw Exception(e.what(), filename_, line_, column_);
        }
    }
src/Modules/BaseModule.hpp
@@ -2,9 +2,6 @@
#ifndef MODULES_BASEMODULE_HPP
#define MODULES_BASEMODULE_HPP
#include <string>
#include "Symbols/SymbolContainer.hpp"
#include "Symbols/SymbolFactory.hpp"
namespace Modules {
src/Modules/FileModule.hpp
New file
@@ -0,0 +1,93 @@
// FileModule.hpp
#ifndef MODULES_FILEMODULE_HPP
#define MODULES_FILEMODULE_HPP
#include <filesystem>
#include <fstream>
#include <iterator>
#include <stdexcept>
#include <vector>
#include <string>
#include "BaseModule.hpp"
#include "ModuleManager.hpp"
#include "Symbols/Value.hpp"
namespace Modules {
/**
 * @brief Module providing simple file I/O functions:
 *  file_get_contents(filename) -> string content
 *  file_put_contents(filename, content, overwrite) -> undefined, throws on error
 *  file_exists(filename) -> bool
 */
class FileModule : public BaseModule {
  public:
    void registerModule() override {
        auto &mgr = ModuleManager::instance();
        // Read entire file content
        mgr.registerFunction("file_get_contents", [](const std::vector<Symbols::Value> &args) {
            using namespace Symbols;
            if (args.size() != 1) {
                throw std::runtime_error("file_get_contents expects 1 argument");
            }
            if (args[0].getType() != Variables::Type::STRING) {
                throw std::runtime_error("file_get_contents expects string filename");
            }
            const std::string filename = args[0].get<std::string>();
            if (!std::filesystem::exists(filename)) {
                throw std::runtime_error("File does not exist: " + filename);
            }
            std::ifstream input(filename, std::ios::in | std::ios::binary);
            if (!input.is_open()) {
                throw std::runtime_error("Could not open file: " + filename);
            }
            std::string content((std::istreambuf_iterator<char>(input)), std::istreambuf_iterator<char>());
            input.close();
            return Value(content);
        });
        // Write content to file, with optional overwrite
        mgr.registerFunction("file_put_contents", [](const std::vector<Symbols::Value> &args) {
            using namespace Symbols;
            if (args.size() != 3) {
                throw std::runtime_error("file_put_contents expects 3 arguments");
            }
            if (args[0].getType() != Variables::Type::STRING ||
                args[1].getType() != Variables::Type::STRING ||
                args[2].getType() != Variables::Type::BOOLEAN) {
                throw std::runtime_error("file_put_contents expects (string, string, bool)");
            }
            const std::string filename = args[0].get<std::string>();
            const std::string content = args[1].get<std::string>();
            const bool overwrite = args[2].get<bool>();
            if (!overwrite && std::filesystem::exists(filename)) {
                throw std::runtime_error("File already exists: " + filename);
            }
            std::ofstream output(filename, std::ios::out | std::ios::binary | std::ios::trunc);
            if (!output.is_open()) {
                throw std::runtime_error("Could not open file for writing: " + filename);
            }
            output << content;
            if (!output) {
                throw std::runtime_error("Failed to write to file: " + filename);
            }
            output.close();
            return Value();
        });
        // Check if file exists
        mgr.registerFunction("file_exists", [](const std::vector<Symbols::Value> &args) {
            using namespace Symbols;
            if (args.size() != 1) {
                throw std::runtime_error("file_exists expects 1 argument");
            }
            if (args[0].getType() != Variables::Type::STRING) {
                throw std::runtime_error("file_exists expects string filename");
            }
            const std::string filename = args[0].get<std::string>();
            bool exists = std::filesystem::exists(filename);
            return Value(exists);
        });
    }
};
} // namespace Modules
#endif // MODULES_FILEMODULE_HPP
src/VoidScript.hpp
@@ -10,6 +10,7 @@
#include "Modules/PrintNlModule.hpp"
#include "Modules/PrintModule.hpp"
#include "Modules/TypeofModule.hpp"
#include "Modules/FileModule.hpp"
#include "Parser/Parser.hpp"
class VoidScript {
@@ -57,6 +58,8 @@
        Modules::ModuleManager::instance().addModule(std::make_unique<Modules::PrintNlModule>());
        // typeof() builtin
        Modules::ModuleManager::instance().addModule(std::make_unique<Modules::TypeofModule>());
        // file I/O builtin
        Modules::ModuleManager::instance().addModule(std::make_unique<Modules::FileModule>());
        this->files.emplace(this->files.begin(), file);
        lexer->setKeyWords(Parser::Parser::keywords);
temp_test_io.txt
New file
@@ -0,0 +1 @@
hello
temp_test_io2.txt
New file
@@ -0,0 +1 @@
second
test_scripts/file_io.vs
New file
@@ -0,0 +1,9 @@
# File I/O Feature Test
string $f = "test_file_io.txt";
print("file_exists before create: ", file_exists($f), "\n");
file_put_contents($f, "Hello from VoidScript!", false);
print("file_exists after create: ", file_exists($f), "\n");
print("file_get_contents: ", file_get_contents($f), "\n");
# Overwrite with permission
file_put_contents($f, "Overwritten content", true);
print("file_get_contents after overwrite: ", file_get_contents($f), "\n");