From 7f7c6e438d5623e5040a4e8db6227bfdaac21fdb Mon Sep 17 00:00:00 2001 From: Bobby Lucero Date: Tue, 12 Aug 2025 03:26:44 -0400 Subject: [PATCH] Started fleshing out built in modules. added policy templates for module safety --- CMakeLists.txt | 10 +- src/headers/builtinModules/base64_module.h | 8 + src/headers/builtinModules/eval.h | 8 + src/headers/builtinModules/io.h | 8 + src/headers/builtinModules/json.h | 8 + src/headers/builtinModules/math_module.h | 8 + src/headers/builtinModules/os.h | 8 + src/headers/builtinModules/path_module.h | 8 + src/headers/builtinModules/rand.h | 8 + src/headers/builtinModules/time_module.h | 8 + src/headers/cli/bob.h | 64 ++++ src/headers/parsing/Statement.h | 7 +- src/headers/runtime/Interpreter.h | 10 + src/sources/builtinModules/base64.cpp | 41 +++ src/sources/builtinModules/eval.cpp | 64 ++++ src/sources/builtinModules/io.cpp | 72 +++++ src/sources/builtinModules/json.cpp | 83 ++++++ src/sources/builtinModules/math.cpp | 56 ++++ src/sources/builtinModules/os.cpp | 106 +++++++ src/sources/builtinModules/path.cpp | 49 ++++ src/sources/builtinModules/rand.cpp | 35 +++ src/sources/builtinModules/register.cpp | 17 ++ src/sources/builtinModules/sys.cpp | 137 ++------- src/sources/builtinModules/time.cpp | 31 ++ src/sources/cli/main.cpp | 23 +- src/sources/parsing/Parser.cpp | 5 + src/sources/runtime/Executor.cpp | 14 + src/sources/runtime/Interpreter.cpp | 11 +- src/sources/stdlib/BobStdLib.cpp | 325 +++------------------ test_bob_language.bob | 47 +-- tests.bob | 16 +- tests/test_imports_basic.bob | 2 +- tests/test_imports_builtin.bob | 35 +-- tests/test_os_basic.bob | 29 ++ 34 files changed, 873 insertions(+), 488 deletions(-) create mode 100644 src/headers/builtinModules/base64_module.h create mode 100644 src/headers/builtinModules/eval.h create mode 100644 src/headers/builtinModules/io.h create mode 100644 src/headers/builtinModules/json.h create mode 100644 src/headers/builtinModules/math_module.h create mode 100644 src/headers/builtinModules/os.h create mode 100644 src/headers/builtinModules/path_module.h create mode 100644 src/headers/builtinModules/rand.h create mode 100644 src/headers/builtinModules/time_module.h create mode 100644 src/sources/builtinModules/base64.cpp create mode 100644 src/sources/builtinModules/eval.cpp create mode 100644 src/sources/builtinModules/io.cpp create mode 100644 src/sources/builtinModules/json.cpp create mode 100644 src/sources/builtinModules/math.cpp create mode 100644 src/sources/builtinModules/os.cpp create mode 100644 src/sources/builtinModules/path.cpp create mode 100644 src/sources/builtinModules/rand.cpp create mode 100644 src/sources/builtinModules/time.cpp create mode 100644 tests/test_os_basic.bob diff --git a/CMakeLists.txt b/CMakeLists.txt index 6b6c8c2..e2fe37c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,11 +45,11 @@ elseif(MSVC) endif() # Collect source files -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") +file(GLOB_RECURSE BOB_RUNTIME_SOURCES CONFIGURE_DEPENDS "src/sources/runtime/*.cpp") +file(GLOB_RECURSE BOB_PARSING_SOURCES CONFIGURE_DEPENDS "src/sources/parsing/*.cpp") +file(GLOB_RECURSE BOB_STDLIB_SOURCES CONFIGURE_DEPENDS "src/sources/stdlib/*.cpp") +file(GLOB_RECURSE BOB_BUILTIN_SOURCES CONFIGURE_DEPENDS "src/sources/builtinModules/*.cpp") +file(GLOB_RECURSE BOB_CLI_SOURCES CONFIGURE_DEPENDS "src/sources/cli/*.cpp") # All source files set(BOB_ALL_SOURCES diff --git a/src/headers/builtinModules/base64_module.h b/src/headers/builtinModules/base64_module.h new file mode 100644 index 0000000..8f75b00 --- /dev/null +++ b/src/headers/builtinModules/base64_module.h @@ -0,0 +1,8 @@ +#pragma once + +class Interpreter; + +// Register the builtin 'base64' module +void registerBase64Module(Interpreter& interpreter); + + diff --git a/src/headers/builtinModules/eval.h b/src/headers/builtinModules/eval.h new file mode 100644 index 0000000..519cc95 --- /dev/null +++ b/src/headers/builtinModules/eval.h @@ -0,0 +1,8 @@ +#pragma once + +class Interpreter; + +// Register the builtin 'eval' module providing eval(code) and evalFile(path) +void registerEvalModule(Interpreter& interpreter); + + diff --git a/src/headers/builtinModules/io.h b/src/headers/builtinModules/io.h new file mode 100644 index 0000000..bbae3af --- /dev/null +++ b/src/headers/builtinModules/io.h @@ -0,0 +1,8 @@ +#pragma once + +class Interpreter; + +// Register the builtin 'io' module (file I/O and input) +void registerIoModule(Interpreter& interpreter); + + diff --git a/src/headers/builtinModules/json.h b/src/headers/builtinModules/json.h new file mode 100644 index 0000000..b0fd70e --- /dev/null +++ b/src/headers/builtinModules/json.h @@ -0,0 +1,8 @@ +#pragma once + +class Interpreter; + +// Register the builtin 'json' module +void registerJsonModule(Interpreter& interpreter); + + diff --git a/src/headers/builtinModules/math_module.h b/src/headers/builtinModules/math_module.h new file mode 100644 index 0000000..1f4c606 --- /dev/null +++ b/src/headers/builtinModules/math_module.h @@ -0,0 +1,8 @@ +#pragma once + +class Interpreter; + +// Register the builtin 'math' module +void registerMathModule(Interpreter& interpreter); + + diff --git a/src/headers/builtinModules/os.h b/src/headers/builtinModules/os.h new file mode 100644 index 0000000..6764206 --- /dev/null +++ b/src/headers/builtinModules/os.h @@ -0,0 +1,8 @@ +#pragma once + +class Interpreter; + +// Register the builtin 'os' module +void registerOsModule(Interpreter& interpreter); + + diff --git a/src/headers/builtinModules/path_module.h b/src/headers/builtinModules/path_module.h new file mode 100644 index 0000000..b5d950f --- /dev/null +++ b/src/headers/builtinModules/path_module.h @@ -0,0 +1,8 @@ +#pragma once + +class Interpreter; + +// Register the builtin 'path' module (path utilities) +void registerPathModule(Interpreter& interpreter); + + diff --git a/src/headers/builtinModules/rand.h b/src/headers/builtinModules/rand.h new file mode 100644 index 0000000..cb1a694 --- /dev/null +++ b/src/headers/builtinModules/rand.h @@ -0,0 +1,8 @@ +#pragma once + +class Interpreter; + +// Register the builtin 'rand' module +void registerRandModule(Interpreter& interpreter); + + diff --git a/src/headers/builtinModules/time_module.h b/src/headers/builtinModules/time_module.h new file mode 100644 index 0000000..6e3bae6 --- /dev/null +++ b/src/headers/builtinModules/time_module.h @@ -0,0 +1,8 @@ +#pragma once + +class Interpreter; + +// Register the builtin 'time' module (time functions) +void registerTimeModule(Interpreter& interpreter); + + diff --git a/src/headers/cli/bob.h b/src/headers/cli/bob.h index 9fd4298..ec2a4e2 100644 --- a/src/headers/cli/bob.h +++ b/src/headers/cli/bob.h @@ -49,6 +49,70 @@ public: bool evalFile(const std::string& path); bool evalString(const std::string& code, const std::string& filename = ""); + // Safety policy helpers (public API) + // Set all safety-related policies at once + void setSafetyPolicy( + bool allowBuiltins, + const std::vector& allowList, + const std::vector& denyList, + bool allowFileImports, + bool preferFileOverBuiltin, + const std::vector& searchPaths + ) { + if (interpreter) { + interpreter->setBuiltinModulePolicy(allowBuiltins); + interpreter->setBuiltinModuleAllowList(allowList); + interpreter->setBuiltinModuleDenyList(denyList); + interpreter->setModulePolicy(allowFileImports, preferFileOverBuiltin, searchPaths); + } else { + pendingConfigurators.push_back([=](Interpreter& I){ + I.setBuiltinModulePolicy(allowBuiltins); + I.setBuiltinModuleAllowList(allowList); + I.setBuiltinModuleDenyList(denyList); + I.setModulePolicy(allowFileImports, preferFileOverBuiltin, searchPaths); + }); + } + } + + // Simple presets: "open", "safe", "locked" + void setSafetyPreset(const std::string& preset) { + if (preset == "open") { + setSafetyPolicy( + true, /* allowBuiltins */ + {}, /* allowList -> empty means allow all */ + {}, + true, /* allowFileImports */ + true, /* preferFileOverBuiltin */ + {} /* searchPaths */ + ); + } else if (preset == "safe") { + // Allow only pure/harmless modules by default + setSafetyPolicy( + true, + std::vector{ + "sys", "time", "rand", "math", "path", "base64" + }, + std::vector{ /* denyList empty when allowList is used */ }, + false, /* disallow file-based imports */ + true, + {} + ); + } else if (preset == "locked") { + // No builtins visible; no file imports + setSafetyPolicy( + false, + {}, + {}, + false, + true, + {} + ); + } else { + // Default to safe + setSafetyPreset("safe"); + } + } + private: void ensureInterpreter(bool interactive); void applyPendingConfigs() { diff --git a/src/headers/parsing/Statement.h b/src/headers/parsing/Statement.h index af86848..2a9e4d9 100644 --- a/src/headers/parsing/Statement.h +++ b/src/headers/parsing/Statement.h @@ -291,11 +291,14 @@ struct ImportStmt : Stmt { // from module import name [as alias], name2 ... struct FromImportStmt : Stmt { Token fromToken; // FROM - Token moduleName; // IDENTIFIER + Token moduleName; // IDENTIFIER or STRING struct ImportItem { Token name; bool hasAlias; Token alias; }; std::vector items; + bool importAll = false; // true for: from module import *; FromImportStmt(Token kw, Token mod, std::vector it) - : fromToken(kw), moduleName(mod), items(std::move(it)) {} + : fromToken(kw), moduleName(mod), items(std::move(it)), importAll(false) {} + FromImportStmt(Token kw, Token mod, bool all) + : fromToken(kw), moduleName(mod), importAll(all) {} void accept(StmtVisitor* visitor, ExecutionContext* context = nullptr) override { visitor->visitFromImportStmt(std::static_pointer_cast(shared_from_this()), context); } diff --git a/src/headers/runtime/Interpreter.h b/src/headers/runtime/Interpreter.h index b7264a5..85082f5 100644 --- a/src/headers/runtime/Interpreter.h +++ b/src/headers/runtime/Interpreter.h @@ -172,9 +172,19 @@ public: void setLastErrorSite(int line, int column) { lastErrorLine = line; lastErrorColumn = column; } int getLastErrorLine() const { return lastErrorLine; } int getLastErrorColumn() const { return lastErrorColumn; } + + // Process/host metadata (for sys module) + void setArgv(const std::vector& args, const std::string& executablePath) { argvData = args; executableFile = executablePath; } + std::vector getArgv() const { return argvData; } + std::string getExecutablePath() const { return executableFile; } + std::unordered_map getModuleCacheSnapshot() const { return moduleCache; } + private: Value runTrampoline(Value initialResult); + // Stored argv/executable for sys module + std::vector argvData; + std::string executableFile; }; diff --git a/src/sources/builtinModules/base64.cpp b/src/sources/builtinModules/base64.cpp new file mode 100644 index 0000000..cfef7dc --- /dev/null +++ b/src/sources/builtinModules/base64.cpp @@ -0,0 +1,41 @@ +#include "base64_module.h" +#include "Interpreter.h" +#include + +static const char* B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +static std::string b64encode(const std::string& in){ + std::string out; out.reserve(((in.size()+2)/3)*4); + int val=0, valb=-6; + for (unsigned char c : in){ + val = (val<<8) + c; + valb += 8; + while (valb >= 0){ out.push_back(B64[(val>>valb)&0x3F]); valb -= 6; } + } + if (valb>-6) out.push_back(B64[((val<<8)>>(valb+8))&0x3F]); + while (out.size()%4) out.push_back('='); + return out; +} + +static std::string b64decode(const std::string& in){ + std::vector T(256,-1); for (int i=0;i<64;i++) T[(unsigned char)B64[i]]=i; + std::string out; out.reserve((in.size()*3)/4); + int val=0, valb=-8; + for (unsigned char c : in){ if (T[c]==-1) break; val=(val<<6)+T[c]; valb+=6; if (valb>=0){ out.push_back(char((val>>valb)&0xFF)); valb-=8; } } + return out; +} + +void registerBase64Module(Interpreter& interpreter) { + interpreter.registerModule("base64", [](Interpreter::ModuleBuilder& m) { + m.fn("encode", [](std::vector a, int, int) -> Value { + if (a.size()!=1 || !a[0].isString()) return NONE_VALUE; + return Value(b64encode(a[0].asString())); + }); + m.fn("decode", [](std::vector a, int, int) -> Value { + if (a.size()!=1 || !a[0].isString()) return NONE_VALUE; + return Value(b64decode(a[0].asString())); + }); + }); +} + + diff --git a/src/sources/builtinModules/eval.cpp b/src/sources/builtinModules/eval.cpp new file mode 100644 index 0000000..af1f848 --- /dev/null +++ b/src/sources/builtinModules/eval.cpp @@ -0,0 +1,64 @@ +#include "eval.h" +#include "Interpreter.h" +#include "ErrorReporter.h" +#include "Lexer.h" +#include "Parser.h" +#include +#include + +void registerEvalModule(Interpreter& interpreter) { + interpreter.registerModule("eval", [](Interpreter::ModuleBuilder& m) { + ErrorReporter* er = m.interpreterRef.getErrorReporter(); + m.fn("eval", [er, &I = m.interpreterRef](std::vector args, int line, int column) -> Value { + if (args.size() != 1 || !args[0].isString()) { + if (er) er->reportError(line, column, "Invalid Arguments", "eval expects exactly 1 string argument", "eval"); + throw std::runtime_error("eval expects exactly 1 string argument"); + } + std::string code = args[0].asString(); + std::string evalName = ""; + try { + if (er) er->pushSource(code, evalName); + Lexer lx; if (er) lx.setErrorReporter(er); + auto toks = lx.Tokenize(code); + Parser p(toks); if (er) p.setErrorReporter(er); + auto stmts = p.parse(); + I.interpret(stmts); + return NONE_VALUE; + } catch (...) { + if (er) er->popSource(); + throw; + } + if (er) er->popSource(); + }); + + m.fn("evalFile", [er, &I = m.interpreterRef](std::vector args, int line, int column) -> Value { + if (args.size() != 1 || !args[0].isString()) { + if (er) er->reportError(line, column, "Invalid Arguments", "evalFile expects exactly 1 string argument (path)", "evalFile"); + throw std::runtime_error("evalFile expects exactly 1 string argument (path)"); + } + std::string filename = args[0].asString(); + std::ifstream f(filename); + if (!f.is_open()) { + if (er) er->reportError(line, column, "StdLib Error", "Could not open file: " + filename, ""); + throw std::runtime_error("Could not open file: " + filename); + } + std::stringstream buf; buf << f.rdbuf(); f.close(); + std::string code = buf.str(); + try { + if (er) er->pushSource(code, filename); + Lexer lx; if (er) lx.setErrorReporter(er); + auto toks = lx.Tokenize(code); + Parser p(toks); if (er) p.setErrorReporter(er); + auto stmts = p.parse(); + I.interpret(stmts); + return NONE_VALUE; + } catch (...) { + if (er) er->popSource(); + throw; + } + if (er) er->popSource(); + }); + }); +} + + diff --git a/src/sources/builtinModules/io.cpp b/src/sources/builtinModules/io.cpp new file mode 100644 index 0000000..e74f315 --- /dev/null +++ b/src/sources/builtinModules/io.cpp @@ -0,0 +1,72 @@ +#include +#include +#include "io.h" +#include "Interpreter.h" +#include "ErrorReporter.h" + +void registerIoModule(Interpreter& interpreter) { + interpreter.registerModule("io", [](Interpreter::ModuleBuilder& m) { + ErrorReporter* er = m.interpreterRef.getErrorReporter(); + + m.fn("readFile", [er](std::vector a, int line, int col) -> Value { + if (a.empty() || !a[0].isString() || a.size() > 2 || (a.size() == 2 && !a[1].isString())) { + if (er) er->reportError(line, col, "Invalid Arguments", "readFile(path[, mode]) expects 1-2 args (strings)", "readFile"); + throw std::runtime_error("readFile(path[, mode]) expects 1-2 string args"); + } + std::string mode = (a.size() == 2) ? a[1].asString() : std::string("r"); + std::ios_base::openmode om = std::ios::in; + if (mode.find('b') != std::string::npos) om |= std::ios::binary; + std::ifstream f(a[0].asString(), om); + if (!f.is_open()) { + if (er) er->reportError(line, col, "StdLib Error", "Could not open file", a[0].asString()); + throw std::runtime_error("Could not open file"); + } + std::stringstream buf; buf << f.rdbuf(); f.close(); + return Value(buf.str()); + }); + + m.fn("writeFile", [er](std::vector a, int line, int col) -> Value { + if (a.size() < 2 || a.size() > 3 || !a[0].isString() || !a[1].isString() || (a.size() == 3 && !a[2].isString())) { + if (er) er->reportError(line, col, "Invalid Arguments", "writeFile(path, data[, mode]) expects 2-3 args (strings)", "writeFile"); + throw std::runtime_error("writeFile(path, data[, mode]) expects 2-3 string args"); + } + std::string mode = (a.size() == 3) ? a[2].asString() : std::string("w"); + std::ios_base::openmode om = std::ios::out; + if (mode.find('b') != std::string::npos) om |= std::ios::binary; + if (mode.find('a') != std::string::npos) om |= std::ios::app; else om |= std::ios::trunc; + std::ofstream f(a[0].asString(), om); + if (!f.is_open()) { + if (er) er->reportError(line, col, "StdLib Error", "Could not create file", a[0].asString()); + throw std::runtime_error("Could not create file"); + } + f << a[1].asString(); f.close(); + return NONE_VALUE; + }); + + m.fn("readLines", [er](std::vector a, int line, int col) -> Value { + if (a.size() != 1 || !a[0].isString()) { + if (er) er->reportError(line, col, "Invalid Arguments", "readLines(path) expects 1 string arg", "readLines"); + throw std::runtime_error("readLines(path) expects 1 string arg"); + } + std::ifstream f(a[0].asString()); + if (!f.is_open()) { + if (er) er->reportError(line, col, "StdLib Error", "Could not open file", a[0].asString()); + throw std::runtime_error("Could not open file"); + } + std::vector lines; std::string s; + while (std::getline(f, s)) lines.emplace_back(s); + f.close(); + return Value(lines); + }); + + m.fn("exists", [](std::vector a, int, int) -> Value { + if (a.size() != 1 || !a[0].isString()) return Value(false); + std::ifstream f(a[0].asString()); bool ok = f.good(); f.close(); + return Value(ok); + }); + + // input remains a global in stdlib; not provided here + }); +} + + diff --git a/src/sources/builtinModules/json.cpp b/src/sources/builtinModules/json.cpp new file mode 100644 index 0000000..9be8ec0 --- /dev/null +++ b/src/sources/builtinModules/json.cpp @@ -0,0 +1,83 @@ +#include "json.h" +#include "Interpreter.h" +#include +#include + +// Minimal JSON parser/stringifier (numbers, strings, booleans, null, arrays, objects) +namespace { + struct Cursor { const std::string* s; size_t i = 0; }; + void skipWs(Cursor& c){ while (c.i < c.s->size() && std::isspace(static_cast((*c.s)[c.i]))) ++c.i; } + bool match(Cursor& c, char ch){ skipWs(c); if (c.i < c.s->size() && (*c.s)[c.i]==ch){ ++c.i; return true;} return false; } + std::string parseString(Cursor& c){ + if (!match(c,'"')) return {}; + std::string out; while (c.i < c.s->size()){ + char ch = (*c.s)[c.i++]; + if (ch=='"') break; + if (ch=='\\' && c.i < c.s->size()){ + char e = (*c.s)[c.i++]; + switch(e){ case '"': out+='"'; break; case '\\': out+='\\'; break; case '/': out+='/'; break; case 'b': out+='\b'; break; case 'f': out+='\f'; break; case 'n': out+='\n'; break; case 'r': out+='\r'; break; case 't': out+='\t'; break; default: out+=e; } + } else out+=ch; + } + return out; + } + double parseNumber(Cursor& c){ skipWs(c); size_t start=c.i; while (c.isize() && (std::isdigit((*c.s)[c.i])||(*c.s)[c.i]=='-'||(*c.s)[c.i]=='+'||(*c.s)[c.i]=='.'||(*c.s)[c.i]=='e'||(*c.s)[c.i]=='E')) ++c.i; return std::stod(c.s->substr(start,c.i-start)); } + Value parseValue(Cursor& c); + Value parseArray(Cursor& c){ + match(c,'['); std::vector arr; skipWs(c); if (match(c,']')) return Value(arr); + while (true){ arr.push_back(parseValue(c)); skipWs(c); if (match(c,']')) break; match(c,','); } + return Value(arr); + } + Value parseObject(Cursor& c){ + match(c,'{'); std::unordered_map obj; skipWs(c); if (match(c,'}')) return Value(obj); + while (true){ std::string k = parseString(c); match(c,':'); Value v = parseValue(c); obj.emplace(k, v); skipWs(c); if (match(c,'}')) break; match(c,','); } + return Value(obj); + } + Value parseValue(Cursor& c){ skipWs(c); if (c.i>=c.s->size()) return NONE_VALUE; char ch=(*c.s)[c.i]; + if (ch=='"') return Value(parseString(c)); + if (ch=='[') return parseArray(c); + if (ch=='{') return parseObject(c); + if (!c.s->compare(c.i,4,"true")) { c.i+=4; return Value(true);} + if (!c.s->compare(c.i,5,"false")) { c.i+=5; return Value(false);} + if (!c.s->compare(c.i,4,"null")) { c.i+=4; return NONE_VALUE;} + return Value(parseNumber(c)); + } + + std::string escapeString(const std::string& s){ + std::string out; out.reserve(s.size()+2); out.push_back('"'); + for(char ch: s){ + switch(ch){ case '"': out+="\\\""; break; case '\\': out+="\\\\"; break; case '\n': out+="\\n"; break; case '\r': out+="\\r"; break; case '\t': out+="\\t"; break; default: out+=ch; } + } + out.push_back('"'); return out; + } + std::string stringifyValue(const Value& v){ + switch(v.type){ + case VAL_NONE: return "null"; + case VAL_BOOLEAN: return v.asBoolean()?"true":"false"; + case VAL_NUMBER: return v.toString(); + case VAL_STRING: return escapeString(v.asString()); + case VAL_ARRAY: { + const auto& a=v.asArray(); std::string out="["; for(size_t i=0;i a, int, int) -> Value { + if (a.size() != 1 || !a[0].isString()) return NONE_VALUE; + Cursor c{&a[0].asString(), 0}; + return parseValue(c); + }); + m.fn("stringify", [](std::vector a, int, int) -> Value { + if (a.size() != 1) return Value(std::string("null")); + return Value(stringifyValue(a[0])); + }); + }); +} + + diff --git a/src/sources/builtinModules/math.cpp b/src/sources/builtinModules/math.cpp new file mode 100644 index 0000000..d2b3a7b --- /dev/null +++ b/src/sources/builtinModules/math.cpp @@ -0,0 +1,56 @@ +#include "math_module.h" +#include "Interpreter.h" +#include + +static Value unary_math(std::vector a, double(*fn)(double)){ + if (a.size() != 1 || !a[0].isNumber()) return NONE_VALUE; + return Value(fn(a[0].asNumber())); +} + +void registerMathModule(Interpreter& interpreter) { + interpreter.registerModule("math", [](Interpreter::ModuleBuilder& m) { + m.fn("sin", [](std::vector a, int, int)->Value{ return unary_math(a, std::sin); }); + m.fn("cos", [](std::vector a, int, int)->Value{ return unary_math(a, std::cos); }); + m.fn("tan", [](std::vector a, int, int)->Value{ return unary_math(a, std::tan); }); + m.fn("asin", [](std::vector a, int, int)->Value{ return unary_math(a, std::asin); }); + m.fn("acos", [](std::vector a, int, int)->Value{ return unary_math(a, std::acos); }); + m.fn("atan", [](std::vector a, int, int)->Value{ return unary_math(a, std::atan); }); + m.fn("sinh", [](std::vector a, int, int)->Value{ return unary_math(a, std::sinh); }); + m.fn("cosh", [](std::vector a, int, int)->Value{ return unary_math(a, std::cosh); }); + m.fn("tanh", [](std::vector a, int, int)->Value{ return unary_math(a, std::tanh); }); + m.fn("exp", [](std::vector a, int, int)->Value{ return unary_math(a, std::exp); }); + m.fn("log", [](std::vector a, int, int)->Value{ return unary_math(a, std::log); }); + m.fn("log10", [](std::vector a, int, int)->Value{ return unary_math(a, std::log10); }); + m.fn("sqrt", [](std::vector a, int, int)->Value{ return unary_math(a, std::sqrt); }); + m.fn("ceil", [](std::vector a, int, int)->Value{ return unary_math(a, std::ceil); }); + m.fn("floor", [](std::vector a, int, int)->Value{ return unary_math(a, std::floor); }); + m.fn("round", [](std::vector a, int, int)->Value{ + if (a.size() != 1 || !a[0].isNumber()) return NONE_VALUE; + return Value(std::round(a[0].asNumber())); + }); + m.fn("abs", [](std::vector a, int, int)->Value{ + if (a.size() != 1 || !a[0].isNumber()) return NONE_VALUE; + return Value(std::fabs(a[0].asNumber())); + }); + m.fn("pow", [](std::vector a, int, int)->Value{ + if (a.size() != 2 || !a[0].isNumber() || !a[1].isNumber()) return NONE_VALUE; + return Value(std::pow(a[0].asNumber(), a[1].asNumber())); + }); + m.fn("min", [](std::vector a, int, int)->Value{ + if (a.empty()) return NONE_VALUE; + double mval = a[0].isNumber()? a[0].asNumber() : 0.0; + for(size_t i=1;i a, int, int)->Value{ + if (a.empty()) return NONE_VALUE; + double mval = a[0].isNumber()? a[0].asNumber() : 0.0; + for(size_t i=1;i +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + +void registerOsModule(Interpreter& interpreter) { + interpreter.registerModule("os", [](Interpreter::ModuleBuilder& m) { + // Process + m.fn("getcwd", [](std::vector, int, int) -> Value { + char buf[PATH_MAX]; + if (getcwd(buf, sizeof(buf))) return Value(std::string(buf)); + return NONE_VALUE; + }); + m.fn("chdir", [](std::vector a, int, int) -> Value { + if (a.size() != 1 || !a[0].isString()) return Value(false); + int rc = ::chdir(a[0].asString().c_str()); + return Value(rc == 0); + }); + m.fn("getpid", [](std::vector, int, int) -> Value { + return Value(static_cast(getpid())); + }); + m.fn("getppid", [](std::vector, int, int) -> Value { + return Value(static_cast(getppid())); + }); + m.fn("name", [](std::vector, int, int) -> Value { +#if defined(_WIN32) + return Value(std::string("nt")); +#else + return Value(std::string("posix")); +#endif + }); + + // Filesystem + m.fn("listdir", [](std::vector a, int, int) -> Value { + std::string path = "."; + if (!a.empty() && a[0].isString()) path = a[0].asString(); + std::vector out; + try { + for (const auto& entry : fs::directory_iterator(path)) { + out.push_back(Value(entry.path().filename().string())); + } + } catch (...) {} + return Value(out); + }); + m.fn("mkdir", [](std::vector a, int, int) -> Value { + if (a.size() != 1 || !a[0].isString()) return Value(false); + try { return Value(fs::create_directory(a[0].asString())); } catch (...) { return Value(false); } + }); + m.fn("rmdir", [](std::vector a, int, int) -> Value { + if (a.size() != 1 || !a[0].isString()) return Value(false); + try { return Value(fs::remove(a[0].asString())); } catch (...) { return Value(false); } + }); + m.fn("remove", [](std::vector a, int, int) -> Value { + if (a.size() != 1 || !a[0].isString()) return Value(false); + try { return Value(fs::remove(a[0].asString())); } catch (...) { return Value(false); } + }); + m.fn("exists", [](std::vector a, int, int) -> Value { + if (a.size() != 1 || !a[0].isString()) return Value(false); + try { return Value(fs::exists(a[0].asString())); } catch (...) { return Value(false); } + }); + m.fn("isfile", [](std::vector a, int, int) -> Value { + if (a.size() != 1 || !a[0].isString()) return Value(false); + try { return Value(fs::is_regular_file(a[0].asString())); } catch (...) { return Value(false); } + }); + m.fn("isdir", [](std::vector a, int, int) -> Value { + if (a.size() != 1 || !a[0].isString()) return Value(false); + try { return Value(fs::is_directory(a[0].asString())); } catch (...) { return Value(false); } + }); + m.fn("rename", [](std::vector a, int, int) -> Value { + if (a.size() != 2 || !a[0].isString() || !a[1].isString()) return Value(false); + try { fs::rename(a[0].asString(), a[1].asString()); return Value(true); } catch (...) { return Value(false); } + }); + + // Separators + m.fn("sep", [](std::vector, int, int) -> Value { +#if defined(_WIN32) + return Value(std::string("\\")); +#else + return Value(std::string("/")); +#endif + }); + m.fn("pathsep", [](std::vector, int, int) -> Value { +#if defined(_WIN32) + return Value(std::string(";")); +#else + return Value(std::string(":")); +#endif + }); + m.fn("linesep", [](std::vector, int, int) -> Value { +#if defined(_WIN32) + return Value(std::string("\r\n")); +#else + return Value(std::string("\n")); +#endif + }); + }); +} + + diff --git a/src/sources/builtinModules/path.cpp b/src/sources/builtinModules/path.cpp new file mode 100644 index 0000000..11f03ce --- /dev/null +++ b/src/sources/builtinModules/path.cpp @@ -0,0 +1,49 @@ +#include "path_module.h" +#include "Interpreter.h" +#include + +namespace fs = std::filesystem; + +static std::string join_impl(const std::vector& parts){ + if (parts.empty()) return std::string(); + fs::path p; + for (const auto& v : parts) if (v.isString()) p /= v.asString(); + return p.generic_string(); +} + +void registerPathModule(Interpreter& interpreter) { + interpreter.registerModule("path", [](Interpreter::ModuleBuilder& m) { + m.fn("join", [](std::vector a, int, int) -> Value { + return Value(join_impl(a)); + }); + m.fn("dirname", [](std::vector a, int, int) -> Value { + if (a.size()!=1 || !a[0].isString()) return NONE_VALUE; + return Value(fs::path(a[0].asString()).parent_path().generic_string()); + }); + m.fn("basename", [](std::vector a, int, int) -> Value { + if (a.size()!=1 || !a[0].isString()) return NONE_VALUE; + return Value(fs::path(a[0].asString()).filename().generic_string()); + }); + m.fn("splitext", [](std::vector a, int, int) -> Value { + if (a.size()!=1 || !a[0].isString()) return NONE_VALUE; + fs::path p(a[0].asString()); + return Value(std::vector{ Value(p.replace_extension("").generic_string()), Value(p.extension().generic_string()) }); + }); + m.fn("normalize", [](std::vector a, int, int) -> Value { + if (a.size()!=1 || !a[0].isString()) return NONE_VALUE; + return Value(fs::path(a[0].asString()).lexically_normal().generic_string()); + }); + m.fn("isabs", [](std::vector a, int, int) -> Value { + if (a.size()!=1 || !a[0].isString()) return Value(false); + return Value(fs::path(a[0].asString()).is_absolute()); + }); + m.fn("relpath", [](std::vector a, int, int) -> Value { + if (a.size()<1 || a.size()>2 || !a[0].isString() || (a.size()==2 && !a[1].isString())) return NONE_VALUE; + fs::path target(a[0].asString()); + fs::path base = (a.size()==2)? fs::path(a[1].asString()) : fs::current_path(); + return Value(fs::relative(target, base).generic_string()); + }); + }); +} + + diff --git a/src/sources/builtinModules/rand.cpp b/src/sources/builtinModules/rand.cpp new file mode 100644 index 0000000..cd7458a --- /dev/null +++ b/src/sources/builtinModules/rand.cpp @@ -0,0 +1,35 @@ +#include "rand.h" +#include "Interpreter.h" +#include + +void registerRandModule(Interpreter& interpreter) { + interpreter.registerModule("rand", [](Interpreter::ModuleBuilder& m) { + static std::mt19937_64 rng{std::random_device{}()}; + m.fn("seed", [](std::vector a, int, int) -> Value { + if (a.size() == 1 && a[0].isNumber()) { + rng.seed(static_cast(a[0].asNumber())); + } + return NONE_VALUE; + }); + m.fn("random", [](std::vector, int, int) -> Value { + std::uniform_real_distribution dist(0.0, 1.0); + return Value(dist(rng)); + }); + m.fn("randint", [](std::vector a, int, int) -> Value { + if (a.size() != 2 || !a[0].isNumber() || !a[1].isNumber()) return NONE_VALUE; + long long lo = static_cast(a[0].asNumber()); + long long hi = static_cast(a[1].asNumber()); + if (hi < lo) std::swap(lo, hi); + std::uniform_int_distribution dist(lo, hi); + return Value(static_cast(dist(rng))); + }); + m.fn("choice", [](std::vector a, int, int) -> Value { + if (a.size() != 1 || !a[0].isArray() || a[0].asArray().empty()) return NONE_VALUE; + const auto& arr = a[0].asArray(); + std::uniform_int_distribution dist(0, arr.size() - 1); + return arr[dist(rng)]; + }); + }); +} + + diff --git a/src/sources/builtinModules/register.cpp b/src/sources/builtinModules/register.cpp index 9bb6220..33ed7f0 100644 --- a/src/sources/builtinModules/register.cpp +++ b/src/sources/builtinModules/register.cpp @@ -1,8 +1,25 @@ #include "register.h" #include "sys.h" +#include "os.h" +#include "eval.h" +#include "io.h" +#include "time_module.h" +#include "rand.h" +#include "math_module.h" +#include "path_module.h" +#include "base64_module.h" void registerAllBuiltinModules(Interpreter& interpreter) { registerSysModule(interpreter); + registerOsModule(interpreter); + registerEvalModule(interpreter); + registerIoModule(interpreter); + registerTimeModule(interpreter); + registerRandModule(interpreter); + registerMathModule(interpreter); + registerPathModule(interpreter); + registerBase64Module(interpreter); + // registerJsonModule(interpreter); // deferred pending extensive testing } diff --git a/src/sources/builtinModules/sys.cpp b/src/sources/builtinModules/sys.cpp index 620badc..b0f1c48 100644 --- a/src/sources/builtinModules/sys.cpp +++ b/src/sources/builtinModules/sys.cpp @@ -6,140 +6,43 @@ #include #include #include -#if defined(__APPLE__) -#define DYLD_BOOL DYLD_BOOL_IGNORED -#include -#undef DYLD_BOOL -#endif +#include void registerSysModule(Interpreter& interpreter) { interpreter.registerModule("sys", [](Interpreter::ModuleBuilder& m) { Interpreter& I = m.interpreterRef; - m.fn("memoryUsage", [&I](std::vector, int l, int c) -> Value { - try { - Value fn = I.getEnvironment()->get(Token{IDENTIFIER, "memoryUsage", l, c}); - if (fn.isBuiltinFunction()) return fn.asBuiltinFunction()->func({}, l, c); - } catch (...) {} - return NONE_VALUE; - }); - m.fn("exit", [](std::vector a, int, int) -> Value { - int code = 0; if (!a.empty() && a[0].isNumber()) code = static_cast(a[0].asNumber()); - std::exit(code); - return NONE_VALUE; - }); - m.fn("cwd", [](std::vector, int, int) -> Value { - char buf[PATH_MAX]; - if (getcwd(buf, sizeof(buf))) { return Value(std::string(buf)); } - return NONE_VALUE; - }); m.fn("platform", [](std::vector, int, int) -> Value { #if defined(_WIN32) - return Value(std::string("windows")); + return Value(std::string("win32")); #elif defined(__APPLE__) - return Value(std::string("macos")); + return Value(std::string("darwin")); #elif defined(__linux__) return Value(std::string("linux")); #else return Value(std::string("unknown")); #endif }); - m.fn("getenv", [](std::vector a, int, int) -> Value { - if (a.size() != 1 || !a[0].isString()) return NONE_VALUE; - const char* v = std::getenv(a[0].asString().c_str()); - if (!v) return NONE_VALUE; - return Value(std::string(v)); - }); - m.fn("pid", [](std::vector, int, int) -> Value { - return Value(static_cast(getpid())); - }); - m.fn("chdir", [](std::vector a, int, int) -> Value { - if (a.size() != 1 || !a[0].isString()) return Value(false); - const std::string& p = a[0].asString(); - int rc = chdir(p.c_str()); - return Value(rc == 0); - }); - m.fn("homeDir", [](std::vector, int, int) -> Value { - const char* h = std::getenv("HOME"); - if (h) return Value(std::string(h)); - const char* up = std::getenv("USERPROFILE"); - if (up) return Value(std::string(up)); - return NONE_VALUE; - }); - m.fn("tempDir", [](std::vector, int, int) -> Value { - const char* t = std::getenv("TMPDIR"); - if (!t) t = std::getenv("TMP"); - if (!t) t = std::getenv("TEMP"); - if (t) return Value(std::string(t)); - return Value(std::string("/tmp")); - }); - m.fn("pathSep", [](std::vector, int, int) -> Value { -#if defined(_WIN32) - return Value(std::string(";")); -#else - return Value(std::string(":")); -#endif - }); - m.fn("dirSep", [](std::vector, int, int) -> Value { -#if defined(_WIN32) - return Value(std::string("\\")); -#else - return Value(std::string("/")); -#endif - }); - m.fn("execPath", [](std::vector, int, int) -> Value { -#if defined(__APPLE__) - uint32_t sz = 0; - _NSGetExecutablePath(nullptr, &sz); - std::string buf(sz, '\0'); - if (_NSGetExecutablePath(buf.data(), &sz) == 0) { - buf.resize(std::strlen(buf.c_str())); - return Value(buf); - } - return NONE_VALUE; -#elif defined(__linux__) - char path[PATH_MAX]; - ssize_t len = readlink("/proc/self/exe", path, sizeof(path) - 1); - if (len > 0) { path[len] = '\0'; return Value(std::string(path)); } - return NONE_VALUE; -#else - return NONE_VALUE; -#endif - }); - m.fn("env", [](std::vector, int, int) -> Value { - std::unordered_map out; -#if defined(__APPLE__) || defined(__linux__) - extern char **environ; - if (environ) { - for (char **e = environ; *e != nullptr; ++e) { - const char* kv = *e; - const char* eq = std::strchr(kv, '='); - if (!eq) continue; - std::string key(kv, eq - kv); - std::string val(eq + 1); - out[key] = Value(val); - } - } -#endif + m.fn("version", [](std::vector, int, int) -> Value { return Value(std::string("0.0.3")); }); + // argv(): array of strings + m.fn("argv", [&I](std::vector, int, int) -> Value { + std::vector out; + for (const auto& s : I.getArgv()) out.push_back(Value(s)); return Value(out); }); - m.fn("setenv", [](std::vector a, int, int) -> Value { - if (a.size() != 2 || !a[0].isString() || !a[1].isString()) return Value(false); -#if defined(__APPLE__) || defined(__linux__) - int rc = ::setenv(a[0].asString().c_str(), a[1].asString().c_str(), 1); - return Value(rc == 0); -#else - return Value(false); -#endif + // executable(): absolute path to the running binary (host-provided) + m.fn("executable", [&I](std::vector, int, int) -> Value { return Value(I.getExecutablePath()); }); + // modules(): read-only snapshot of module cache + m.fn("modules", [&I](std::vector, int, int) -> Value { + Value dictVal = Value(std::unordered_map{}); + auto snapshot = I.getModuleCacheSnapshot(); + return Value(snapshot); }); - m.fn("unsetenv", [](std::vector a, int, int) -> Value { - if (a.size() != 1 || !a[0].isString()) return Value(false); -#if defined(__APPLE__) || defined(__linux__) - int rc = ::unsetenv(a[0].asString().c_str()); - return Value(rc == 0); -#else - return Value(false); -#endif + m.fn("exit", [](std::vector a, int, int) -> Value { + int code = 0; if (!a.empty() && a[0].isNumber()) code = static_cast(a[0].asNumber()); + std::exit(code); + return NONE_VALUE; }); + // env/cwd/pid moved to os; keep sys minimal }); } diff --git a/src/sources/builtinModules/time.cpp b/src/sources/builtinModules/time.cpp new file mode 100644 index 0000000..190354f --- /dev/null +++ b/src/sources/builtinModules/time.cpp @@ -0,0 +1,31 @@ +#include "time_module.h" +#include "Interpreter.h" +#include "Environment.h" +#include +#include + +void registerTimeModule(Interpreter& interpreter) { + interpreter.registerModule("time", [](Interpreter::ModuleBuilder& m) { + m.fn("now", [](std::vector, int, int) -> Value { + using namespace std::chrono; + auto now = system_clock::now().time_since_epoch(); + auto us = duration_cast(now).count(); + return Value(static_cast(us)); + }); + m.fn("monotonic", [](std::vector, int, int) -> Value { + using namespace std::chrono; + auto now = steady_clock::now().time_since_epoch(); + auto us = duration_cast(now).count(); + return Value(static_cast(us)); + }); + m.fn("sleep", [](std::vector a, int, int) -> Value { + if (a.size() != 1 || !a[0].isNumber()) return NONE_VALUE; + double seconds = a[0].asNumber(); + if (seconds < 0) return NONE_VALUE; + std::this_thread::sleep_for(std::chrono::milliseconds(static_cast(seconds * 1000))); + return NONE_VALUE; + }); + }); +} + + diff --git a/src/sources/cli/main.cpp b/src/sources/cli/main.cpp index fb2654c..c81d47b 100644 --- a/src/sources/cli/main.cpp +++ b/src/sources/cli/main.cpp @@ -6,20 +6,23 @@ int main(int argc, char* argv[]){ Bob bobLang; - // Example: host can register a custom module via Bob bridge (applied on first use) - bobLang.registerModule("demo", [](ModuleRegistry::ModuleBuilder& m) { - m.fn("hello", [](std::vector a, int, int) -> Value { - std::string who = (a.size() >= 1 && a[0].isString()) ? a[0].asString() : std::string("world"); - return Value(std::string("hello ") + who); - }); - m.val("meaning", Value(42.0)); - }); - //bobLang.setBuiltinModuleDenyList({"sys"}); + // Enable open preset (all builtins, file imports allowed) + bobLang.setSafetyPreset("open"); + if(argc > 1) { + // Seed argv/executable for sys module + std::vector args; for (int i = 2; i < argc; ++i) args.emplace_back(argv[i]); + bobLang.registerModule("__configure_sys_argv__", [args, execPath = std::string(argv[0])](ModuleRegistry::ModuleBuilder& m){ + m.interpreterRef.setArgv(args, execPath); + }); bobLang.runFile(argv[1]); } else { - // For REPL, use interactive mode + // For REPL, use interactive mode and seed empty argv + std::vector args; + bobLang.registerModule("__configure_sys_argv__", [args, execPath = std::string(argv[0])](ModuleRegistry::ModuleBuilder& m){ + m.interpreterRef.setArgv(args, execPath); + }); bobLang.runPrompt(); } diff --git a/src/sources/parsing/Parser.cpp b/src/sources/parsing/Parser.cpp index 01a3c95..988a7aa 100644 --- a/src/sources/parsing/Parser.cpp +++ b/src/sources/parsing/Parser.cpp @@ -648,6 +648,11 @@ std::shared_ptr Parser::fromImportStatement() { 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."); + // Support star-import: from module import *; + if (match({STAR})) { + consume(SEMICOLON, "Expected ';' after from-import statement."); + return msptr(FromImportStmt)(fromTok, mod, true); + } std::vector items; do { Token name = consume(IDENTIFIER, "Expected name to import."); diff --git a/src/sources/runtime/Executor.cpp b/src/sources/runtime/Executor.cpp index c0d72ef..9143587 100644 --- a/src/sources/runtime/Executor.cpp +++ b/src/sources/runtime/Executor.cpp @@ -336,6 +336,20 @@ void Executor::visitImportStmt(const std::shared_ptr& statement, Exe void Executor::visitFromImportStmt(const std::shared_ptr& statement, ExecutionContext* context) { std::string spec = statement->moduleName.lexeme; // already STRING with .bob from parser if name-based + // Star-import case + if (statement->importAll) { + // Import the module and bind all public exports into current environment + Value mod = interpreter->importModule(spec, statement->fromToken.line, statement->fromToken.column); + const std::unordered_map* src = nullptr; + if (mod.isModule()) src = mod.asModule()->exports.get(); else if (mod.isDict()) src = &mod.asDict(); + if (!src) { throw std::runtime_error("from-import * on non-module"); } + for (const auto& kv : *src) { + const std::string& name = kv.first; + if (!name.empty() && name[0] == '_') continue; // skip private + interpreter->getEnvironment()->define(name, kv.second); + } + return; + } // Build item list name->alias std::vector> items; for (const auto& it : statement->items) { diff --git a/src/sources/runtime/Interpreter.cpp b/src/sources/runtime/Interpreter.cpp index fd2263d..2fc44fe 100644 --- a/src/sources/runtime/Interpreter.cpp +++ b/src/sources/runtime/Interpreter.cpp @@ -124,13 +124,9 @@ Value Interpreter::importModule(const std::string& spec, int line, int column) { // 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 (errorReporter && !errorReporter->getCurrentFileName().empty()) { + std::filesystem::path p(errorReporter->getCurrentFileName()); + baseDir = p.has_parent_path() ? p.parent_path().string() : baseDir; } if (looksPath) { if (!allowFileImports) { @@ -222,7 +218,6 @@ Value Interpreter::importModule(const std::string& spec, int line, int column) { // Restore env and reporter setEnvironment(saved); if (errorReporter) errorReporter->popSource(); - return moduleVal; } diff --git a/src/sources/stdlib/BobStdLib.cpp b/src/sources/stdlib/BobStdLib.cpp index 2a2cf43..210f981 100644 --- a/src/sources/stdlib/BobStdLib.cpp +++ b/src/sources/stdlib/BobStdLib.cpp @@ -119,31 +119,7 @@ void BobStdLib::addToEnvironment(std::shared_ptr env, Interpreter& // Store the shared_ptr in the interpreter to keep it alive interpreter.addBuiltinFunction(assertFunc); - // Create a built-in time function (returns strictly increasing microseconds) - auto timeFunc = std::make_shared("time", - [errorReporter](std::vector args, int line, int column) -> Value { - if (args.size() != 0) { - if (errorReporter) { - errorReporter->reportError(line, column, "StdLib Error", - "Expected 0 arguments but got " + std::to_string(args.size()) + ".", "", true); - } - throw std::runtime_error("Expected 0 arguments but got " + std::to_string(args.size()) + "."); - } - - static long long lastReturnedMicros = 0; - auto now = std::chrono::high_resolution_clock::now(); - auto duration = now.time_since_epoch(); - long long microseconds = std::chrono::duration_cast(duration).count(); - if (microseconds <= lastReturnedMicros) { - microseconds = lastReturnedMicros + 1; - } - lastReturnedMicros = microseconds; - return Value(static_cast(microseconds)); - }); - env->define("time", Value(timeFunc)); - - // Store the shared_ptr in the interpreter to keep it alive - interpreter.addBuiltinFunction(timeFunc); + // time-related globals moved into builtin time module // Create a built-in input function auto inputFunc = std::make_shared("input", @@ -333,44 +309,49 @@ void BobStdLib::addToEnvironment(std::shared_ptr env, Interpreter& // Store the shared_ptr in the interpreter to keep it alive interpreter.addBuiltinFunction(exitFunc); - // Create a built-in sleep function for animations and timing - auto sleepFunc = std::make_shared("sleep", - [errorReporter](std::vector args, int line, int column) -> Value { - if (args.size() != 1) { - if (errorReporter) { - errorReporter->reportError(line, column, "StdLib Error", - "Expected 1 argument but got " + std::to_string(args.size()) + ".", "", true); + // sleep moved into builtin time module + + // Introspection: dir(obj) and functions(obj) + auto dirFunc = std::make_shared("dir", + [](std::vector args, int, int) -> Value { + if (args.size() != 1) return Value(std::vector{}); + Value obj = args[0]; + std::vector out; + if (obj.isModule()) { + auto* mod = obj.asModule(); + if (mod && mod->exports) { + for (const auto& kv : *mod->exports) out.push_back(Value(kv.first)); } - throw std::runtime_error("Expected 1 argument but got " + std::to_string(args.size()) + "."); + } else if (obj.isDict()) { + const auto& d = obj.asDict(); + for (const auto& kv : d) out.push_back(Value(kv.first)); } - - if (!args[0].isNumber()) { - if (errorReporter) { - errorReporter->reportError(line, column, "StdLib Error", - "sleep() argument must be a number", "", true); - } - throw std::runtime_error("sleep() argument must be a number"); - } - - double seconds = args[0].asNumber(); - if (seconds < 0) { - if (errorReporter) { - errorReporter->reportError(line, column, "StdLib Error", - "sleep() argument cannot be negative", "", true); - } - throw std::runtime_error("sleep() argument cannot be negative"); - } - - // Convert to milliseconds and sleep - int milliseconds = static_cast(seconds * 1000); - std::this_thread::sleep_for(std::chrono::milliseconds(milliseconds)); - - return NONE_VALUE; + return Value(out); }); - env->define("sleep", Value(sleepFunc)); - - // Store the shared_ptr in the interpreter to keep it alive - interpreter.addBuiltinFunction(sleepFunc); + env->define("dir", Value(dirFunc)); + interpreter.addBuiltinFunction(dirFunc); + + auto functionsFunc = std::make_shared("functions", + [](std::vector args, int, int) -> Value { + if (args.size() != 1) return Value(std::vector{}); + Value obj = args[0]; + std::vector out; + auto pushIfFn = [&out](const std::pair& kv){ + if (kv.second.isFunction() || kv.second.isBuiltinFunction()) out.push_back(Value(kv.first)); + }; + if (obj.isModule()) { + auto* mod = obj.asModule(); + if (mod && mod->exports) { + for (const auto& kv : *mod->exports) pushIfFn(kv); + } + } else if (obj.isDict()) { + const auto& d = obj.asDict(); + for (const auto& kv : d) pushIfFn(kv); + } + return Value(out); + }); + env->define("functions", Value(functionsFunc)); + interpreter.addBuiltinFunction(functionsFunc); // Create a built-in random function auto randomFunc = std::make_shared("random", @@ -397,231 +378,11 @@ void BobStdLib::addToEnvironment(std::shared_ptr env, Interpreter& // Store the shared_ptr in the interpreter to keep it alive interpreter.addBuiltinFunction(randomFunc); - // Create a built-in eval function (like Python's eval) - auto evalFunc = std::make_shared("eval", - [&interpreter, errorReporter](std::vector args, int line, int column) -> Value { - if (args.size() != 1) { - if (errorReporter) { - errorReporter->reportError(line, column, "Invalid Arguments", - "eval expects exactly 1 argument (string)", "eval"); - } - throw std::runtime_error("eval expects exactly 1 argument"); - } - - if (!args[0].isString()) { - if (errorReporter) { - errorReporter->reportError(line, column, "Invalid Type", - "eval argument must be a string", "eval"); - } - throw std::runtime_error("eval argument must be a string"); - } - - std::string code = args[0].asString(); - std::string evalName = ""; - - try { - // Push eval source for correct error context - if (errorReporter) { - errorReporter->pushSource(code, evalName); - } - // Create a new lexer for the code string - Lexer lexer; - lexer.setErrorReporter(errorReporter); - std::vector tokens = lexer.Tokenize(code); - - // Create a new parser - Parser parser(tokens); - parser.setErrorReporter(errorReporter); - std::vector> statements = parser.parse(); - - // Execute the statements in the current environment - // Note: This runs in the current scope, so variables are shared - interpreter.interpret(statements); - - // For now, return NONE_VALUE since we don't have a way to get the last expression value - // In a more sophisticated implementation, we'd track the last expression result - return NONE_VALUE; - - } catch (const std::exception& e) { - if (errorReporter) { - errorReporter->reportError(line, column, "Eval Error", - "Failed to evaluate code: " + std::string(e.what()), code); - } - throw std::runtime_error("eval failed: " + std::string(e.what())); - } catch (...) { - if (errorReporter) { - errorReporter->popSource(); - } - throw; - } - if (errorReporter) { - errorReporter->popSource(); - } - }); - env->define("eval", Value(evalFunc)); - - // Store the shared_ptr in the interpreter to keep it alive - interpreter.addBuiltinFunction(evalFunc); + // (eval and evalFile moved to eval module) - // Create a built-in readFile function - auto readFileFunc = std::make_shared("readFile", - [errorReporter](std::vector args, int line, int column) -> Value { - if (args.size() != 1) { - if (errorReporter) { - errorReporter->reportError(line, column, "StdLib Error", - "Expected 1 argument but got " + std::to_string(args.size()) + ".", "", true); - } - throw std::runtime_error("Expected 1 argument but got " + std::to_string(args.size()) + "."); - } - - if (!args[0].isString()) { - if (errorReporter) { - errorReporter->reportError(line, column, "StdLib Error", - "readFile() argument must be a string", "", true); - } - throw std::runtime_error("readFile() argument must be a string"); - } - - std::string filename = args[0].asString(); - std::ifstream file(filename); - - if (!file.is_open()) { - if (errorReporter) { - errorReporter->reportError(line, column, "StdLib Error", - "Could not open file: " + filename, "", true); - } - throw std::runtime_error("Could not open file: " + filename); - } - - std::stringstream buffer; - buffer << file.rdbuf(); - file.close(); - - return Value(buffer.str()); - }); - env->define("readFile", Value(readFileFunc)); - interpreter.addBuiltinFunction(readFileFunc); - - // Create a built-in writeFile function - auto writeFileFunc = std::make_shared("writeFile", - [errorReporter](std::vector args, int line, int column) -> Value { - if (args.size() != 2) { - if (errorReporter) { - errorReporter->reportError(line, column, "StdLib Error", - "Expected 2 arguments but got " + std::to_string(args.size()) + ".", "", true); - } - throw std::runtime_error("Expected 2 arguments but got " + std::to_string(args.size()) + "."); - } - - if (!args[0].isString()) { - if (errorReporter) { - errorReporter->reportError(line, column, "StdLib Error", - "First argument to writeFile() must be a string", "", true); - } - throw std::runtime_error("First argument to writeFile() must be a string"); - } - - if (!args[1].isString()) { - if (errorReporter) { - errorReporter->reportError(line, column, "StdLib Error", - "Second argument to writeFile() must be a string", "", true); - } - throw std::runtime_error("Second argument to writeFile() must be a string"); - } - - std::string filename = args[0].asString(); - std::string content = args[1].asString(); - - std::ofstream file(filename); - if (!file.is_open()) { - if (errorReporter) { - errorReporter->reportError(line, column, "StdLib Error", - "Could not create file: " + filename, "", true); - } - throw std::runtime_error("Could not create file: " + filename); - } - - file << content; - file.close(); - - return NONE_VALUE; - }); - env->define("writeFile", Value(writeFileFunc)); - interpreter.addBuiltinFunction(writeFileFunc); - - // Create a built-in readLines function - auto readLinesFunc = std::make_shared("readLines", - [errorReporter](std::vector args, int line, int column) -> Value { - if (args.size() != 1) { - if (errorReporter) { - errorReporter->reportError(line, column, "StdLib Error", - "Expected 1 argument but got " + std::to_string(args.size()) + ".", "", true); - } - throw std::runtime_error("Expected 1 argument but got " + std::to_string(args.size()) + "."); - } - - if (!args[0].isString()) { - if (errorReporter) { - errorReporter->reportError(line, column, "StdLib Error", - "readLines() argument must be a string", "", true); - } - throw std::runtime_error("readLines() argument must be a string"); - } - - std::string filename = args[0].asString(); - std::ifstream file(filename); - - if (!file.is_open()) { - if (errorReporter) { - errorReporter->reportError(line, column, "StdLib Error", - "Could not open file: " + filename, "", true); - } - throw std::runtime_error("Could not open file: " + filename); - } - - std::vector lines; - std::string line_content; - - while (std::getline(file, line_content)) { - lines.push_back(Value(line_content)); - } - - file.close(); - return Value(lines); - }); - env->define("readLines", Value(readLinesFunc)); - interpreter.addBuiltinFunction(readLinesFunc); - - // Create a built-in fileExists function - auto fileExistsFunc = std::make_shared("fileExists", - [errorReporter](std::vector args, int line, int column) -> Value { - if (args.size() != 1) { - if (errorReporter) { - errorReporter->reportError(line, column, "StdLib Error", - "Expected 1 argument but got " + std::to_string(args.size()) + ".", "", true); - } - throw std::runtime_error("Expected 1 argument but got " + std::to_string(args.size()) + "."); - } - - if (!args[0].isString()) { - if (errorReporter) { - errorReporter->reportError(line, column, "StdLib Error", - "fileExists() argument must be a string", "", true); - } - throw std::runtime_error("fileExists() argument must be a string"); - } - - std::string filename = args[0].asString(); - std::ifstream file(filename); - bool exists = file.good(); - file.close(); - - return Value(exists); - }); - env->define("fileExists", Value(fileExistsFunc)); - interpreter.addBuiltinFunction(fileExistsFunc); + // (file I/O moved to io module) // Create a built-in memoryUsage function (platform-specific, best effort) auto memoryUsageFunc = std::make_shared("memoryUsage", diff --git a/test_bob_language.bob b/test_bob_language.bob index 3da81d8..d7adea4 100644 --- a/test_bob_language.bob +++ b/test_bob_language.bob @@ -3274,54 +3274,57 @@ print(" * Error reporting in both modes"); // Additional Tests: Classes and Extensions print("\n--- Additional Tests: Classes and Extensions ---"); var path1 = fileExists("tests/test_method_calls.bob") ? "tests/test_method_calls.bob" : "../tests/test_method_calls.bob"; -eval(readFile(path1)); +evalFile(path1); var path2 = fileExists("tests/test_class_basic.bob") ? "tests/test_class_basic.bob" : "../tests/test_class_basic.bob"; -eval(readFile(path2)); +evalFile(path2); var path3 = fileExists("tests/test_class_with_this.bob") ? "tests/test_class_with_this.bob" : "../tests/test_class_with_this.bob"; -eval(readFile(path3)); +evalFile(path3); var path4 = fileExists("tests/test_class_init.bob") ? "tests/test_class_init.bob" : "../tests/test_class_init.bob"; -eval(readFile(path4)); +evalFile(path4); var path5 = fileExists("tests/test_class_extension_user.bob") ? "tests/test_class_extension_user.bob" : "../tests/test_class_extension_user.bob"; -eval(readFile(path5)); +evalFile(path5); var path6 = fileExists("tests/test_extension_methods.bob") ? "tests/test_extension_methods.bob" : "../tests/test_extension_methods.bob"; -eval(readFile(path6)); +evalFile(path6); var path7 = fileExists("tests/test_class_inheritance.bob") ? "tests/test_class_inheritance.bob" : "../tests/test_class_inheritance.bob"; -eval(readFile(path7)); +evalFile(path7); var path8 = fileExists("tests/test_classes_comprehensive.bob") ? "tests/test_classes_comprehensive.bob" : "../tests/test_classes_comprehensive.bob"; -eval(readFile(path8)); +evalFile(path8); var path9 = fileExists("tests/test_class_super.bob") ? "tests/test_class_super.bob" : "../tests/test_class_super.bob"; -eval(readFile(path9)); +evalFile(path9); var path10 = fileExists("tests/test_classes_extensive.bob") ? "tests/test_classes_extensive.bob" : "../tests/test_classes_extensive.bob"; -eval(readFile(path10)); +evalFile(path10); var path11 = fileExists("tests/test_class_edge_cases.bob") ? "tests/test_class_edge_cases.bob" : "../tests/test_class_edge_cases.bob"; -eval(readFile(path11)); +evalFile(path11); var path12 = fileExists("tests/test_polymorphism.bob") ? "tests/test_polymorphism.bob" : "../tests/test_polymorphism.bob"; -eval(readFile(path12)); +evalFile(path12); var path13 = fileExists("tests/test_polymorphism_practical.bob") ? "tests/test_polymorphism_practical.bob" : "../tests/test_polymorphism_practical.bob"; -eval(readFile(path13)); +evalFile(path13); var path14 = fileExists("tests/test_builtin_methods_style.bob") ? "tests/test_builtin_methods_style.bob" : "../tests/test_builtin_methods_style.bob"; -eval(readFile(path14)); +evalFile(path14); var path15 = fileExists("tests/test_try_catch.bob") ? "tests/test_try_catch.bob" : "../tests/test_try_catch.bob"; -eval(readFile(path15)); +evalFile(path15); var path15a = fileExists("tests/test_try_catch_runtime.bob") ? "tests/test_try_catch_runtime.bob" : "../tests/test_try_catch_runtime.bob"; -eval(readFile(path15a)); +evalFile(path15a); var path15b = fileExists("tests/test_try_catch_extensive.bob") ? "tests/test_try_catch_extensive.bob" : "../tests/test_try_catch_extensive.bob"; -eval(readFile(path15b)); +evalFile(path15b); var path15c = fileExists("tests/test_try_catch_edge_cases2.bob") ? "tests/test_try_catch_edge_cases2.bob" : "../tests/test_try_catch_edge_cases2.bob"; -eval(readFile(path15c)); +evalFile(path15c); var path15d = fileExists("tests/test_try_catch_cross_function.bob") ? "tests/test_try_catch_cross_function.bob" : "../tests/test_try_catch_cross_function.bob"; -eval(readFile(path15d)); +evalFile(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)); +evalFile(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)); +evalFile(pathMods); var pathModsB = fileExists("tests/test_imports_builtin.bob") ? "tests/test_imports_builtin.bob" : "../tests/test_imports_builtin.bob"; -eval(readFile(pathModsB)); +evalFile(pathModsB); + +var pathOs = fileExists("tests/test_os_basic.bob") ? "tests/test_os_basic.bob" : "../tests/test_os_basic.bob"; +evalFile(pathOs); print("\nAll tests passed."); print("Test suite complete."); \ No newline at end of file diff --git a/tests.bob b/tests.bob index de36e24..112fba2 100644 --- a/tests.bob +++ b/tests.bob @@ -89,18 +89,8 @@ // print("done"); -from bobby import A as fart; -import bobby; -var a = fart(); -var b = bobby.A(); -a.test(); -b.test(); -print(bobby); - - - - - - +// Modules: basic imports suite +var pathMods = fileExists("tests/test_imports_basic.bob") ? "tests/test_imports_basic.bob" : "../tests/test_imports_basic.bob"; +evalFile(pathMods); print("done"); \ No newline at end of file diff --git a/tests/test_imports_basic.bob b/tests/test_imports_basic.bob index f05c644..a098653 100644 --- a/tests/test_imports_basic.bob +++ b/tests/test_imports_basic.bob @@ -28,7 +28,7 @@ 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")); +evalFile("tests/import_user_of_mod_hello.bob"); // Immutability: cannot reassign module binding var immFail = false; diff --git a/tests/test_imports_builtin.bob b/tests/test_imports_builtin.bob index e782140..15a50c6 100644 --- a/tests/test_imports_builtin.bob +++ b/tests/test_imports_builtin.bob @@ -3,39 +3,18 @@ print("\n--- Test: builtin imports ---"); import sys; assert(type(sys) == "module", "sys is module"); -from sys import memoryUsage as mem; +from sys import memoryUsage as mem, platform, version; 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 ver = version(); +assert(type(ver) == "string", "version returns string"); -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"); +// OS functions are in os module now +import os; +var dir = os.getcwd(); +assert(type(dir) == "string" || type(dir) == "none", "getcwd returns string/none"); print("builtin imports: PASS"); diff --git a/tests/test_os_basic.bob b/tests/test_os_basic.bob new file mode 100644 index 0000000..4a833f2 --- /dev/null +++ b/tests/test_os_basic.bob @@ -0,0 +1,29 @@ +print("\n--- Test: os basic ---"); + +import os; + +var cwd = os.getcwd(); +assert(type(cwd) == "string", "getcwd returns string"); + +var pid = os.getpid(); +assert(type(pid) == "number", "getpid returns number"); + +var nm = os.name(); +assert(type(nm) == "string", "name returns string"); + +var sep = os.sep(); +var psep = os.pathsep(); +var lsep = os.linesep(); +assert(type(sep) == "string" && type(psep) == "string" && type(lsep) == "string", "separators are strings"); + +// listdir should return array +var listing = os.listdir("."); +assert(type(listing) == "array", "listdir returns array"); + +// exists/isdir should be true for current dir +assert(os.exists(".") == true, "exists(.) is true"); +assert(os.isdir(".") == true, "isdir(.) is true"); + +print("os basic: PASS"); + +