A simple scripting language in C++
Ferenc Szontágh
2025-04-18 d092df5264f6e48f9d59650c092b8297382b1316
some built-in io module
7 files modified
4 files added
165 ■■■■ changed files
src/Interpreter/CallStatementNode.hpp 18 ●●●●● patch | view | raw | blame | history
src/Interpreter/ConditionalStatementNode.hpp 8 ●●●● patch | view | raw | blame | history
src/Interpreter/DeclareFunctionStatementNode.hpp 7 ●●●● patch | view | raw | blame | history
src/Interpreter/DeclareVariableStatementNode.hpp 10 ●●●●● patch | view | raw | blame | history
src/Interpreter/ForStatementNode.hpp 12 ●●●● 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,15 +34,13 @@
        args_(std::move(args)) {}
    void interpret(Interpreter & interpreter) const override {
        try {
        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_)) {
@@ -50,7 +48,6 @@
                return;
            }
        }
        // Lookup function symbol in functions namespace
        SymbolContainer * sc        = SymbolContainer::instance();
        const std::string currentNs = sc->currentScopeName();
        const std::string fnSymNs   = currentNs + ".functions";
@@ -59,8 +56,6 @@
            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(
@@ -68,26 +63,25 @@
                " 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();
        } catch (const Exception &) {
            throw;
        } catch (const std::exception &e) {
            throw Exception(e.what(), filename_, line_, column_);
        }
    }
    std::string toString() const override {
src/Interpreter/ConditionalStatementNode.hpp
@@ -33,7 +33,7 @@
         elseBranch_(std::move(elseBranch)) {}
     void interpret(class Interpreter & interpreter) const override {
         // Evaluate condition
         try {
         auto val = condition_->evaluate(interpreter);
         bool cond = false;
         if (val.getType() == Symbols::Variables::Type::BOOLEAN) {
@@ -41,11 +41,15 @@
        } 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);
         }
         } catch (const Exception &) {
             throw;
         } catch (const std::exception &e) {
             throw Exception(e.what(), filename_, line_, column_);
         }
     }
     std::string toString() const override {
src/Interpreter/DeclareFunctionStatementNode.hpp
@@ -37,12 +37,17 @@
        ns(ns) {}
    void interpret(Interpreter & /*interpreter*/) const override {
        //Symbols::Value value = expression_->evaluate(interpreter);
        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_);
        }
    }
    std::string toString() const override {
src/Interpreter/DeclareVariableStatementNode.hpp
@@ -33,13 +33,11 @@
        ns(ns) {}
    void interpret(Interpreter & interpreter) const override {
        // Evaluate the expression and enforce declared type matches actual value type
        try {
        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_);
        }
        // Enforce type correctness: the evaluated value must match the declared type
        if (value.getType() != variableType_) {
            using namespace Symbols::Variables;
            std::string expected = TypeToString(variableType_);
@@ -49,9 +47,13 @@
                "': 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);
        } catch (const Exception &) {
            throw;
        } catch (const std::exception &e) {
            throw Exception(e.what(), filename_, line_, column_);
        }
    }
    std::string toString() const override {
src/Interpreter/ForStatementNode.hpp
@@ -43,20 +43,17 @@
        body_(std::move(body)) {}
    void interpret(Interpreter & interpreter) const override {
        try {
        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)) {
@@ -66,7 +63,6 @@
                auto sym = symContainer->get(var_ns, keyName_);
                sym->setValue(keyVal);
            }
            // Value binding
            Value valVal = entry.second;
            if (!symContainer->exists(valueName_, var_ns)) {
                auto sym = SymbolFactory::createVariable(valueName_, valVal, base_ns);
@@ -75,11 +71,15 @@
                auto sym = symContainer->get(var_ns, valueName_);
                sym->setValue(valVal);
            }
            // 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_);
        }
    }
    std::string toString() const override {
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");