Started fleshing out built in modules.

added policy templates for module safety
This commit is contained in:
Bobby Lucero 2025-08-12 03:26:44 -04:00
parent 8cdccae214
commit 7f7c6e438d
34 changed files with 873 additions and 488 deletions

View File

@ -45,11 +45,11 @@ elseif(MSVC)
endif() endif()
# Collect source files # Collect source files
file(GLOB_RECURSE BOB_RUNTIME_SOURCES "src/sources/runtime/*.cpp") file(GLOB_RECURSE BOB_RUNTIME_SOURCES CONFIGURE_DEPENDS "src/sources/runtime/*.cpp")
file(GLOB_RECURSE BOB_PARSING_SOURCES "src/sources/parsing/*.cpp") file(GLOB_RECURSE BOB_PARSING_SOURCES CONFIGURE_DEPENDS "src/sources/parsing/*.cpp")
file(GLOB_RECURSE BOB_STDLIB_SOURCES "src/sources/stdlib/*.cpp") file(GLOB_RECURSE BOB_STDLIB_SOURCES CONFIGURE_DEPENDS "src/sources/stdlib/*.cpp")
file(GLOB_RECURSE BOB_BUILTIN_SOURCES "src/sources/builtinModules/*.cpp") file(GLOB_RECURSE BOB_BUILTIN_SOURCES CONFIGURE_DEPENDS "src/sources/builtinModules/*.cpp")
file(GLOB_RECURSE BOB_CLI_SOURCES "src/sources/cli/*.cpp") file(GLOB_RECURSE BOB_CLI_SOURCES CONFIGURE_DEPENDS "src/sources/cli/*.cpp")
# All source files # All source files
set(BOB_ALL_SOURCES set(BOB_ALL_SOURCES

View File

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

View File

@ -0,0 +1,8 @@
#pragma once
class Interpreter;
// Register the builtin 'eval' module providing eval(code) and evalFile(path)
void registerEvalModule(Interpreter& interpreter);

View File

@ -0,0 +1,8 @@
#pragma once
class Interpreter;
// Register the builtin 'io' module (file I/O and input)
void registerIoModule(Interpreter& interpreter);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -49,6 +49,70 @@ public:
bool evalFile(const std::string& path); bool evalFile(const std::string& path);
bool evalString(const std::string& code, const std::string& filename = "<eval>"); bool evalString(const std::string& code, const std::string& filename = "<eval>");
// Safety policy helpers (public API)
// Set all safety-related policies at once
void setSafetyPolicy(
bool allowBuiltins,
const std::vector<std::string>& allowList,
const std::vector<std::string>& denyList,
bool allowFileImports,
bool preferFileOverBuiltin,
const std::vector<std::string>& 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<std::string>{
"sys", "time", "rand", "math", "path", "base64"
},
std::vector<std::string>{ /* 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: private:
void ensureInterpreter(bool interactive); void ensureInterpreter(bool interactive);
void applyPendingConfigs() { void applyPendingConfigs() {

View File

@ -291,11 +291,14 @@ struct ImportStmt : Stmt {
// from module import name [as alias], name2 ... // from module import name [as alias], name2 ...
struct FromImportStmt : Stmt { struct FromImportStmt : Stmt {
Token fromToken; // FROM Token fromToken; // FROM
Token moduleName; // IDENTIFIER Token moduleName; // IDENTIFIER or STRING
struct ImportItem { Token name; bool hasAlias; Token alias; }; struct ImportItem { Token name; bool hasAlias; Token alias; };
std::vector<ImportItem> items; std::vector<ImportItem> items;
bool importAll = false; // true for: from module import *;
FromImportStmt(Token kw, Token mod, std::vector<ImportItem> it) FromImportStmt(Token kw, Token mod, std::vector<ImportItem> 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 { void accept(StmtVisitor* visitor, ExecutionContext* context = nullptr) override {
visitor->visitFromImportStmt(std::static_pointer_cast<FromImportStmt>(shared_from_this()), context); visitor->visitFromImportStmt(std::static_pointer_cast<FromImportStmt>(shared_from_this()), context);
} }

View File

@ -172,9 +172,19 @@ public:
void setLastErrorSite(int line, int column) { lastErrorLine = line; lastErrorColumn = column; } void setLastErrorSite(int line, int column) { lastErrorLine = line; lastErrorColumn = column; }
int getLastErrorLine() const { return lastErrorLine; } int getLastErrorLine() const { return lastErrorLine; }
int getLastErrorColumn() const { return lastErrorColumn; } int getLastErrorColumn() const { return lastErrorColumn; }
// Process/host metadata (for sys module)
void setArgv(const std::vector<std::string>& args, const std::string& executablePath) { argvData = args; executableFile = executablePath; }
std::vector<std::string> getArgv() const { return argvData; }
std::string getExecutablePath() const { return executableFile; }
std::unordered_map<std::string, Value> getModuleCacheSnapshot() const { return moduleCache; }
private: private:
Value runTrampoline(Value initialResult); Value runTrampoline(Value initialResult);
// Stored argv/executable for sys module
std::vector<std::string> argvData;
std::string executableFile;
}; };

View File

@ -0,0 +1,41 @@
#include "base64_module.h"
#include "Interpreter.h"
#include <string>
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<int> 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<Value> 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<Value> a, int, int) -> Value {
if (a.size()!=1 || !a[0].isString()) return NONE_VALUE;
return Value(b64decode(a[0].asString()));
});
});
}

View File

@ -0,0 +1,64 @@
#include "eval.h"
#include "Interpreter.h"
#include "ErrorReporter.h"
#include "Lexer.h"
#include "Parser.h"
#include <fstream>
#include <sstream>
void registerEvalModule(Interpreter& interpreter) {
interpreter.registerModule("eval", [](Interpreter::ModuleBuilder& m) {
ErrorReporter* er = m.interpreterRef.getErrorReporter();
m.fn("eval", [er, &I = m.interpreterRef](std::vector<Value> 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 = "<eval>";
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<Value> 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();
});
});
}

View File

@ -0,0 +1,72 @@
#include <fstream>
#include <sstream>
#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<Value> 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<Value> 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<Value> 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<Value> lines; std::string s;
while (std::getline(f, s)) lines.emplace_back(s);
f.close();
return Value(lines);
});
m.fn("exists", [](std::vector<Value> 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
});
}

View File

@ -0,0 +1,83 @@
#include "json.h"
#include "Interpreter.h"
#include <string>
#include <cctype>
// 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<unsigned char>((*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.i<c.s->size() && (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<Value> 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<std::string,Value> 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.size();++i){ if(i) out+=","; out+=stringifyValue(a[i]); } out+="]"; return out;
}
case VAL_DICT: {
const auto& d=v.asDict(); std::string out="{"; bool first=true; for(const auto& kv:d){ if(!first) out+=","; first=false; out+=escapeString(kv.first); out+=":"; out+=stringifyValue(kv.second);} out+="}"; return out;
}
default: return "null";
}
}
}
void registerJsonModule(Interpreter& interpreter) {
interpreter.registerModule("json", [](Interpreter::ModuleBuilder& m) {
m.fn("parse", [](std::vector<Value> 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<Value> a, int, int) -> Value {
if (a.size() != 1) return Value(std::string("null"));
return Value(stringifyValue(a[0]));
});
});
}

View File

@ -0,0 +1,56 @@
#include "math_module.h"
#include "Interpreter.h"
#include <cmath>
static Value unary_math(std::vector<Value> 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<Value> a, int, int)->Value{ return unary_math(a, std::sin); });
m.fn("cos", [](std::vector<Value> a, int, int)->Value{ return unary_math(a, std::cos); });
m.fn("tan", [](std::vector<Value> a, int, int)->Value{ return unary_math(a, std::tan); });
m.fn("asin", [](std::vector<Value> a, int, int)->Value{ return unary_math(a, std::asin); });
m.fn("acos", [](std::vector<Value> a, int, int)->Value{ return unary_math(a, std::acos); });
m.fn("atan", [](std::vector<Value> a, int, int)->Value{ return unary_math(a, std::atan); });
m.fn("sinh", [](std::vector<Value> a, int, int)->Value{ return unary_math(a, std::sinh); });
m.fn("cosh", [](std::vector<Value> a, int, int)->Value{ return unary_math(a, std::cosh); });
m.fn("tanh", [](std::vector<Value> a, int, int)->Value{ return unary_math(a, std::tanh); });
m.fn("exp", [](std::vector<Value> a, int, int)->Value{ return unary_math(a, std::exp); });
m.fn("log", [](std::vector<Value> a, int, int)->Value{ return unary_math(a, std::log); });
m.fn("log10", [](std::vector<Value> a, int, int)->Value{ return unary_math(a, std::log10); });
m.fn("sqrt", [](std::vector<Value> a, int, int)->Value{ return unary_math(a, std::sqrt); });
m.fn("ceil", [](std::vector<Value> a, int, int)->Value{ return unary_math(a, std::ceil); });
m.fn("floor", [](std::vector<Value> a, int, int)->Value{ return unary_math(a, std::floor); });
m.fn("round", [](std::vector<Value> 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<Value> 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<Value> 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<Value> 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.size();++i){ if (a[i].isNumber()) mval = std::min(mval, a[i].asNumber()); }
return Value(mval);
});
m.fn("max", [](std::vector<Value> 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.size();++i){ if (a[i].isNumber()) mval = std::max(mval, a[i].asNumber()); }
return Value(mval);
});
m.val("pi", Value(3.14159265358979323846));
m.val("e", Value(2.71828182845904523536));
});
}

View File

@ -0,0 +1,106 @@
#include "os.h"
#include "Interpreter.h"
#include "Lexer.h"
#include <vector>
#include <string>
#include <filesystem>
#include <unistd.h>
#include <sys/types.h>
#include <dirent.h>
namespace fs = std::filesystem;
void registerOsModule(Interpreter& interpreter) {
interpreter.registerModule("os", [](Interpreter::ModuleBuilder& m) {
// Process
m.fn("getcwd", [](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("chdir", [](std::vector<Value> 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<Value>, int, int) -> Value {
return Value(static_cast<double>(getpid()));
});
m.fn("getppid", [](std::vector<Value>, int, int) -> Value {
return Value(static_cast<double>(getppid()));
});
m.fn("name", [](std::vector<Value>, int, int) -> Value {
#if defined(_WIN32)
return Value(std::string("nt"));
#else
return Value(std::string("posix"));
#endif
});
// Filesystem
m.fn("listdir", [](std::vector<Value> a, int, int) -> Value {
std::string path = ".";
if (!a.empty() && a[0].isString()) path = a[0].asString();
std::vector<Value> 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<Value> 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<Value> 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<Value> 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<Value> 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<Value> 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<Value> 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<Value> 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<Value>, int, int) -> Value {
#if defined(_WIN32)
return Value(std::string("\\"));
#else
return Value(std::string("/"));
#endif
});
m.fn("pathsep", [](std::vector<Value>, int, int) -> Value {
#if defined(_WIN32)
return Value(std::string(";"));
#else
return Value(std::string(":"));
#endif
});
m.fn("linesep", [](std::vector<Value>, int, int) -> Value {
#if defined(_WIN32)
return Value(std::string("\r\n"));
#else
return Value(std::string("\n"));
#endif
});
});
}

View File

@ -0,0 +1,49 @@
#include "path_module.h"
#include "Interpreter.h"
#include <filesystem>
namespace fs = std::filesystem;
static std::string join_impl(const std::vector<Value>& 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<Value> a, int, int) -> Value {
return Value(join_impl(a));
});
m.fn("dirname", [](std::vector<Value> 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<Value> 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<Value> 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>{ Value(p.replace_extension("").generic_string()), Value(p.extension().generic_string()) });
});
m.fn("normalize", [](std::vector<Value> 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<Value> 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<Value> 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());
});
});
}

View File

@ -0,0 +1,35 @@
#include "rand.h"
#include "Interpreter.h"
#include <random>
void registerRandModule(Interpreter& interpreter) {
interpreter.registerModule("rand", [](Interpreter::ModuleBuilder& m) {
static std::mt19937_64 rng{std::random_device{}()};
m.fn("seed", [](std::vector<Value> a, int, int) -> Value {
if (a.size() == 1 && a[0].isNumber()) {
rng.seed(static_cast<uint64_t>(a[0].asNumber()));
}
return NONE_VALUE;
});
m.fn("random", [](std::vector<Value>, int, int) -> Value {
std::uniform_real_distribution<double> dist(0.0, 1.0);
return Value(dist(rng));
});
m.fn("randint", [](std::vector<Value> a, int, int) -> Value {
if (a.size() != 2 || !a[0].isNumber() || !a[1].isNumber()) return NONE_VALUE;
long long lo = static_cast<long long>(a[0].asNumber());
long long hi = static_cast<long long>(a[1].asNumber());
if (hi < lo) std::swap(lo, hi);
std::uniform_int_distribution<long long> dist(lo, hi);
return Value(static_cast<double>(dist(rng)));
});
m.fn("choice", [](std::vector<Value> 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<size_t> dist(0, arr.size() - 1);
return arr[dist(rng)];
});
});
}

View File

@ -1,8 +1,25 @@
#include "register.h" #include "register.h"
#include "sys.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) { void registerAllBuiltinModules(Interpreter& interpreter) {
registerSysModule(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
} }

View File

@ -6,140 +6,43 @@
#include <limits.h> #include <limits.h>
#include <cstdlib> #include <cstdlib>
#include <cstring> #include <cstring>
#if defined(__APPLE__) #include <vector>
#define DYLD_BOOL DYLD_BOOL_IGNORED
#include <mach-o/dyld.h>
#undef DYLD_BOOL
#endif
void registerSysModule(Interpreter& interpreter) { void registerSysModule(Interpreter& interpreter) {
interpreter.registerModule("sys", [](Interpreter::ModuleBuilder& m) { interpreter.registerModule("sys", [](Interpreter::ModuleBuilder& m) {
Interpreter& I = m.interpreterRef; 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 { m.fn("platform", [](std::vector<Value>, int, int) -> Value {
#if defined(_WIN32) #if defined(_WIN32)
return Value(std::string("windows")); return Value(std::string("win32"));
#elif defined(__APPLE__) #elif defined(__APPLE__)
return Value(std::string("macos")); return Value(std::string("darwin"));
#elif defined(__linux__) #elif defined(__linux__)
return Value(std::string("linux")); return Value(std::string("linux"));
#else #else
return Value(std::string("unknown")); return Value(std::string("unknown"));
#endif #endif
}); });
m.fn("getenv", [](std::vector<Value> a, int, int) -> Value { m.fn("version", [](std::vector<Value>, int, int) -> Value { return Value(std::string("0.0.3")); });
if (a.size() != 1 || !a[0].isString()) return NONE_VALUE; // argv(): array of strings
const char* v = std::getenv(a[0].asString().c_str()); m.fn("argv", [&I](std::vector<Value>, int, int) -> Value {
if (!v) return NONE_VALUE; std::vector<Value> out;
return Value(std::string(v)); for (const auto& s : I.getArgv()) out.push_back(Value(s));
});
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); return Value(out);
}); });
m.fn("setenv", [](std::vector<Value> a, int, int) -> Value { // executable(): absolute path to the running binary (host-provided)
if (a.size() != 2 || !a[0].isString() || !a[1].isString()) return Value(false); m.fn("executable", [&I](std::vector<Value>, int, int) -> Value { return Value(I.getExecutablePath()); });
#if defined(__APPLE__) || defined(__linux__) // modules(): read-only snapshot of module cache
int rc = ::setenv(a[0].asString().c_str(), a[1].asString().c_str(), 1); m.fn("modules", [&I](std::vector<Value>, int, int) -> Value {
return Value(rc == 0); Value dictVal = Value(std::unordered_map<std::string, Value>{});
#else auto snapshot = I.getModuleCacheSnapshot();
return Value(false); return Value(snapshot);
#endif
}); });
m.fn("unsetenv", [](std::vector<Value> a, int, int) -> Value { m.fn("exit", [](std::vector<Value> a, int, int) -> Value {
if (a.size() != 1 || !a[0].isString()) return Value(false); int code = 0; if (!a.empty() && a[0].isNumber()) code = static_cast<int>(a[0].asNumber());
#if defined(__APPLE__) || defined(__linux__) std::exit(code);
int rc = ::unsetenv(a[0].asString().c_str()); return NONE_VALUE;
return Value(rc == 0);
#else
return Value(false);
#endif
}); });
// env/cwd/pid moved to os; keep sys minimal
}); });
} }

View File

@ -0,0 +1,31 @@
#include "time_module.h"
#include "Interpreter.h"
#include "Environment.h"
#include <chrono>
#include <thread>
void registerTimeModule(Interpreter& interpreter) {
interpreter.registerModule("time", [](Interpreter::ModuleBuilder& m) {
m.fn("now", [](std::vector<Value>, int, int) -> Value {
using namespace std::chrono;
auto now = system_clock::now().time_since_epoch();
auto us = duration_cast<microseconds>(now).count();
return Value(static_cast<double>(us));
});
m.fn("monotonic", [](std::vector<Value>, int, int) -> Value {
using namespace std::chrono;
auto now = steady_clock::now().time_since_epoch();
auto us = duration_cast<microseconds>(now).count();
return Value(static_cast<double>(us));
});
m.fn("sleep", [](std::vector<Value> 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<int>(seconds * 1000)));
return NONE_VALUE;
});
});
}

View File

@ -6,20 +6,23 @@
int main(int argc, char* argv[]){ int main(int argc, char* argv[]){
Bob bobLang; Bob bobLang;
// Example: host can register a custom module via Bob bridge (applied on first use) // Enable open preset (all builtins, file imports allowed)
bobLang.registerModule("demo", [](ModuleRegistry::ModuleBuilder& m) { bobLang.setSafetyPreset("open");
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) { if(argc > 1) {
// Seed argv/executable for sys module
std::vector<std::string> 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]); bobLang.runFile(argv[1]);
} else { } else {
// For REPL, use interactive mode // For REPL, use interactive mode and seed empty argv
std::vector<std::string> args;
bobLang.registerModule("__configure_sys_argv__", [args, execPath = std::string(argv[0])](ModuleRegistry::ModuleBuilder& m){
m.interpreterRef.setArgv(args, execPath);
});
bobLang.runPrompt(); bobLang.runPrompt();
} }

View File

@ -648,6 +648,11 @@ std::shared_ptr<Stmt> Parser::fromImportStatement() {
Token mod = isString ? advance() : consume(IDENTIFIER, "Expected module name or path string after 'from'."); Token mod = isString ? advance() : consume(IDENTIFIER, "Expected module name or path string after 'from'.");
// Keep IDENTIFIER for name-based from-imports // Keep IDENTIFIER for name-based from-imports
consume(IMPORT, "Expected 'import' after module name."); 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<FromImportStmt::ImportItem> items; std::vector<FromImportStmt::ImportItem> items;
do { do {
Token name = consume(IDENTIFIER, "Expected name to import."); Token name = consume(IDENTIFIER, "Expected name to import.");

View File

@ -336,6 +336,20 @@ void Executor::visitImportStmt(const std::shared_ptr<ImportStmt>& statement, Exe
void Executor::visitFromImportStmt(const std::shared_ptr<FromImportStmt>& statement, ExecutionContext* context) { 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 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<std::string, Value>* 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 // Build item list name->alias
std::vector<std::pair<std::string,std::string>> items; std::vector<std::pair<std::string,std::string>> items;
for (const auto& it : statement->items) { for (const auto& it : statement->items) {

View File

@ -124,13 +124,9 @@ Value Interpreter::importModule(const std::string& spec, int line, int column) {
// Cache key resolution // Cache key resolution
std::string key = spec; std::string key = spec;
std::string baseDir = ""; std::string baseDir = "";
if (errorReporter) { if (errorReporter && !errorReporter->getCurrentFileName().empty()) {
// Try to use current file from reporter; else cwd std::filesystem::path p(errorReporter->getCurrentFileName());
if (!errorReporter->getCurrentFileName().empty()) { baseDir = p.has_parent_path() ? p.parent_path().string() : baseDir;
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 (looksPath) {
if (!allowFileImports) { if (!allowFileImports) {
@ -222,7 +218,6 @@ Value Interpreter::importModule(const std::string& spec, int line, int column) {
// Restore env and reporter // Restore env and reporter
setEnvironment(saved); setEnvironment(saved);
if (errorReporter) errorReporter->popSource(); if (errorReporter) errorReporter->popSource();
return moduleVal; return moduleVal;
} }

View File

@ -119,31 +119,7 @@ void BobStdLib::addToEnvironment(std::shared_ptr<Environment> env, Interpreter&
// Store the shared_ptr in the interpreter to keep it alive // Store the shared_ptr in the interpreter to keep it alive
interpreter.addBuiltinFunction(assertFunc); interpreter.addBuiltinFunction(assertFunc);
// Create a built-in time function (returns strictly increasing microseconds) // time-related globals moved into builtin time module
auto timeFunc = std::make_shared<BuiltinFunction>("time",
[errorReporter](std::vector<Value> 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<std::chrono::microseconds>(duration).count();
if (microseconds <= lastReturnedMicros) {
microseconds = lastReturnedMicros + 1;
}
lastReturnedMicros = microseconds;
return Value(static_cast<double>(microseconds));
});
env->define("time", Value(timeFunc));
// Store the shared_ptr in the interpreter to keep it alive
interpreter.addBuiltinFunction(timeFunc);
// Create a built-in input function // Create a built-in input function
auto inputFunc = std::make_shared<BuiltinFunction>("input", auto inputFunc = std::make_shared<BuiltinFunction>("input",
@ -333,44 +309,49 @@ void BobStdLib::addToEnvironment(std::shared_ptr<Environment> env, Interpreter&
// Store the shared_ptr in the interpreter to keep it alive // Store the shared_ptr in the interpreter to keep it alive
interpreter.addBuiltinFunction(exitFunc); interpreter.addBuiltinFunction(exitFunc);
// Create a built-in sleep function for animations and timing // sleep moved into builtin time module
auto sleepFunc = std::make_shared<BuiltinFunction>("sleep",
[errorReporter](std::vector<Value> args, int line, int column) -> Value { // Introspection: dir(obj) and functions(obj)
if (args.size() != 1) { auto dirFunc = std::make_shared<BuiltinFunction>("dir",
if (errorReporter) { [](std::vector<Value> args, int, int) -> Value {
errorReporter->reportError(line, column, "StdLib Error", if (args.size() != 1) return Value(std::vector<Value>{});
"Expected 1 argument but got " + std::to_string(args.size()) + ".", "", true); Value obj = args[0];
std::vector<Value> 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));
} }
return Value(out);
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<int>(seconds * 1000);
std::this_thread::sleep_for(std::chrono::milliseconds(milliseconds));
return NONE_VALUE;
}); });
env->define("sleep", Value(sleepFunc)); env->define("dir", Value(dirFunc));
interpreter.addBuiltinFunction(dirFunc);
// Store the shared_ptr in the interpreter to keep it alive
interpreter.addBuiltinFunction(sleepFunc); auto functionsFunc = std::make_shared<BuiltinFunction>("functions",
[](std::vector<Value> args, int, int) -> Value {
if (args.size() != 1) return Value(std::vector<Value>{});
Value obj = args[0];
std::vector<Value> out;
auto pushIfFn = [&out](const std::pair<const std::string, Value>& 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 // Create a built-in random function
auto randomFunc = std::make_shared<BuiltinFunction>("random", auto randomFunc = std::make_shared<BuiltinFunction>("random",
@ -397,231 +378,11 @@ void BobStdLib::addToEnvironment(std::shared_ptr<Environment> env, Interpreter&
// Store the shared_ptr in the interpreter to keep it alive // Store the shared_ptr in the interpreter to keep it alive
interpreter.addBuiltinFunction(randomFunc); interpreter.addBuiltinFunction(randomFunc);
// Create a built-in eval function (like Python's eval) // (eval and evalFile moved to eval module)
auto evalFunc = std::make_shared<BuiltinFunction>("eval",
[&interpreter, errorReporter](std::vector<Value> 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 = "<eval>";
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<Token> tokens = lexer.Tokenize(code);
// Create a new parser
Parser parser(tokens);
parser.setErrorReporter(errorReporter);
std::vector<std::shared_ptr<Stmt>> 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);
// Create a built-in readFile function // (file I/O moved to io module)
auto readFileFunc = std::make_shared<BuiltinFunction>("readFile",
[errorReporter](std::vector<Value> 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<BuiltinFunction>("writeFile",
[errorReporter](std::vector<Value> 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<BuiltinFunction>("readLines",
[errorReporter](std::vector<Value> 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<Value> 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<BuiltinFunction>("fileExists",
[errorReporter](std::vector<Value> 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);
// Create a built-in memoryUsage function (platform-specific, best effort) // Create a built-in memoryUsage function (platform-specific, best effort)
auto memoryUsageFunc = std::make_shared<BuiltinFunction>("memoryUsage", auto memoryUsageFunc = std::make_shared<BuiltinFunction>("memoryUsage",

View File

@ -3274,54 +3274,57 @@ print(" * Error reporting in both modes");
// Additional Tests: Classes and Extensions // Additional Tests: Classes and Extensions
print("\n--- 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"; 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"; 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"; 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"; 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"; 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"; 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"; 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"; 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"; 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"; 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"; 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"; 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"; 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"; 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"; 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"; 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"; 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"; 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"; 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"; 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 // Modules: basic imports suite
var pathMods = fileExists("tests/test_imports_basic.bob") ? "tests/test_imports_basic.bob" : "../tests/test_imports_basic.bob"; 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"; 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("\nAll tests passed.");
print("Test suite complete."); print("Test suite complete.");

View File

@ -89,18 +89,8 @@
// print("done"); // print("done");
from bobby import A as fart; // Modules: basic imports suite
import bobby; var pathMods = fileExists("tests/test_imports_basic.bob") ? "tests/test_imports_basic.bob" : "../tests/test_imports_basic.bob";
var a = fart(); evalFile(pathMods);
var b = bobby.A();
a.test();
b.test();
print(bobby);
print("done"); print("done");

View File

@ -28,7 +28,7 @@ var after = mod_hello.X;
assert(before == after, "module executed once (cached)"); assert(before == after, "module executed once (cached)");
// Cross-file visibility in same interpreter: eval user file after importing here // 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 // Immutability: cannot reassign module binding
var immFail = false; var immFail = false;

View File

@ -3,39 +3,18 @@ print("\n--- Test: builtin imports ---");
import sys; import sys;
assert(type(sys) == "module", "sys is module"); assert(type(sys) == "module", "sys is module");
from sys import memoryUsage as mem; from sys import memoryUsage as mem, platform, version;
var m = mem(); var m = mem();
assert(type(m) == "number" || type(m) == "none", "memoryUsage returns number or none"); 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(); var plat = platform();
assert(type(plat) == "string", "platform returns string"); assert(type(plat) == "string", "platform returns string");
var home = getenv("HOME"); var ver = version();
assert(type(home) == "string" || type(home) == "none", "getenv returns string/none"); assert(type(ver) == "string", "version returns string");
var p = pid();
assert(type(p) == "number", "pid returns number");
var hs = homeDir(); // OS functions are in os module now
assert(type(hs) == "string" || type(hs) == "none", "homeDir returns string/none"); import os;
var td = tempDir(); var dir = os.getcwd();
assert(type(td) == "string", "tempDir returns string"); assert(type(dir) == "string" || type(dir) == "none", "getcwd returns string/none");
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"); print("builtin imports: PASS");

29
tests/test_os_basic.bob Normal file
View File

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