Built in modules, user modules, ability to disable builtin modules

This commit is contained in:
Bobby Lucero 2025-08-12 00:16:36 -04:00
parent fc63c3e46f
commit 8cdccae214
33 changed files with 1063 additions and 142 deletions

View File

@ -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
)

113
Reference/EMBEDDING.md Normal file
View File

@ -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<Value> 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\"));", "<host>");
// 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 = "<eval>")`
- `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<void(ModuleRegistry::ModuleBuilder&)> init)`
- `void setBuiltinModulePolicy(bool allow)`
- `void setBuiltinModuleAllowList(const std::vector<std::string>& allowed)`
- `void setBuiltinModuleDenyList(const std::vector<std::string>& 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<Value> 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 `<module 'name'>`.
- 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<string>)` allow only listed modules (deny others).
- `setBuiltinModuleDenyList(vector<string>)` 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.

13
bobby.bob Normal file
View File

@ -0,0 +1,13 @@
class A {
var inner = 10;
func test(){
print(this.inner);
}
}
func hello(){
print("hello");
}

View File

@ -0,0 +1,8 @@
#pragma once
class Interpreter;
// Registers all builtin modules with the interpreter
void registerAllBuiltinModules(Interpreter& interpreter);

View File

@ -0,0 +1,8 @@
#pragma once
class Interpreter;
// Register the builtin 'sys' module
void registerSysModule(Interpreter& interpreter);

View File

@ -5,6 +5,7 @@
#include <string>
#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<void(ModuleRegistry::ModuleBuilder&)> 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<std::string>& allowed) {
if (interpreter) interpreter->setBuiltinModuleAllowList(allowed);
else pendingConfigurators.push_back([allowed](Interpreter& I){ I.setBuiltinModuleAllowList(allowed); });
}
void setBuiltinModuleDenyList(const std::vector<std::string>& 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 = "<eval>");
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<std::function<void(Interpreter&)>> pendingConfigurators;
};

View File

@ -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);

View File

@ -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<std::string, TokenType> KEYWORDS {
{"return", RETURN},
{"break", BREAK},
{"continue", CONTINUE},
{"import", IMPORT},
{"from", FROM},
{"as", AS},
{"try", TRY},
{"catch", CATCH},
{"finally", FINALLY},

View File

@ -72,6 +72,8 @@ private:
std::shared_ptr<Stmt> extensionDeclaration();
std::shared_ptr<Stmt> tryStatement();
std::shared_ptr<Stmt> throwStatement();
std::shared_ptr<Stmt> importStatement();
std::shared_ptr<Stmt> fromImportStatement();
std::shared_ptr<Stmt> varDeclaration();

View File

@ -40,6 +40,8 @@ struct StmtVisitor
virtual void visitExtensionStmt(const std::shared_ptr<ExtensionStmt>& stmt, ExecutionContext* context = nullptr) = 0;
virtual void visitTryStmt(const std::shared_ptr<struct TryStmt>& stmt, ExecutionContext* context = nullptr) = 0;
virtual void visitThrowStmt(const std::shared_ptr<struct ThrowStmt>& stmt, ExecutionContext* context = nullptr) = 0;
virtual void visitImportStmt(const std::shared_ptr<struct ImportStmt>& stmt, ExecutionContext* context = nullptr) = 0;
virtual void visitFromImportStmt(const std::shared_ptr<struct FromImportStmt>& stmt, ExecutionContext* context = nullptr) = 0;
};
struct Stmt : public std::enable_shared_from_this<Stmt>
@ -272,3 +274,29 @@ struct ThrowStmt : Stmt {
visitor->visitThrowStmt(std::static_pointer_cast<ThrowStmt>(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<ImportStmt>(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<ImportItem> items;
FromImportStmt(Token kw, Token mod, std::vector<ImportItem> it)
: fromToken(kw), moduleName(mod), items(std::move(it)) {}
void accept(StmtVisitor* visitor, ExecutionContext* context = nullptr) override {
visitor->visitFromImportStmt(std::static_pointer_cast<FromImportStmt>(shared_from_this()), context);
}
};

View File

@ -3,6 +3,7 @@
#include <unordered_map>
#include <string>
#include <memory>
#include <unordered_set>
#include "Value.h"
#include "Lexer.h"
@ -46,11 +47,14 @@ public:
void pruneForClosureCapture();
std::shared_ptr<Environment> getParent() const { return parent; }
// Export all variables (shallow copy) for module namespace
std::unordered_map<std::string, Value> getAll() const { return variables; }
private:
std::unordered_map<std::string, Value> variables;
std::shared_ptr<Environment> parent;
ErrorReporter* errorReporter;
std::unordered_set<std::string> constBindings;
};

View File

@ -42,6 +42,8 @@ public:
void visitExtensionStmt(const std::shared_ptr<ExtensionStmt>& statement, ExecutionContext* context = nullptr) override;
void visitTryStmt(const std::shared_ptr<TryStmt>& statement, ExecutionContext* context = nullptr) override;
void visitThrowStmt(const std::shared_ptr<ThrowStmt>& statement, ExecutionContext* context = nullptr) override;
void visitImportStmt(const std::shared_ptr<ImportStmt>& statement, ExecutionContext* context = nullptr) override;
void visitFromImportStmt(const std::shared_ptr<FromImportStmt>& statement, ExecutionContext* context = nullptr) override;
private:
void execute(const std::shared_ptr<Stmt>& statement, ExecutionContext* context);

View File

@ -7,7 +7,10 @@
#include <functional>
#include "Value.h"
#include "TypeWrapper.h"
#include "RuntimeDiagnostics.h"
#include "ModuleRegistry.h"
#include <unordered_set>
struct Expr;
struct Stmt;
@ -81,6 +84,15 @@ private:
RuntimeDiagnostics diagnostics; // Utility functions for runtime operations
std::unique_ptr<Evaluator> evaluator;
std::unique_ptr<Executor> executor;
// Module cache: module key -> module dict value
std::unordered_map<std::string, Value> moduleCache;
// Builtin module registry
ModuleRegistry builtinModules;
// Import policy flags
bool allowFileImports = true;
bool preferFileOverBuiltin = true;
bool allowBuiltinImports = true;
std::vector<std::string> 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<std::string, Value>& out) const;
std::unordered_map<std::string, Value> 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<std::pair<std::string, std::string>>& items, int line, int column); // name->alias
void setModulePolicy(bool allowFiles, bool preferFiles, const std::vector<std::string>& searchPaths);
void setBuiltinModulePolicy(bool allowBuiltins) { allowBuiltinImports = allowBuiltins; builtinModules.setPolicy(allowBuiltins); }
void setBuiltinModuleAllowList(const std::vector<std::string>& allowed) { builtinModules.setAllowList(allowed); }
void setBuiltinModuleDenyList(const std::vector<std::string>& denied) { builtinModules.setDenyList(denied); }
void registerBuiltinModule(const std::string& name, std::function<Value(Interpreter&)> factory) { builtinModules.registerFactory(name, std::move(factory)); }
// Simple module registration API
using ModuleBuilder = ModuleRegistry::ModuleBuilder;
void registerModule(const std::string& name, std::function<void(ModuleBuilder&)> 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; }

View File

@ -0,0 +1,14 @@
// ModuleDef.h
#pragma once
#include <memory>
#include <string>
#include <unordered_map>
#include "Value.h"
struct Module {
std::string name;
std::shared_ptr<std::unordered_map<std::string, Value>> exports;
};

View File

@ -0,0 +1,70 @@
#pragma once
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <functional>
#include <memory>
#include "TypeWrapper.h" // BuiltinFunction, Value
class Interpreter; // fwd
class ModuleRegistry {
public:
struct ModuleBuilder {
std::string moduleName;
Interpreter& interpreterRef;
std::unordered_map<std::string, Value> exports;
ModuleBuilder(const std::string& n, Interpreter& i) : moduleName(n), interpreterRef(i) {}
void fn(const std::string& name, std::function<Value(std::vector<Value>, int, int)> func) {
exports[name] = Value(std::make_shared<BuiltinFunction>(name, func));
}
void val(const std::string& name, const Value& v) { exports[name] = v; }
};
using Factory = std::function<Value(Interpreter&)>;
void registerFactory(const std::string& name, Factory factory) {
factories[name] = std::move(factory);
}
void registerModule(const std::string& name, std::function<void(ModuleBuilder&)> init) {
registerFactory(name, [name, init](Interpreter& I) -> Value {
ModuleBuilder b(name, I);
init(b);
auto m = std::make_shared<Module>(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<std::string>& allowed) { allowList = std::unordered_set<std::string>(allowed.begin(), allowed.end()); }
void setDenyList(const std::vector<std::string>& denied) { denyList = std::unordered_set<std::string>(denied.begin(), denied.end()); }
private:
std::unordered_map<std::string, Factory> factories;
std::unordered_set<std::string> allowList;
std::unordered_set<std::string> denyList;
bool allowBuiltins = true;
};

View File

@ -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<std::vector<Value> > array_value; // Store arrays as shared_ptr for mutability
std::shared_ptr<std::unordered_map<std::string, Value> > dict_value; // Store dictionaries as shared_ptr for mutability
std::shared_ptr<Module> module_value; // Module object
// Store functions as shared_ptr for proper reference counting
std::shared_ptr<Function> function;
@ -58,6 +63,7 @@ struct Value {
Value(std::vector<Value>&& arr) : type(ValueType::VAL_ARRAY), array_value(std::make_shared<std::vector<Value> >(std::move(arr))) {}
Value(const std::unordered_map<std::string, Value>& dict) : type(ValueType::VAL_DICT), dict_value(std::make_shared<std::unordered_map<std::string, Value> >(dict)) {}
Value(std::unordered_map<std::string, Value>&& dict) : type(ValueType::VAL_DICT), dict_value(std::make_shared<std::unordered_map<std::string, Value> >(std::move(dict))) {}
Value(std::shared_ptr<Module> 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<std::string, Value>& 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<Module>&);
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<std::unordered_map<std::string, Value>> exports;
Module() = default;
Module(const std::string& n, const std::unordered_map<std::string, Value>& dict)
: name(n), exports(std::make_shared<std::unordered_map<std::string, Value>>(dict)) {}
};
// Global constants for common values
extern const Value NONE_VALUE;
extern const Value TRUE_VALUE;

View File

@ -0,0 +1,8 @@
#include "register.h"
#include "sys.h"
void registerAllBuiltinModules(Interpreter& interpreter) {
registerSysModule(interpreter);
}

View File

@ -0,0 +1,146 @@
#include "sys.h"
#include "Interpreter.h"
#include "Environment.h"
#include "Lexer.h" // for Token and IDENTIFIER
#include <unistd.h>
#include <limits.h>
#include <cstdlib>
#include <cstring>
#if defined(__APPLE__)
#define DYLD_BOOL DYLD_BOOL_IGNORED
#include <mach-o/dyld.h>
#undef DYLD_BOOL
#endif
void registerSysModule(Interpreter& interpreter) {
interpreter.registerModule("sys", [](Interpreter::ModuleBuilder& m) {
Interpreter& I = m.interpreterRef;
m.fn("memoryUsage", [&I](std::vector<Value>, 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<Value> a, int, int) -> Value {
int code = 0; if (!a.empty() && a[0].isNumber()) code = static_cast<int>(a[0].asNumber());
std::exit(code);
return NONE_VALUE;
});
m.fn("cwd", [](std::vector<Value>, 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<Value>, 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<Value> 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<Value>, int, int) -> Value {
return Value(static_cast<double>(getpid()));
});
m.fn("chdir", [](std::vector<Value> 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<Value>, 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<Value>, 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<Value>, int, int) -> Value {
#if defined(_WIN32)
return Value(std::string(";"));
#else
return Value(std::string(":"));
#endif
});
m.fn("dirSep", [](std::vector<Value>, int, int) -> Value {
#if defined(_WIN32)
return Value(std::string("\\"));
#else
return Value(std::string("/"));
#endif
});
m.fn("execPath", [](std::vector<Value>, 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<Value>, int, int) -> Value {
std::unordered_map<std::string, Value> 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<Value> 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<Value> 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
});
});
}

View File

@ -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<char>(file), std::istreambuf_iterator<char>());
}
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<char>(file)), std::istreambuf_iterator<char>());
errorReporter.loadSource(src, path);
interpreter->setErrorReporter(&errorReporter);
try {
// Connect error reporter to lexer
lexer.setErrorReporter(&errorReporter);
std::vector<Token> tokens = lexer.Tokenize(std::move(source));
auto tokens = lexer.Tokenize(src);
Parser p(tokens);
// Connect error reporter to parser
p.setErrorReporter(&errorReporter);
std::vector<sptr(Stmt)> 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;
}
return true;
} catch (...) { return false; }
}
// 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;
}
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; }
}

View File

@ -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<Value> 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();
}

View File

@ -586,6 +586,11 @@ std::shared_ptr<Expr> 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<Stmt> 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<Stmt> 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<FromImportStmt::ImportItem> 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.");

View File

@ -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;

View File

@ -316,7 +316,15 @@ Value Evaluator::visitPropertyExpr(const std::shared_ptr<PropertyExpr>& 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<PropertyExpr>& 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<PropertyExpr>& 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_ptr<PropertyAssignExp
Value value = expr->value->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<std::string, Value>& dict = object.asDict();
dict[propertyName] = value;

View File

@ -312,6 +312,40 @@ void Executor::visitThrowStmt(const std::shared_ptr<ThrowStmt>& statement, Execu
}
}
void Executor::visitImportStmt(const std::shared_ptr<ImportStmt>& 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<FromImportStmt>& 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<std::pair<std::string,std::string>> 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<AssignStmt>& statement, ExecutionContext* context) {
Value value = statement->value->accept(evaluator);

View File

@ -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 <filesystem>
#include <unistd.h>
#include <fstream>
#include <iostream>
Interpreter::Interpreter(bool isInteractive)
@ -12,6 +17,11 @@ Interpreter::Interpreter(bool isInteractive)
evaluator = std::make_unique<Evaluator>(this);
executor = std::make_unique<Executor>(this, evaluator.get());
environment = std::make_shared<Environment>();
// 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>& 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<std::string>& 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<std::string>& 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<char>(file)), std::istreambuf_iterator<char>());
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<Token> toks = lx.Tokenize(code);
Parser p(toks); if (errorReporter) p.setErrorReporter(errorReporter);
std::vector<std::shared_ptr<Stmt>> stmts = p.parse();
// Isolated environment
auto saved = getEnvironment();
auto modEnv = std::make_shared<Environment>(saved);
modEnv->setErrorReporter(errorReporter);
setEnvironment(modEnv);
// Execute
executor->interpret(stmts);
// Build module object from env
std::unordered_map<std::string, Value> 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<Module>(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<std::pair<std::string, std::string>>& 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<std::string, Value> const* src = nullptr;
std::unordered_map<std::string, Value> 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<BuiltinFunction> func) {
builtinFunctions.push_back(func);

View File

@ -5,3 +5,11 @@ 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<Module>& mod) {
if (mod && !mod->name.empty()) {
return std::string("<module '") + mod->name + "'>";
}
return std::string("<module>");
}

View File

@ -200,6 +200,8 @@ void BobStdLib::addToEnvironment(std::shared_ptr<Environment> env, Interpreter&
typeName = "array";
} else if (args[0].isDict()) {
typeName = "dict";
} else if (args[0].isModule()) {
typeName = "module";
} else {
typeName = "unknown";
}

View File

@ -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.");

166
tests.bob
View File

@ -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);
// }
// print("Before: " + len(a));
// print("Memory usage: " + memoryUsage() + " MB");
// var a = 10;
// // 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
// }
// func test() {
// //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...");
// 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");
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");

View File

@ -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");

3
tests/mod_hello.bob Normal file
View File

@ -0,0 +1,3 @@
var X = 5;
func greet(name) { return "hi " + name; }

View File

@ -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");

View File

@ -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");