Built in modules, user modules, ability to disable builtin modules
This commit is contained in:
parent
fc63c3e46f
commit
8cdccae214
@ -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
113
Reference/EMBEDDING.md
Normal 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
13
bobby.bob
Normal file
@ -0,0 +1,13 @@
|
||||
class A {
|
||||
var inner = 10;
|
||||
|
||||
func test(){
|
||||
print(this.inner);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func hello(){
|
||||
print("hello");
|
||||
}
|
||||
|
||||
8
src/headers/builtinModules/register.h
Normal file
8
src/headers/builtinModules/register.h
Normal file
@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
class Interpreter;
|
||||
|
||||
// Registers all builtin modules with the interpreter
|
||||
void registerAllBuiltinModules(Interpreter& interpreter);
|
||||
|
||||
|
||||
8
src/headers/builtinModules/sys.h
Normal file
8
src/headers/builtinModules/sys.h
Normal file
@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
class Interpreter;
|
||||
|
||||
// Register the builtin 'sys' module
|
||||
void registerSysModule(Interpreter& interpreter);
|
||||
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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},
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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; }
|
||||
|
||||
14
src/headers/runtime/ModuleDef.h
Normal file
14
src/headers/runtime/ModuleDef.h
Normal 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;
|
||||
};
|
||||
|
||||
|
||||
70
src/headers/runtime/ModuleRegistry.h
Normal file
70
src/headers/runtime/ModuleRegistry.h
Normal 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;
|
||||
};
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
8
src/sources/builtinModules/register.cpp
Normal file
8
src/sources/builtinModules/register.cpp
Normal file
@ -0,0 +1,8 @@
|
||||
#include "register.h"
|
||||
#include "sys.h"
|
||||
|
||||
void registerAllBuiltinModules(Interpreter& interpreter) {
|
||||
registerSysModule(interpreter);
|
||||
}
|
||||
|
||||
|
||||
146
src/sources/builtinModules/sys.cpp
Normal file
146
src/sources/builtinModules/sys.cpp
Normal 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
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
@ -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.");
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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>");
|
||||
}
|
||||
|
||||
@ -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";
|
||||
}
|
||||
|
||||
@ -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
166
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);
|
||||
// }
|
||||
|
||||
// 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");
|
||||
5
tests/import_user_of_mod_hello.bob
Normal file
5
tests/import_user_of_mod_hello.bob
Normal 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
3
tests/mod_hello.bob
Normal file
@ -0,0 +1,3 @@
|
||||
var X = 5;
|
||||
func greet(name) { return "hi " + name; }
|
||||
|
||||
47
tests/test_imports_basic.bob
Normal file
47
tests/test_imports_basic.bob
Normal 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");
|
||||
|
||||
42
tests/test_imports_builtin.bob
Normal file
42
tests/test_imports_builtin.bob
Normal 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");
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user