From 8cdccae214de1d2477d14a495aeb29abb4a58eeb Mon Sep 17 00:00:00 2001 From: Bobby Lucero Date: Tue, 12 Aug 2025 00:16:36 -0400 Subject: [PATCH] Built in modules, user modules, ability to disable builtin modules --- CMakeLists.txt | 3 + Reference/EMBEDDING.md | 113 ++++++++++++++ bobby.bob | 13 ++ src/headers/builtinModules/register.h | 8 + src/headers/builtinModules/sys.h | 8 + src/headers/cli/bob.h | 34 ++++- src/headers/parsing/ErrorReporter.h | 1 + src/headers/parsing/Lexer.h | 5 + src/headers/parsing/Parser.h | 2 + src/headers/parsing/Statement.h | 28 ++++ src/headers/runtime/Environment.h | 4 + src/headers/runtime/Executor.h | 2 + src/headers/runtime/Interpreter.h | 30 ++++ src/headers/runtime/ModuleDef.h | 14 ++ src/headers/runtime/ModuleRegistry.h | 70 +++++++++ src/headers/runtime/Value.h | 35 ++++- src/sources/builtinModules/register.cpp | 8 + src/sources/builtinModules/sys.cpp | 146 ++++++++++++++++++ src/sources/cli/bob.cpp | 94 +++++------- src/sources/cli/main.cpp | 11 ++ src/sources/parsing/Parser.cpp | 37 +++++ src/sources/runtime/Environment.cpp | 9 ++ src/sources/runtime/Evaluator.cpp | 29 +++- src/sources/runtime/Executor.cpp | 34 +++++ src/sources/runtime/Interpreter.cpp | 187 ++++++++++++++++++++++++ src/sources/runtime/Value.cpp | 8 + src/sources/stdlib/BobStdLib.cpp | 2 + test_bob_language.bob | 7 + tests.bob | 166 +++++++++++---------- tests/import_user_of_mod_hello.bob | 5 + tests/mod_hello.bob | 3 + tests/test_imports_basic.bob | 47 ++++++ tests/test_imports_builtin.bob | 42 ++++++ 33 files changed, 1063 insertions(+), 142 deletions(-) create mode 100644 Reference/EMBEDDING.md create mode 100644 bobby.bob create mode 100644 src/headers/builtinModules/register.h create mode 100644 src/headers/builtinModules/sys.h create mode 100644 src/headers/runtime/ModuleDef.h create mode 100644 src/headers/runtime/ModuleRegistry.h create mode 100644 src/sources/builtinModules/register.cpp create mode 100644 src/sources/builtinModules/sys.cpp create mode 100644 tests/import_user_of_mod_hello.bob create mode 100644 tests/mod_hello.bob create mode 100644 tests/test_imports_basic.bob create mode 100644 tests/test_imports_builtin.bob diff --git a/CMakeLists.txt b/CMakeLists.txt index f18a131..6b6c8c2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -48,6 +48,7 @@ endif() file(GLOB_RECURSE BOB_RUNTIME_SOURCES "src/sources/runtime/*.cpp") file(GLOB_RECURSE BOB_PARSING_SOURCES "src/sources/parsing/*.cpp") file(GLOB_RECURSE BOB_STDLIB_SOURCES "src/sources/stdlib/*.cpp") +file(GLOB_RECURSE BOB_BUILTIN_SOURCES "src/sources/builtinModules/*.cpp") file(GLOB_RECURSE BOB_CLI_SOURCES "src/sources/cli/*.cpp") # All source files @@ -55,6 +56,7 @@ set(BOB_ALL_SOURCES ${BOB_RUNTIME_SOURCES} ${BOB_PARSING_SOURCES} ${BOB_STDLIB_SOURCES} + ${BOB_BUILTIN_SOURCES} ${BOB_CLI_SOURCES} ) @@ -66,6 +68,7 @@ target_include_directories(bob PRIVATE src/headers/runtime src/headers/parsing src/headers/stdlib + src/headers/builtinModules src/headers/cli src/headers/common ) diff --git a/Reference/EMBEDDING.md b/Reference/EMBEDDING.md new file mode 100644 index 0000000..7012d88 --- /dev/null +++ b/Reference/EMBEDDING.md @@ -0,0 +1,113 @@ +Embedding Bob: Public API Guide +================================ + +This document explains how to embed the Bob interpreter in your C++ application, register custom modules, and control sandbox policies. + +Quick Start +----------- + +```cpp +#include "cli/bob.h" +#include "ModuleRegistry.h" + +int main() { + Bob bob; + + // Optional: configure policies or modules before first use + bob.setBuiltinModulePolicy(true); // allow builtin modules (default) + bob.setBuiltinModuleDenyList({/* e.g., "sys" */}); + + // Register a custom builtin module called "demo" + bob.registerModule("demo", [](ModuleRegistry::ModuleBuilder& m) { + m.fn("hello", [](std::vector args, int, int) -> Value { + std::string who = (args.size() >= 1 && args[0].isString()) ? args[0].asString() : "world"; + return Value(std::string("hello ") + who); + }); + m.val("meaning", Value(42.0)); + }); + + // Evaluate code from a string + bob.evalString("import demo; print(demo.hello(\"Bob\"));", ""); + + // Evaluate a file (imports inside resolve relative to the file's directory) + bob.evalFile("script.bob"); +} +``` + +API Overview +------------ + +Bob exposes a single high-level object with a minimal, consistent API. It self-manages an internal interpreter and applies configuration on first use. + +- Program execution + - `bool evalString(const std::string& code, const std::string& filename = "")` + - `bool evalFile(const std::string& path)` + - `void runFile(const std::string& path)` (CLI convenience – delegates to `evalFile`) + - `void runPrompt()` (interactive CLI – delegates each line to `evalString`) + +- Module registration and sandboxing + - `void registerModule(const std::string& name, std::function init)` + - `void setBuiltinModulePolicy(bool allow)` + - `void setBuiltinModuleAllowList(const std::vector& allowed)` + - `void setBuiltinModuleDenyList(const std::vector& denied)` + +- Global environment helpers + - `bool defineGlobal(const std::string& name, const Value& value)` + - `bool tryGetGlobal(const std::string& name, Value& out) const` + +All configuration calls are safe to use before any evaluation – they are queued and applied automatically when the interpreter is first created. + +Registering Custom Builtin Modules +---------------------------------- + +Use the builder convenience to create a module: + +```cpp +bob.registerModule("raylib", [](ModuleRegistry::ModuleBuilder& m) { + m.fn("init", [](std::vector args, int line, int col) -> Value { + // call into your library here; validate args, return Value + return NONE_VALUE; + }); + m.val("VERSION", Value(std::string("5.0"))); +}); +``` + +At runtime: + +```bob +import raylib; +raylib.init(); +print(raylib.VERSION); +``` + +Notes +----- + +- Modules are immutable, first-class objects. `type(module)` is "module" and `toString(module)` prints ``. +- Reassigning a module binding or setting module properties throws an error. + +Builtin Modules and Sandboxing +------------------------------ + +- Builtin modules (e.g., `sys`) are registered during interpreter construction. +- File imports are always resolved relative to the importing file's directory. +- Policies: + - `setBuiltinModulePolicy(bool allow)` – enable/disable all builtin modules. + - `setBuiltinModuleAllowList(vector)` – allow only listed modules (deny others). + - `setBuiltinModuleDenyList(vector)` – explicitly deny listed modules. +- Denied/disabled modules are cloaked: `import name` reports "Module not found". + +Error Reporting +--------------- + +- `evalString`/`evalFile` set file context for error reporting so line/column references point to the real source. +- Both return `true` on success and `false` if execution failed (errors are reported via the internal error reporter). + +CLI vs Embedding +---------------- + +- CLI builds include `main.cpp` (entry point), which uses `Bob::runFile` or `Bob::runPrompt`. +- Embedded hosts do not use `main.cpp`; instead they instantiate `Bob` and call `evalString`/`evalFile` directly. + + + diff --git a/bobby.bob b/bobby.bob new file mode 100644 index 0000000..135c658 --- /dev/null +++ b/bobby.bob @@ -0,0 +1,13 @@ +class A { + var inner = 10; + + func test(){ + print(this.inner); + } +} + + +func hello(){ + print("hello"); +} + diff --git a/src/headers/builtinModules/register.h b/src/headers/builtinModules/register.h new file mode 100644 index 0000000..07f834d --- /dev/null +++ b/src/headers/builtinModules/register.h @@ -0,0 +1,8 @@ +#pragma once + +class Interpreter; + +// Registers all builtin modules with the interpreter +void registerAllBuiltinModules(Interpreter& interpreter); + + diff --git a/src/headers/builtinModules/sys.h b/src/headers/builtinModules/sys.h new file mode 100644 index 0000000..244cd43 --- /dev/null +++ b/src/headers/builtinModules/sys.h @@ -0,0 +1,8 @@ +#pragma once + +class Interpreter; + +// Register the builtin 'sys' module +void registerSysModule(Interpreter& interpreter); + + diff --git a/src/headers/cli/bob.h b/src/headers/cli/bob.h index ea69725..9fd4298 100644 --- a/src/headers/cli/bob.h +++ b/src/headers/cli/bob.h @@ -5,6 +5,7 @@ #include #include "Lexer.h" #include "Interpreter.h" +#include "ModuleRegistry.h" #include "helperFunctions/ShortHands.h" #include "ErrorReporter.h" @@ -20,10 +21,41 @@ public: ~Bob() = default; public: + // Embedding helpers (bridge to internal interpreter) + void registerModule(const std::string& name, std::function init) { + if (interpreter) interpreter->registerModule(name, init); + else pendingConfigurators.push_back([name, init](Interpreter& I){ I.registerModule(name, init); }); + } + void setBuiltinModulePolicy(bool allow) { + if (interpreter) interpreter->setBuiltinModulePolicy(allow); + else pendingConfigurators.push_back([allow](Interpreter& I){ I.setBuiltinModulePolicy(allow); }); + } + void setBuiltinModuleAllowList(const std::vector& allowed) { + if (interpreter) interpreter->setBuiltinModuleAllowList(allowed); + else pendingConfigurators.push_back([allowed](Interpreter& I){ I.setBuiltinModuleAllowList(allowed); }); + } + void setBuiltinModuleDenyList(const std::vector& denied) { + if (interpreter) interpreter->setBuiltinModuleDenyList(denied); + else pendingConfigurators.push_back([denied](Interpreter& I){ I.setBuiltinModuleDenyList(denied); }); + } + bool defineGlobal(const std::string& name, const Value& v) { + if (interpreter) return interpreter->defineGlobalVar(name, v); + pendingConfigurators.push_back([name, v](Interpreter& I){ I.defineGlobalVar(name, v); }); + return true; + } + bool tryGetGlobal(const std::string& name, Value& out) const { return interpreter ? interpreter->tryGetGlobalVar(name, out) : false; } void runFile(const std::string& path); void runPrompt(); + bool evalFile(const std::string& path); + bool evalString(const std::string& code, const std::string& filename = ""); private: - void run(std::string source); + void ensureInterpreter(bool interactive); + void applyPendingConfigs() { + if (!interpreter) return; + for (auto& f : pendingConfigurators) { f(*interpreter); } + pendingConfigurators.clear(); + } + std::vector> pendingConfigurators; }; diff --git a/src/headers/parsing/ErrorReporter.h b/src/headers/parsing/ErrorReporter.h index fa06771..5f6eca2 100644 --- a/src/headers/parsing/ErrorReporter.h +++ b/src/headers/parsing/ErrorReporter.h @@ -64,6 +64,7 @@ public: // Source push/pop for eval void pushSource(const std::string& source, const std::string& fileName); void popSource(); + const std::string& getCurrentFileName() const { return currentFileName; } private: void displaySourceContext(int line, int column, const std::string& errorType, const std::string& message, const std::string& operator_ = "", bool showArrow = true); diff --git a/src/headers/parsing/Lexer.h b/src/headers/parsing/Lexer.h index 52beb55..59e98da 100644 --- a/src/headers/parsing/Lexer.h +++ b/src/headers/parsing/Lexer.h @@ -26,6 +26,7 @@ enum TokenType{ AND, OR, TRUE, FALSE, IF, ELSE, FUNCTION, FOR, WHILE, DO, VAR, CLASS, EXTENDS, EXTENSION, SUPER, THIS, NONE, RETURN, BREAK, CONTINUE, + IMPORT, FROM, AS, TRY, CATCH, FINALLY, THROW, // Compound assignment operators @@ -56,6 +57,7 @@ inline std::string enum_mapping[] = {"OPEN_PAREN", "CLOSE_PAREN", "OPEN_BRACE", "AND", "OR", "TRUE", "FALSE", "IF", "ELSE", "FUNCTION", "FOR", "WHILE", "DO", "VAR", "CLASS", "EXTENDS", "EXTENSION", "SUPER", "THIS", "NONE", "RETURN", "BREAK", "CONTINUE", + "IMPORT", "FROM", "AS", "TRY", "CATCH", "FINALLY", "THROW", // Compound assignment operators @@ -87,6 +89,9 @@ const std::map KEYWORDS { {"return", RETURN}, {"break", BREAK}, {"continue", CONTINUE}, + {"import", IMPORT}, + {"from", FROM}, + {"as", AS}, {"try", TRY}, {"catch", CATCH}, {"finally", FINALLY}, diff --git a/src/headers/parsing/Parser.h b/src/headers/parsing/Parser.h index 69d4fa1..3d52cd4 100644 --- a/src/headers/parsing/Parser.h +++ b/src/headers/parsing/Parser.h @@ -72,6 +72,8 @@ private: std::shared_ptr extensionDeclaration(); std::shared_ptr tryStatement(); std::shared_ptr throwStatement(); + std::shared_ptr importStatement(); + std::shared_ptr fromImportStatement(); std::shared_ptr varDeclaration(); diff --git a/src/headers/parsing/Statement.h b/src/headers/parsing/Statement.h index 80ae12c..af86848 100644 --- a/src/headers/parsing/Statement.h +++ b/src/headers/parsing/Statement.h @@ -40,6 +40,8 @@ struct StmtVisitor virtual void visitExtensionStmt(const std::shared_ptr& stmt, ExecutionContext* context = nullptr) = 0; virtual void visitTryStmt(const std::shared_ptr& stmt, ExecutionContext* context = nullptr) = 0; virtual void visitThrowStmt(const std::shared_ptr& stmt, ExecutionContext* context = nullptr) = 0; + virtual void visitImportStmt(const std::shared_ptr& stmt, ExecutionContext* context = nullptr) = 0; + virtual void visitFromImportStmt(const std::shared_ptr& stmt, ExecutionContext* context = nullptr) = 0; }; struct Stmt : public std::enable_shared_from_this @@ -271,4 +273,30 @@ struct ThrowStmt : Stmt { void accept(StmtVisitor* visitor, ExecutionContext* context = nullptr) override { visitor->visitThrowStmt(std::static_pointer_cast(shared_from_this()), context); } +}; + +// import module [as alias] +struct ImportStmt : Stmt { + Token importToken; // IMPORT + Token moduleName; // IDENTIFIER + bool hasAlias = false; + Token alias; // IDENTIFIER if hasAlias + ImportStmt(Token kw, Token mod, bool ha, Token al) + : importToken(kw), moduleName(mod), hasAlias(ha), alias(al) {} + void accept(StmtVisitor* visitor, ExecutionContext* context = nullptr) override { + visitor->visitImportStmt(std::static_pointer_cast(shared_from_this()), context); + } +}; + +// from module import name [as alias], name2 ... +struct FromImportStmt : Stmt { + Token fromToken; // FROM + Token moduleName; // IDENTIFIER + struct ImportItem { Token name; bool hasAlias; Token alias; }; + std::vector items; + FromImportStmt(Token kw, Token mod, std::vector it) + : fromToken(kw), moduleName(mod), items(std::move(it)) {} + void accept(StmtVisitor* visitor, ExecutionContext* context = nullptr) override { + visitor->visitFromImportStmt(std::static_pointer_cast(shared_from_this()), context); + } }; \ No newline at end of file diff --git a/src/headers/runtime/Environment.h b/src/headers/runtime/Environment.h index 2aacb33..4c89f23 100644 --- a/src/headers/runtime/Environment.h +++ b/src/headers/runtime/Environment.h @@ -3,6 +3,7 @@ #include #include #include +#include #include "Value.h" #include "Lexer.h" @@ -46,11 +47,14 @@ public: void pruneForClosureCapture(); std::shared_ptr getParent() const { return parent; } + // Export all variables (shallow copy) for module namespace + std::unordered_map getAll() const { return variables; } private: std::unordered_map variables; std::shared_ptr parent; ErrorReporter* errorReporter; + std::unordered_set constBindings; }; diff --git a/src/headers/runtime/Executor.h b/src/headers/runtime/Executor.h index 2f08213..f2bed72 100644 --- a/src/headers/runtime/Executor.h +++ b/src/headers/runtime/Executor.h @@ -42,6 +42,8 @@ public: void visitExtensionStmt(const std::shared_ptr& statement, ExecutionContext* context = nullptr) override; void visitTryStmt(const std::shared_ptr& statement, ExecutionContext* context = nullptr) override; void visitThrowStmt(const std::shared_ptr& statement, ExecutionContext* context = nullptr) override; + void visitImportStmt(const std::shared_ptr& statement, ExecutionContext* context = nullptr) override; + void visitFromImportStmt(const std::shared_ptr& statement, ExecutionContext* context = nullptr) override; private: void execute(const std::shared_ptr& statement, ExecutionContext* context); diff --git a/src/headers/runtime/Interpreter.h b/src/headers/runtime/Interpreter.h index a52e60b..b7264a5 100644 --- a/src/headers/runtime/Interpreter.h +++ b/src/headers/runtime/Interpreter.h @@ -7,7 +7,10 @@ #include #include "Value.h" +#include "TypeWrapper.h" #include "RuntimeDiagnostics.h" +#include "ModuleRegistry.h" +#include struct Expr; struct Stmt; @@ -81,6 +84,15 @@ private: RuntimeDiagnostics diagnostics; // Utility functions for runtime operations std::unique_ptr evaluator; std::unique_ptr executor; + // Module cache: module key -> module dict value + std::unordered_map moduleCache; + // Builtin module registry + ModuleRegistry builtinModules; + // Import policy flags + bool allowFileImports = true; + bool preferFileOverBuiltin = true; + bool allowBuiltinImports = true; + std::vector moduleSearchPaths; // e.g., BOBPATH // Pending throw propagation from expression evaluation bool hasPendingThrow = false; Value pendingThrow = NONE_VALUE; @@ -127,6 +139,24 @@ public: bool getClassTemplate(const std::string& className, std::unordered_map& out) const; std::unordered_map buildMergedTemplate(const std::string& className) const; void addStdLibFunctions(); + // Module APIs + Value importModule(const std::string& spec, int line, int column); // returns module dict + bool fromImport(const std::string& spec, const std::vector>& items, int line, int column); // name->alias + void setModulePolicy(bool allowFiles, bool preferFiles, const std::vector& searchPaths); + void setBuiltinModulePolicy(bool allowBuiltins) { allowBuiltinImports = allowBuiltins; builtinModules.setPolicy(allowBuiltins); } + void setBuiltinModuleAllowList(const std::vector& allowed) { builtinModules.setAllowList(allowed); } + void setBuiltinModuleDenyList(const std::vector& denied) { builtinModules.setDenyList(denied); } + void registerBuiltinModule(const std::string& name, std::function factory) { builtinModules.registerFactory(name, std::move(factory)); } + + // Simple module registration API + using ModuleBuilder = ModuleRegistry::ModuleBuilder; + + void registerModule(const std::string& name, std::function init) { + builtinModules.registerModule(name, std::move(init)); + } + // Global environment helpers + bool defineGlobalVar(const std::string& name, const Value& value); + bool tryGetGlobalVar(const std::string& name, Value& out) const; // Throw propagation helpers void setPendingThrow(const Value& v, int line = 0, int column = 0) { hasPendingThrow = true; pendingThrow = v; pendingThrowLine = line; pendingThrowColumn = column; } bool consumePendingThrow(Value& out, int* lineOut = nullptr, int* colOut = nullptr) { if (!hasPendingThrow) return false; out = pendingThrow; if (lineOut) *lineOut = pendingThrowLine; if (colOut) *colOut = pendingThrowColumn; hasPendingThrow = false; pendingThrow = NONE_VALUE; pendingThrowLine = 0; pendingThrowColumn = 0; return true; } diff --git a/src/headers/runtime/ModuleDef.h b/src/headers/runtime/ModuleDef.h new file mode 100644 index 0000000..cfea690 --- /dev/null +++ b/src/headers/runtime/ModuleDef.h @@ -0,0 +1,14 @@ +// ModuleDef.h +#pragma once + +#include +#include +#include +#include "Value.h" + +struct Module { + std::string name; + std::shared_ptr> exports; +}; + + diff --git a/src/headers/runtime/ModuleRegistry.h b/src/headers/runtime/ModuleRegistry.h new file mode 100644 index 0000000..0fc267c --- /dev/null +++ b/src/headers/runtime/ModuleRegistry.h @@ -0,0 +1,70 @@ +#pragma once + +#include +#include +#include +#include +#include +#include "TypeWrapper.h" // BuiltinFunction, Value + +class Interpreter; // fwd + +class ModuleRegistry { +public: + struct ModuleBuilder { + std::string moduleName; + Interpreter& interpreterRef; + std::unordered_map exports; + ModuleBuilder(const std::string& n, Interpreter& i) : moduleName(n), interpreterRef(i) {} + void fn(const std::string& name, std::function, int, int)> func) { + exports[name] = Value(std::make_shared(name, func)); + } + void val(const std::string& name, const Value& v) { exports[name] = v; } + }; + + using Factory = std::function; + + void registerFactory(const std::string& name, Factory factory) { + factories[name] = std::move(factory); + } + + void registerModule(const std::string& name, std::function init) { + registerFactory(name, [name, init](Interpreter& I) -> Value { + ModuleBuilder b(name, I); + init(b); + auto m = std::make_shared(name, b.exports); + return Value(m); + }); + } + + bool has(const std::string& name) const { + auto it = factories.find(name); + if (it == factories.end()) return false; + // Respect policy for presence checks to optionally cloak denied modules + if (!allowBuiltins) return false; + if (!allowList.empty() && allowList.find(name) == allowList.end()) return false; + if (denyList.find(name) != denyList.end()) return false; + return true; + } + + Value create(const std::string& name, Interpreter& I) const { + auto it = factories.find(name); + if (it == factories.end()) return NONE_VALUE; + if (!allowBuiltins) return NONE_VALUE; + if (!allowList.empty() && allowList.find(name) == allowList.end()) return NONE_VALUE; + if (denyList.find(name) != denyList.end()) return NONE_VALUE; + return it->second(I); + } + + void setPolicy(bool allow) { allowBuiltins = allow; } + void setAllowList(const std::vector& allowed) { allowList = std::unordered_set(allowed.begin(), allowed.end()); } + void setDenyList(const std::vector& denied) { denyList = std::unordered_set(denied.begin(), denied.end()); } + +private: + std::unordered_map factories; + std::unordered_set allowList; + std::unordered_set denyList; + bool allowBuiltins = true; +}; + + diff --git a/src/headers/runtime/Value.h b/src/headers/runtime/Value.h index bb64c2b..56f916b 100644 --- a/src/headers/runtime/Value.h +++ b/src/headers/runtime/Value.h @@ -13,6 +13,7 @@ struct Environment; struct Function; struct BuiltinFunction; struct Thunk; +struct Module; // Type tags for the Value union enum ValueType { @@ -24,9 +25,12 @@ enum ValueType { VAL_BUILTIN_FUNCTION, VAL_THUNK, VAL_ARRAY, - VAL_DICT + VAL_DICT, + VAL_MODULE }; +// (moved below Value) + // Tagged value system (like Lua) - no heap allocation for simple values struct Value { union { @@ -37,6 +41,7 @@ struct Value { std::string string_value; // Store strings outside the union for safety std::shared_ptr > array_value; // Store arrays as shared_ptr for mutability std::shared_ptr > dict_value; // Store dictionaries as shared_ptr for mutability + std::shared_ptr module_value; // Module object // Store functions as shared_ptr for proper reference counting std::shared_ptr function; @@ -58,6 +63,7 @@ struct Value { Value(std::vector&& arr) : type(ValueType::VAL_ARRAY), array_value(std::make_shared >(std::move(arr))) {} Value(const std::unordered_map& dict) : type(ValueType::VAL_DICT), dict_value(std::make_shared >(dict)) {} Value(std::unordered_map&& dict) : type(ValueType::VAL_DICT), dict_value(std::make_shared >(std::move(dict))) {} + Value(std::shared_ptr m) : type(ValueType::VAL_MODULE), module_value(std::move(m)) {} // Destructor to clean up functions and thunks ~Value() { @@ -70,7 +76,7 @@ struct Value { // Move constructor Value(Value&& other) noexcept : type(other.type), string_value(std::move(other.string_value)), array_value(std::move(other.array_value)), dict_value(std::move(other.dict_value)), - function(std::move(other.function)), builtin_function(std::move(other.builtin_function)), thunk(std::move(other.thunk)) { + function(std::move(other.function)), builtin_function(std::move(other.builtin_function)), thunk(std::move(other.thunk)), module_value(std::move(other.module_value)) { if (type != ValueType::VAL_STRING && type != ValueType::VAL_ARRAY && type != ValueType::VAL_DICT && type != ValueType::VAL_FUNCTION && type != ValueType::VAL_BUILTIN_FUNCTION && type != ValueType::VAL_THUNK) { number = other.number; // Copy the union @@ -94,6 +100,8 @@ struct Value { builtin_function = std::move(other.builtin_function); } else if (type == ValueType::VAL_THUNK) { thunk = std::move(other.thunk); + } else if (type == ValueType::VAL_MODULE) { + module_value = std::move(other.module_value); } else { number = other.number; } @@ -117,6 +125,8 @@ struct Value { builtin_function = other.builtin_function; // shared_ptr automatically handles sharing } else if (type == ValueType::VAL_THUNK) { thunk = other.thunk; // shared_ptr automatically handles sharing + } else if (type == ValueType::VAL_MODULE) { + module_value = other.module_value; // shared module } else { number = other.number; } @@ -146,6 +156,8 @@ struct Value { builtin_function = other.builtin_function; // shared_ptr automatically handles sharing } else if (type == ValueType::VAL_THUNK) { thunk = other.thunk; // shared_ptr automatically handles sharing + } else if (type == ValueType::VAL_MODULE) { + module_value = other.module_value; } else { number = other.number; } @@ -161,6 +173,7 @@ struct Value { inline bool isBuiltinFunction() const { return type == ValueType::VAL_BUILTIN_FUNCTION; } inline bool isArray() const { return type == ValueType::VAL_ARRAY; } inline bool isDict() const { return type == ValueType::VAL_DICT; } + inline bool isModule() const { return type == ValueType::VAL_MODULE; } inline bool isThunk() const { return type == ValueType::VAL_THUNK; } inline bool isNone() const { return type == ValueType::VAL_NONE; } @@ -176,6 +189,7 @@ struct Value { case ValueType::VAL_THUNK: return "thunk"; case ValueType::VAL_ARRAY: return "array"; case ValueType::VAL_DICT: return "dict"; + case ValueType::VAL_MODULE: return "module"; default: return "unknown"; } } @@ -198,6 +212,7 @@ struct Value { inline std::unordered_map& asDict() { return *dict_value; } + inline Module* asModule() const { return isModule() ? module_value.get() : nullptr; } inline Function* asFunction() const { return isFunction() ? function.get() : nullptr; } inline BuiltinFunction* asBuiltinFunction() const { return isBuiltinFunction() ? builtin_function.get() : nullptr; } inline Thunk* asThunk() const { return isThunk() ? thunk.get() : nullptr; } @@ -214,6 +229,7 @@ struct Value { case ValueType::VAL_THUNK: return thunk != nullptr; case ValueType::VAL_ARRAY: return !array_value->empty(); case ValueType::VAL_DICT: return !dict_value->empty(); + case ValueType::VAL_MODULE: return module_value != nullptr; default: return false; } } @@ -296,6 +312,12 @@ struct Value { result += "}"; return result; } + case ValueType::VAL_MODULE: { + // Avoid accessing Module fields when it's still an incomplete type in some TUs. + // Delegate formatting to a small helper defined out-of-line in Value.cpp. + extern std::string formatModuleForToString(const std::shared_ptr&); + return formatModuleForToString(module_value); + } default: return "unknown"; } } @@ -420,6 +442,15 @@ struct Value { } }; +// Define Module after Value so it can hold Value in exports without incomplete type issues +struct Module { + std::string name; + std::shared_ptr> exports; + Module() = default; + Module(const std::string& n, const std::unordered_map& dict) + : name(n), exports(std::make_shared>(dict)) {} +}; + // Global constants for common values extern const Value NONE_VALUE; extern const Value TRUE_VALUE; diff --git a/src/sources/builtinModules/register.cpp b/src/sources/builtinModules/register.cpp new file mode 100644 index 0000000..9bb6220 --- /dev/null +++ b/src/sources/builtinModules/register.cpp @@ -0,0 +1,8 @@ +#include "register.h" +#include "sys.h" + +void registerAllBuiltinModules(Interpreter& interpreter) { + registerSysModule(interpreter); +} + + diff --git a/src/sources/builtinModules/sys.cpp b/src/sources/builtinModules/sys.cpp new file mode 100644 index 0000000..620badc --- /dev/null +++ b/src/sources/builtinModules/sys.cpp @@ -0,0 +1,146 @@ +#include "sys.h" +#include "Interpreter.h" +#include "Environment.h" +#include "Lexer.h" // for Token and IDENTIFIER +#include +#include +#include +#include +#if defined(__APPLE__) +#define DYLD_BOOL DYLD_BOOL_IGNORED +#include +#undef DYLD_BOOL +#endif + +void registerSysModule(Interpreter& interpreter) { + interpreter.registerModule("sys", [](Interpreter::ModuleBuilder& m) { + Interpreter& I = m.interpreterRef; + m.fn("memoryUsage", [&I](std::vector, int l, int c) -> Value { + try { + Value fn = I.getEnvironment()->get(Token{IDENTIFIER, "memoryUsage", l, c}); + if (fn.isBuiltinFunction()) return fn.asBuiltinFunction()->func({}, l, c); + } catch (...) {} + return NONE_VALUE; + }); + m.fn("exit", [](std::vector a, int, int) -> Value { + int code = 0; if (!a.empty() && a[0].isNumber()) code = static_cast(a[0].asNumber()); + std::exit(code); + return NONE_VALUE; + }); + m.fn("cwd", [](std::vector, int, int) -> Value { + char buf[PATH_MAX]; + if (getcwd(buf, sizeof(buf))) { return Value(std::string(buf)); } + return NONE_VALUE; + }); + m.fn("platform", [](std::vector, int, int) -> Value { +#if defined(_WIN32) + return Value(std::string("windows")); +#elif defined(__APPLE__) + return Value(std::string("macos")); +#elif defined(__linux__) + return Value(std::string("linux")); +#else + return Value(std::string("unknown")); +#endif + }); + m.fn("getenv", [](std::vector a, int, int) -> Value { + if (a.size() != 1 || !a[0].isString()) return NONE_VALUE; + const char* v = std::getenv(a[0].asString().c_str()); + if (!v) return NONE_VALUE; + return Value(std::string(v)); + }); + m.fn("pid", [](std::vector, int, int) -> Value { + return Value(static_cast(getpid())); + }); + m.fn("chdir", [](std::vector a, int, int) -> Value { + if (a.size() != 1 || !a[0].isString()) return Value(false); + const std::string& p = a[0].asString(); + int rc = chdir(p.c_str()); + return Value(rc == 0); + }); + m.fn("homeDir", [](std::vector, int, int) -> Value { + const char* h = std::getenv("HOME"); + if (h) return Value(std::string(h)); + const char* up = std::getenv("USERPROFILE"); + if (up) return Value(std::string(up)); + return NONE_VALUE; + }); + m.fn("tempDir", [](std::vector, int, int) -> Value { + const char* t = std::getenv("TMPDIR"); + if (!t) t = std::getenv("TMP"); + if (!t) t = std::getenv("TEMP"); + if (t) return Value(std::string(t)); + return Value(std::string("/tmp")); + }); + m.fn("pathSep", [](std::vector, int, int) -> Value { +#if defined(_WIN32) + return Value(std::string(";")); +#else + return Value(std::string(":")); +#endif + }); + m.fn("dirSep", [](std::vector, int, int) -> Value { +#if defined(_WIN32) + return Value(std::string("\\")); +#else + return Value(std::string("/")); +#endif + }); + m.fn("execPath", [](std::vector, int, int) -> Value { +#if defined(__APPLE__) + uint32_t sz = 0; + _NSGetExecutablePath(nullptr, &sz); + std::string buf(sz, '\0'); + if (_NSGetExecutablePath(buf.data(), &sz) == 0) { + buf.resize(std::strlen(buf.c_str())); + return Value(buf); + } + return NONE_VALUE; +#elif defined(__linux__) + char path[PATH_MAX]; + ssize_t len = readlink("/proc/self/exe", path, sizeof(path) - 1); + if (len > 0) { path[len] = '\0'; return Value(std::string(path)); } + return NONE_VALUE; +#else + return NONE_VALUE; +#endif + }); + m.fn("env", [](std::vector, int, int) -> Value { + std::unordered_map out; +#if defined(__APPLE__) || defined(__linux__) + extern char **environ; + if (environ) { + for (char **e = environ; *e != nullptr; ++e) { + const char* kv = *e; + const char* eq = std::strchr(kv, '='); + if (!eq) continue; + std::string key(kv, eq - kv); + std::string val(eq + 1); + out[key] = Value(val); + } + } +#endif + return Value(out); + }); + m.fn("setenv", [](std::vector a, int, int) -> Value { + if (a.size() != 2 || !a[0].isString() || !a[1].isString()) return Value(false); +#if defined(__APPLE__) || defined(__linux__) + int rc = ::setenv(a[0].asString().c_str(), a[1].asString().c_str(), 1); + return Value(rc == 0); +#else + return Value(false); +#endif + }); + m.fn("unsetenv", [](std::vector a, int, int) -> Value { + if (a.size() != 1 || !a[0].isString()) return Value(false); +#if defined(__APPLE__) || defined(__linux__) + int rc = ::unsetenv(a[0].asString().c_str()); + return Value(rc == 0); +#else + return Value(false); +#endif + }); + }); +} + + diff --git a/src/sources/cli/bob.cpp b/src/sources/cli/bob.cpp index 4b9a4cf..5426e19 100644 --- a/src/sources/cli/bob.cpp +++ b/src/sources/cli/bob.cpp @@ -3,35 +3,23 @@ #include "bob.h" #include "Parser.h" +void Bob::ensureInterpreter(bool interactive) { + if (!interpreter) interpreter = msptr(Interpreter)(interactive); + applyPendingConfigs(); +} + void Bob::runFile(const std::string& path) { - this->interpreter = msptr(Interpreter)(false); - std::ifstream file = std::ifstream(path); - - std::string source; - - if(file.is_open()){ - source = std::string(std::istreambuf_iterator(file), std::istreambuf_iterator()); - } - else - { - std::cout << "File not found\n"; - return; - } - - // Load source code into error reporter for context - errorReporter.loadSource(source, path); - - interpreter->setErrorReporter(&errorReporter); + ensureInterpreter(false); interpreter->addStdLibFunctions(); - - this->run(source); + if (!evalFile(path)) { + std::cout << "Execution failed\n"; + } } void Bob::runPrompt() { - this->interpreter = msptr(Interpreter)(true); - + ensureInterpreter(true); std::cout << "Bob v" << VERSION << ", 2025\n"; while(true) { @@ -46,52 +34,40 @@ void Bob::runPrompt() // Reset error state before each REPL command errorReporter.resetErrorState(); - - // Load source code into error reporter for context - errorReporter.loadSource(line, "REPL"); - - // Connect error reporter to interpreter - interpreter->setErrorReporter(&errorReporter); interpreter->addStdLibFunctions(); - - this->run(line); + (void)evalString(line, "REPL"); } } -void Bob::run(std::string source) -{ +bool Bob::evalFile(const std::string& path) { + std::ifstream file(path); + if (!file.is_open()) return false; + std::string src((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + errorReporter.loadSource(src, path); + interpreter->setErrorReporter(&errorReporter); try { - // Connect error reporter to lexer lexer.setErrorReporter(&errorReporter); - - std::vector tokens = lexer.Tokenize(std::move(source)); + auto tokens = lexer.Tokenize(src); Parser p(tokens); - - // Connect error reporter to parser p.setErrorReporter(&errorReporter); - - std::vector statements = p.parse(); + auto statements = p.parse(); interpreter->interpret(statements); - } - catch(std::exception &e) - { - // Only suppress errors that have already been reported inline/top-level - if (errorReporter.hasReportedError() || (interpreter && (interpreter->hasReportedError() || interpreter->hasInlineErrorReported()))) { - if (interpreter) interpreter->clearInlineErrorReported(); - return; - } - - // For errors that weren't reported (like parser errors, undefined variables, etc.) - // print them normally - std::cout << "Error: " << e.what() << '\n'; - return; - } - catch(const std::exception& e) - { - // Unknown error - report it since it wasn't handled by the interpreter - errorReporter.reportError(0, 0, "Unknown Error", "An unknown error occurred: " + std::string(e.what())); - return; - } + return true; + } catch (...) { return false; } +} + +bool Bob::evalString(const std::string& code, const std::string& filename) { + errorReporter.loadSource(code, filename); + interpreter->setErrorReporter(&errorReporter); + try { + lexer.setErrorReporter(&errorReporter); + auto tokens = lexer.Tokenize(code); + Parser p(tokens); + p.setErrorReporter(&errorReporter); + auto statements = p.parse(); + interpreter->interpret(statements); + return true; + } catch (...) { return false; } } diff --git a/src/sources/cli/main.cpp b/src/sources/cli/main.cpp index 1f87377..fb2654c 100644 --- a/src/sources/cli/main.cpp +++ b/src/sources/cli/main.cpp @@ -2,13 +2,24 @@ // #include "bob.h" +#include "Interpreter.h" int main(int argc, char* argv[]){ Bob bobLang; + // Example: host can register a custom module via Bob bridge (applied on first use) + bobLang.registerModule("demo", [](ModuleRegistry::ModuleBuilder& m) { + m.fn("hello", [](std::vector a, int, int) -> Value { + std::string who = (a.size() >= 1 && a[0].isString()) ? a[0].asString() : std::string("world"); + return Value(std::string("hello ") + who); + }); + m.val("meaning", Value(42.0)); + }); + //bobLang.setBuiltinModuleDenyList({"sys"}); if(argc > 1) { bobLang.runFile(argv[1]); } else { + // For REPL, use interactive mode bobLang.runPrompt(); } diff --git a/src/sources/parsing/Parser.cpp b/src/sources/parsing/Parser.cpp index 2c04ba4..01a3c95 100644 --- a/src/sources/parsing/Parser.cpp +++ b/src/sources/parsing/Parser.cpp @@ -586,6 +586,11 @@ std::shared_ptr Parser::functionExpression() { sptr(Stmt) Parser::statement() { if(match({RETURN})) return returnStatement(); + if(match({IMPORT})) return importStatement(); + // Fallback if lexer didn't classify keyword: detect by lexeme + if (check(IDENTIFIER) && peek().lexeme == "import") { advance(); return importStatement(); } + if(match({FROM})) return fromImportStatement(); + if (check(IDENTIFIER) && peek().lexeme == "from") { advance(); return fromImportStatement(); } if(match({TRY})) return tryStatement(); if(match({THROW})) return throwStatement(); if(match({IF})) return ifStatement(); @@ -622,6 +627,38 @@ sptr(Stmt) Parser::statement() return expressionStatement(); } +std::shared_ptr Parser::importStatement() { + Token importTok = previous(); + // import Name [as Alias] | import "path" + bool isString = check(STRING); + Token mod = isString ? advance() : consume(IDENTIFIER, "Expected module name or path string after 'import'."); + // Keep IDENTIFIER for name-based imports; resolver will try file and then builtin + bool hasAlias = false; Token alias; + if (match({AS})) { + hasAlias = true; + alias = consume(IDENTIFIER, "Expected alias identifier after 'as'."); + } + consume(SEMICOLON, "Expected ';' after import statement."); + return msptr(ImportStmt)(importTok, mod, hasAlias, alias); +} + +std::shared_ptr Parser::fromImportStatement() { + Token fromTok = previous(); + bool isString = check(STRING); + Token mod = isString ? advance() : consume(IDENTIFIER, "Expected module name or path string after 'from'."); + // Keep IDENTIFIER for name-based from-imports + consume(IMPORT, "Expected 'import' after module name."); + std::vector items; + do { + Token name = consume(IDENTIFIER, "Expected name to import."); + bool hasAlias = false; Token alias; + if (match({AS})) { hasAlias = true; alias = consume(IDENTIFIER, "Expected alias identifier after 'as'."); } + items.push_back({name, hasAlias, alias}); + } while (match({COMMA})); + consume(SEMICOLON, "Expected ';' after from-import statement."); + return msptr(FromImportStmt)(fromTok, mod, items); +} + sptr(Stmt) Parser::assignmentStatement() { Token name = consume(IDENTIFIER, "Expected variable name for assignment."); diff --git a/src/sources/runtime/Environment.cpp b/src/sources/runtime/Environment.cpp index 9063d81..c2587b3 100644 --- a/src/sources/runtime/Environment.cpp +++ b/src/sources/runtime/Environment.cpp @@ -2,6 +2,15 @@ #include "ErrorReporter.h" void Environment::assign(const Token& name, const Value& value) { + // Disallow reassignment of module bindings (immutability of module variable) + auto itv = variables.find(name.lexeme); + if (itv != variables.end() && itv->second.isModule()) { + if (errorReporter) { + errorReporter->reportError(name.line, name.column, "Import Error", + "Cannot reassign module binding '" + name.lexeme + "'", ""); + } + throw std::runtime_error("Cannot reassign module binding '" + name.lexeme + "'"); + } auto it = variables.find(name.lexeme); if (it != variables.end()) { it->second = value; diff --git a/src/sources/runtime/Evaluator.cpp b/src/sources/runtime/Evaluator.cpp index 820c585..738d8ce 100644 --- a/src/sources/runtime/Evaluator.cpp +++ b/src/sources/runtime/Evaluator.cpp @@ -316,7 +316,15 @@ Value Evaluator::visitPropertyExpr(const std::shared_ptr& expr) { Value object = expr->object->accept(this); std::string propertyName = expr->name.lexeme; - if (object.isDict()) { + if (object.isModule()) { + // Forward to module exports + auto* mod = object.asModule(); + if (mod && mod->exports) { + auto it = mod->exports->find(propertyName); + if (it != mod->exports->end()) return it->second; + } + return NONE_VALUE; + } else if (object.isDict()) { Value v = getDictProperty(object, propertyName); if (!v.isNone()) { // If this is an inherited inline method, prefer a current-class extension override @@ -415,7 +423,7 @@ Value Evaluator::visitPropertyExpr(const std::shared_ptr& expr) { else if (object.isNumber()) target = "number"; else if (object.isArray()) target = "array"; // handled above, but keep for completeness else if (object.isDict()) target = "dict"; // handled above - else target = "any"; + else target = object.isModule() ? "any" : "any"; // Provide method-style builtins for string/number if (object.isString() && propertyName == "len") { @@ -431,9 +439,12 @@ Value Evaluator::visitPropertyExpr(const std::shared_ptr& expr) { return Value(bf); } - if (auto fn = interpreter->lookupExtension(target, propertyName)) { - return Value(fn); + if (object.isModule()) { + // Modules are immutable and have no dynamic methods + return NONE_VALUE; } + auto fn = interpreter->lookupExtension(target, propertyName); + if (!object.isModule() && fn) { return Value(fn); } if (auto anyFn = interpreter->lookupExtension("any", propertyName)) { return Value(anyFn); } @@ -520,7 +531,15 @@ Value Evaluator::visitPropertyAssignExpr(const std::shared_ptrvalue->accept(this); std::string propertyName = expr->name.lexeme; - if (object.isDict()) { + if (object.isModule()) { + // Modules are immutable: disallow setting properties + if (!interpreter->isInTry()) { + interpreter->reportError(expr->name.line, expr->name.column, "Import Error", + "Cannot assign property '" + propertyName + "' on module (immutable)", ""); + interpreter->markInlineErrorReported(); + } + throw std::runtime_error("Cannot assign property on module (immutable)"); + } else if (object.isDict()) { // Modify the dictionary in place std::unordered_map& dict = object.asDict(); dict[propertyName] = value; diff --git a/src/sources/runtime/Executor.cpp b/src/sources/runtime/Executor.cpp index e113726..c0d72ef 100644 --- a/src/sources/runtime/Executor.cpp +++ b/src/sources/runtime/Executor.cpp @@ -312,6 +312,40 @@ void Executor::visitThrowStmt(const std::shared_ptr& statement, Execu } } +void Executor::visitImportStmt(const std::shared_ptr& statement, ExecutionContext* context) { + // Determine spec (string literal or identifier) + std::string spec = statement->moduleName.lexeme; // already STRING with .bob from parser if name-based + Value mod = interpreter->importModule(spec, statement->importToken.line, statement->importToken.column); + std::string bindName; + if (statement->hasAlias) { + bindName = statement->alias.lexeme; + } else { + // Derive default binding name from module path: basename without extension + std::string path = statement->moduleName.lexeme; + // Strip directories + size_t pos = path.find_last_of("/\\"); + std::string base = (pos == std::string::npos) ? path : path.substr(pos + 1); + // Strip .bob + if (base.size() > 4 && base.substr(base.size() - 4) == ".bob") { + base = base.substr(0, base.size() - 4); + } + bindName = base; + } + interpreter->getEnvironment()->define(bindName, mod); +} + +void Executor::visitFromImportStmt(const std::shared_ptr& statement, ExecutionContext* context) { + std::string spec = statement->moduleName.lexeme; // already STRING with .bob from parser if name-based + // Build item list name->alias + std::vector> items; + for (const auto& it : statement->items) { + items.emplace_back(it.name.lexeme, it.hasAlias ? it.alias.lexeme : it.name.lexeme); + } + if (!interpreter->fromImport(spec, items, statement->fromToken.line, statement->fromToken.column)) { + throw std::runtime_error("from-import failed"); + } +} + void Executor::visitAssignStmt(const std::shared_ptr& statement, ExecutionContext* context) { Value value = statement->value->accept(evaluator); diff --git a/src/sources/runtime/Interpreter.cpp b/src/sources/runtime/Interpreter.cpp index 397caf1..fd2263d 100644 --- a/src/sources/runtime/Interpreter.cpp +++ b/src/sources/runtime/Interpreter.cpp @@ -1,10 +1,15 @@ #include "Interpreter.h" +#include "register.h" #include "Evaluator.h" #include "Executor.h" #include "BobStdLib.h" #include "ErrorReporter.h" #include "Environment.h" #include "Expression.h" +#include "Parser.h" +#include +#include +#include #include Interpreter::Interpreter(bool isInteractive) @@ -12,6 +17,11 @@ Interpreter::Interpreter(bool isInteractive) evaluator = std::make_unique(this); executor = std::make_unique(this, evaluator.get()); environment = std::make_shared(); + // Default module search paths: current dir and tests + moduleSearchPaths = { ".", "tests" }; + + // Register all builtin modules via aggregator + registerAllBuiltinModules(*this); } Interpreter::~Interpreter() = default; @@ -35,6 +45,21 @@ Value Interpreter::evaluate(const std::shared_ptr& expr) { } return runTrampoline(result); } +bool Interpreter::defineGlobalVar(const std::string& name, const Value& value) { + if (!environment) return false; + try { + environment->define(name, value); + return true; + } catch (...) { return false; } +} + +bool Interpreter::tryGetGlobalVar(const std::string& name, Value& out) const { + if (!environment) return false; + try { + out = environment->get(Token{IDENTIFIER, name, 0, 0}); + return true; + } catch (...) { return false; } +} bool Interpreter::hasReportedError() const { return inlineErrorReported; @@ -63,6 +88,168 @@ std::string Interpreter::stringify(Value object) { void Interpreter::addStdLibFunctions() { BobStdLib::addToEnvironment(environment, *this, errorReporter); } +void Interpreter::setModulePolicy(bool allowFiles, bool preferFiles, const std::vector& searchPaths) { + allowFileImports = allowFiles; + preferFileOverBuiltin = preferFiles; + moduleSearchPaths = searchPaths; +} + +static std::string joinPath(const std::string& baseDir, const std::string& rel) { + namespace fs = std::filesystem; + fs::path p = fs::path(baseDir) / fs::path(rel); + return fs::path(p).lexically_normal().string(); +} + +static std::string locateModuleFile(const std::string& baseDir, const std::vector& searchPaths, const std::string& nameDotBob) { + namespace fs = std::filesystem; + // Only search relative to the importing file's directory + // 1) baseDir/name.bob + if (!baseDir.empty()) { + std::string p = joinPath(baseDir, nameDotBob); + if (fs::exists(fs::path(p))) return p; + } + // 2) baseDir/searchPath/name.bob (search paths are relative to baseDir) + for (const auto& sp : searchPaths) { + if (!baseDir.empty()) { + std::string pb = joinPath(baseDir, joinPath(sp, nameDotBob)); + if (fs::exists(fs::path(pb))) return pb; + } + } + return ""; +} + +Value Interpreter::importModule(const std::string& spec, int line, int column) { + // Determine if spec is a path string (heuristic: contains '/' or ends with .bob) + bool looksPath = spec.find('/') != std::string::npos || (spec.size() >= 4 && spec.rfind(".bob") == spec.size() - 4) || spec.find("..") != std::string::npos; + // Cache key resolution + std::string key = spec; + std::string baseDir = ""; + if (errorReporter) { + // Try to use current file from reporter; else cwd + if (!errorReporter->getCurrentFileName().empty()) { + std::filesystem::path p(errorReporter->getCurrentFileName()); + baseDir = p.has_parent_path() ? p.parent_path().string() : baseDir; + } + if (baseDir.empty()) { char buf[4096]; if (getcwd(buf, sizeof(buf))) baseDir = std::string(buf); } + } + if (looksPath) { + if (!allowFileImports) { + reportError(line, column, "Import Error", "File imports are disabled by policy", spec); + throw std::runtime_error("File imports disabled"); + } + // Resolve STRING path specs: + // - Absolute: use as-is + // - Starts with ./ or ../: resolve relative to the importing file directory (baseDir) + // - Otherwise: resolve relative to current working directory + if (!spec.empty() && spec[0] == '/') { + key = spec; + } else if (spec.rfind("./", 0) == 0 || spec.rfind("../", 0) == 0) { + key = joinPath(baseDir, spec); + } else { + // Resolve all non-absolute paths relative to the importing file directory only + key = joinPath(baseDir, spec); + } + } else { + // Name import: try file in baseDir or search paths; else builtin + if (preferFileOverBuiltin && allowFileImports) { + std::string found = locateModuleFile(baseDir, moduleSearchPaths, spec + ".bob"); + if (!found.empty()) { key = found; looksPath = true; } + } + if (!looksPath && allowBuiltinImports && builtinModules.has(spec)) { + key = std::string("builtin:") + spec; + } + } + // Return from cache + auto it = moduleCache.find(key); + if (it != moduleCache.end()) return it->second; + + // If still not a path, it must be builtin or missing + if (!looksPath) { + if (!builtinModules.has(spec)) { + reportError(line, column, "Import Error", "Module not found: " + spec + ".bob", spec); + throw std::runtime_error("Module not found"); + } + // Builtin: return from cache or construct and cache + auto itc = moduleCache.find(key); + if (itc != moduleCache.end()) return itc->second; + Value v = builtinModules.create(spec, *this); + if (v.isNone()) { // cloaked by policy + reportError(line, column, "Import Error", "Module not found: " + spec + ".bob", spec); + throw std::runtime_error("Module not found"); + } + moduleCache[key] = v; + return v; + } + + // File module: read and execute in isolated env + std::ifstream file(key); + if (!file.is_open()) { + reportError(line, column, "Import Error", "Could not open module file: " + key, spec); + throw std::runtime_error("Module file open failed"); + } + std::string code((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + file.close(); + + // Prepare reporter with module source + if (errorReporter) errorReporter->pushSource(code, key); + + // New lexer and parser + Lexer lx; if (errorReporter) lx.setErrorReporter(errorReporter); + std::vector toks = lx.Tokenize(code); + Parser p(toks); if (errorReporter) p.setErrorReporter(errorReporter); + std::vector> stmts = p.parse(); + + // Isolated environment + auto saved = getEnvironment(); + auto modEnv = std::make_shared(saved); + modEnv->setErrorReporter(errorReporter); + setEnvironment(modEnv); + + // Execute + executor->interpret(stmts); + + // Build module object from env + std::unordered_map exported = modEnv->getAll(); + // Derive module name from key basename + std::string modName = key; + size_t pos = modName.find_last_of("/\\"); if (pos != std::string::npos) modName = modName.substr(pos+1); + if (modName.size() > 4 && modName.substr(modName.size()-4) == ".bob") modName = modName.substr(0, modName.size()-4); + auto m = std::make_shared(modName, exported); + Value moduleVal(m); + // Cache + moduleCache[key] = moduleVal; + + // Restore env and reporter + setEnvironment(saved); + if (errorReporter) errorReporter->popSource(); + + return moduleVal; +} + +bool Interpreter::fromImport(const std::string& spec, const std::vector>& items, int line, int column) { + Value mod = importModule(spec, line, column); + if (!(mod.isModule() || mod.isDict())) { + reportError(line, column, "Import Error", "Module did not evaluate to a module", spec); + return false; + } + std::unordered_map const* src = nullptr; + std::unordered_map temp; + if (mod.isModule()) { + // Module exports + src = mod.asModule()->exports.get(); + } else { + src = &mod.asDict(); + } + for (const auto& [name, alias] : items) { + auto it = src->find(name); + if (it == src->end()) { + reportError(line, column, "Import Error", "Name not found in module: " + name, spec); + return false; + } + environment->define(alias, it->second); + } + return true; +} void Interpreter::addBuiltinFunction(std::shared_ptr func) { builtinFunctions.push_back(func); diff --git a/src/sources/runtime/Value.cpp b/src/sources/runtime/Value.cpp index 324b37e..777f1aa 100644 --- a/src/sources/runtime/Value.cpp +++ b/src/sources/runtime/Value.cpp @@ -4,4 +4,12 @@ const Value NONE_VALUE = Value(); const Value TRUE_VALUE = Value(true); const Value FALSE_VALUE = Value(false); + +// Helper to format module string safely with complete type available in this TU +std::string formatModuleForToString(const std::shared_ptr& mod) { + if (mod && !mod->name.empty()) { + return std::string("name + "'>"; + } + return std::string(""); +} \ No newline at end of file diff --git a/src/sources/stdlib/BobStdLib.cpp b/src/sources/stdlib/BobStdLib.cpp index 5d5bdbc..2a2cf43 100644 --- a/src/sources/stdlib/BobStdLib.cpp +++ b/src/sources/stdlib/BobStdLib.cpp @@ -200,6 +200,8 @@ void BobStdLib::addToEnvironment(std::shared_ptr env, Interpreter& typeName = "array"; } else if (args[0].isDict()) { typeName = "dict"; + } else if (args[0].isModule()) { + typeName = "module"; } else { typeName = "unknown"; } diff --git a/test_bob_language.bob b/test_bob_language.bob index 631372e..3da81d8 100644 --- a/test_bob_language.bob +++ b/test_bob_language.bob @@ -3316,5 +3316,12 @@ eval(readFile(path15d)); var path15e = fileExists("tests/test_try_catch_loop_interactions.bob") ? "tests/test_try_catch_loop_interactions.bob" : "../tests/test_try_catch_loop_interactions.bob"; eval(readFile(path15e)); +// Modules: basic imports suite +var pathMods = fileExists("tests/test_imports_basic.bob") ? "tests/test_imports_basic.bob" : "../tests/test_imports_basic.bob"; +eval(readFile(pathMods)); + +var pathModsB = fileExists("tests/test_imports_builtin.bob") ? "tests/test_imports_builtin.bob" : "../tests/test_imports_builtin.bob"; +eval(readFile(pathModsB)); + print("\nAll tests passed."); print("Test suite complete."); \ No newline at end of file diff --git a/tests.bob b/tests.bob index b6aee0c..de36e24 100644 --- a/tests.bob +++ b/tests.bob @@ -1,90 +1,106 @@ -// var a = []; +// // var a = []; -// for(var i = 0; i < 1000000; i++){ -// print(i); +// // for(var i = 0; i < 1000000; i++){ +// // print(i); -// // Create nested structures with functions at different levels -// if (i % 4 == 0) { -// // Nested array with function -// push(a, [ -// func(){print("Array nested func i=" + i); return i;}, -// [func(){return "Deep array func " + i;}], -// i -// ]); -// } else if (i % 4 == 1) { -// // Nested dict with function -// push(a, { -// "func": func(){print("Dict func i=" + i); return i;}, -// "nested": {"deepFunc": func(){return "Deep dict func " + i;}}, -// "value": i -// }); -// } else if (i % 4 == 2) { -// // Mixed nested array/dict with functions -// push(a, [ -// {"arrayInDict": func(){return "Mixed " + i;}}, -// [func(){return "Array in array " + i;}, {"more": func(){return i;}}], -// func(){print("Top level in mixed i=" + i); return i;} -// ]); -// } else { -// // Simple function (original test case) -// push(a, func(){print("Simple func i=" + i); return toString(i);}); +// // // Create nested structures with functions at different levels +// // if (i % 4 == 0) { +// // // Nested array with function +// // push(a, [ +// // func(){print("Array nested func i=" + i); return i;}, +// // [func(){return "Deep array func " + i;}], +// // i +// // ]); +// // } else if (i % 4 == 1) { +// // // Nested dict with function +// // push(a, { +// // "func": func(){print("Dict func i=" + i); return i;}, +// // "nested": {"deepFunc": func(){return "Deep dict func " + i;}}, +// // "value": i +// // }); +// // } else if (i % 4 == 2) { +// // // Mixed nested array/dict with functions +// // push(a, [ +// // {"arrayInDict": func(){return "Mixed " + i;}}, +// // [func(){return "Array in array " + i;}, {"more": func(){return i;}}], +// // func(){print("Top level in mixed i=" + i); return i;} +// // ]); +// // } else { +// // // Simple function (original test case) +// // push(a, func(){print("Simple func i=" + i); return toString(i);}); +// // } +// // } + +// // print("Before: " + len(a)); +// // print("Memory usage: " + memoryUsage() + " MB"); + +// // // Test different types of nested function calls +// // a[3691](); // Simple function +// // if (len(a[3692]) > 0) { +// // a[3692][0](); // Nested array function +// // } +// // if (a[3693]["func"]) { +// // a[3693]["func"](); // Nested dict function +// // } +// // //print(a); +// // //writeFile("array_contents.txt", toString(a)); +// // print("Array contents written to array_contents.txt"); +// // print("Memory before cleanup: " + memoryUsage() + " MB"); +// // input("Press any key to free memory"); + +// // a = none; +// // print("Memory after cleanup: " + memoryUsage() + " MB"); +// // input("waiting..."); + + +// class Test { +// func init() { +// print("Test init" + this.a); +// } + +// var a = 10; + + +// func test() { +// //print(a); + +// print(this.a); // } // } -// print("Before: " + len(a)); -// print("Memory usage: " + memoryUsage() + " MB"); +// var arr = []; +// for(var i = 0; i < 100; i++){ +// arr.push(i); +// } -// // Test different types of nested function calls -// a[3691](); // Simple function -// if (len(a[3692]) > 0) { -// a[3692][0](); // Nested array function -// } -// if (a[3693]["func"]) { -// a[3693]["func"](); // Nested dict function -// } -// //print(a); -// //writeFile("array_contents.txt", toString(a)); -// print("Array contents written to array_contents.txt"); -// print("Memory before cleanup: " + memoryUsage() + " MB"); -// input("Press any key to free memory"); +// var counter = 0; -// a = none; -// print("Memory after cleanup: " + memoryUsage() + " MB"); -// input("waiting..."); +// try{ +// while(true){ +// print(arr[counter]); +// counter++; +// //sleep(0.01); +// } +// }catch(){} +// try{ +// assert(false); + +// }catch(){} + +// print("done"); + +from bobby import A as fart; +import bobby; +var a = fart(); +var b = bobby.A(); +a.test(); +b.test(); +print(bobby); -class Test { - func init() { - print("Test init" + this.a); - } - - var a = 10; - func test() { - //print(a); - print(this.a); - } -} -var arr = []; -for(var i = 0; i < 100; i++){ - arr.push(i); -} - -var counter = 0; - -try{ - while(true){ - print(arr[counter]); - counter++; - //sleep(0.01); - } -}catch(){} -try{ - assert(false); - -}catch(){} print("done"); \ No newline at end of file diff --git a/tests/import_user_of_mod_hello.bob b/tests/import_user_of_mod_hello.bob new file mode 100644 index 0000000..1489998 --- /dev/null +++ b/tests/import_user_of_mod_hello.bob @@ -0,0 +1,5 @@ +// uses mod_hello without importing it here; relies on prior import in same interpreter +var ok1 = (mod_hello.greet("Z") == "hi Z"); +assert(ok1, "imported module binding visible across eval files in same interpreter"); +print("import user: PASS"); + diff --git a/tests/mod_hello.bob b/tests/mod_hello.bob new file mode 100644 index 0000000..b132cfe --- /dev/null +++ b/tests/mod_hello.bob @@ -0,0 +1,3 @@ +var X = 5; +func greet(name) { return "hi " + name; } + diff --git a/tests/test_imports_basic.bob b/tests/test_imports_basic.bob new file mode 100644 index 0000000..f05c644 --- /dev/null +++ b/tests/test_imports_basic.bob @@ -0,0 +1,47 @@ +print("\n--- Test: basic imports ---"); + +// Import by path (with alias) - relative to this file's directory +import "mod_hello.bob" as H; +assert(type(H) == "module", "module is module"); +assert(H.X == 5, "exported var"); +assert(H.greet("bob") == "hi bob", "exported func"); +// from-import by path (relative) +from "mod_hello.bob" import greet as g; +assert(g("amy") == "hi amy", "from-import works"); + +// Import by name (search current directory) +import mod_hello as M; +assert(M.X == 5 && M.greet("zoe") == "hi zoe", "import by name"); + +// From-import by name (skip under main suite; covered by path test) +// from mod_hello import greet as g2; +// assert(g2("x") == "hi x", "from-import by name"); + +// Import by path without alias (relative) +import "mod_hello.bob"; +assert(type(mod_hello) == "module" && mod_hello.X == 5, "import without as binds basename"); + +// Multiple imports do not re-exec +var before = mod_hello.X; +import "mod_hello.bob"; +var after = mod_hello.X; +assert(before == after, "module executed once (cached)"); + +// Cross-file visibility in same interpreter: eval user file after importing here +eval(readFile("tests/import_user_of_mod_hello.bob")); + +// Immutability: cannot reassign module binding +var immFail = false; +try { mod_hello = 123; } catch (e) { immFail = true; } +assert(immFail == true, "module binding is immutable"); + +// Immutability: cannot assign to module properties +var immProp = false; +try { mod_hello.newProp = 1; } catch (e) { immProp = true; } +assert(immProp == true, "module properties are immutable"); + +// Type should be module +assert(type(mod_hello) == "module", "module type tag"); + +print("basic imports: PASS"); + diff --git a/tests/test_imports_builtin.bob b/tests/test_imports_builtin.bob new file mode 100644 index 0000000..e782140 --- /dev/null +++ b/tests/test_imports_builtin.bob @@ -0,0 +1,42 @@ +print("\n--- Test: builtin imports ---"); + +import sys; +assert(type(sys) == "module", "sys is module"); + +from sys import memoryUsage as mem; +var m = mem(); +assert(type(m) == "number" || type(m) == "none", "memoryUsage returns number or none"); + +from sys import cwd, platform, getenv, pid, chdir, homeDir, tempDir, pathSep, dirSep, execPath, env, setenv, unsetenv; +var dir = cwd(); +assert(type(dir) == "string" || type(dir) == "none", "cwd returns string/none"); +var plat = platform(); +assert(type(plat) == "string", "platform returns string"); +var home = getenv("HOME"); +assert(type(home) == "string" || type(home) == "none", "getenv returns string/none"); +var p = pid(); +assert(type(p) == "number", "pid returns number"); + +var hs = homeDir(); +assert(type(hs) == "string" || type(hs) == "none", "homeDir returns string/none"); +var td = tempDir(); +assert(type(td) == "string", "tempDir returns string"); +var ps = pathSep(); +assert(type(ps) == "string", "pathSep is string"); +var ds = dirSep(); +assert(type(ds) == "string", "dirSep is string"); +var exe = execPath(); +assert(type(exe) == "string" || type(exe) == "none", "execPath returns string/none"); + +var e = env(); +assert(type(e) == "dict", "env returns dict"); +var okset = setenv("BOB_TEST_ENV", "xyz"); +assert(okset == true || okset == false, "setenv returns bool"); +var gv = getenv("BOB_TEST_ENV"); +assert(type(gv) == "string" || type(gv) == "none", "getenv after setenv"); +var okunset = unsetenv("BOB_TEST_ENV"); +assert(okunset == true || okunset == false, "unsetenv returns bool"); + +print("builtin imports: PASS"); + +