Compare commits

..

10 Commits

Author SHA1 Message Date
ec4b5afa9c windows specific changes 2025-08-13 00:12:54 -04:00
6e3379b5b8 Updated tests and testing built in modules 2025-08-12 16:50:54 -04:00
7f7c6e438d Started fleshing out built in modules.
added policy templates for module safety
2025-08-12 03:26:50 -04:00
8cdccae214 Built in modules, user modules, ability to disable builtin modules 2025-08-12 00:16:36 -04:00
fc63c3e46f Try Catch bug fixing 2025-08-11 20:12:24 -04:00
227586c583 Try Catch 2025-08-11 18:26:41 -04:00
3138f6fb92 Various changes, again. Updated extension. Added classes, super, this, polymorphism.
Runtime: add method dispatch for array/string/dict/number (.len, .push, .pop, .keys, .values, .has, .toInt)
Stdlib: delete global len/push/pop/keys/values/has
Tests/docs/examples: migrate to method style; add tests/test_builtin_methods_style.bob
All tests pass
Breaking: global len/push/pop/keys/values/has removed; use methods instead
Parser/AST: add class/extends/extension/super, field initializers
Runtime: shared methods with this injection; classParents/classTemplates; super resolution; ownerClass/currentClass; extension lookup order
Builtins: method dispatch for array/string/dict/number (.len/.push/.pop/.keys/.values/.has/.toInt); remove global forms
Tests/docs/examples: add/refresh for classes, inheritance, super, polymorphism; migrate to method style; all tests pass
VS Code extension: update grammar/readme/snippets for new features
2025-08-10 22:44:46 -04:00
7a9c0b7ea9 Pre class implementation commit 2025-08-10 16:50:18 -04:00
266cca5b42 Code cleanup 2025-08-10 16:33:48 -04:00
85d3381575 Cleanup 2025-08-10 15:09:37 -04:00
117 changed files with 4609 additions and 2029 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@ build/
.DS_Store .DS_Store
build-ninja build-ninja
build-release

View File

@ -45,16 +45,18 @@ 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_CLI_SOURCES "src/sources/cli/*.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 # All source files
set(BOB_ALL_SOURCES set(BOB_ALL_SOURCES
${BOB_RUNTIME_SOURCES} ${BOB_RUNTIME_SOURCES}
${BOB_PARSING_SOURCES} ${BOB_PARSING_SOURCES}
${BOB_STDLIB_SOURCES} ${BOB_STDLIB_SOURCES}
${BOB_BUILTIN_SOURCES}
${BOB_CLI_SOURCES} ${BOB_CLI_SOURCES}
) )
@ -66,6 +68,7 @@ target_include_directories(bob PRIVATE
src/headers/runtime src/headers/runtime
src/headers/parsing src/headers/parsing
src/headers/stdlib src/headers/stdlib
src/headers/builtinModules
src/headers/cli src/headers/cli
src/headers/common src/headers/common
) )

View File

@ -233,19 +233,26 @@ toBoolean(1); // true
type(42); // "number" type(42); // "number"
``` ```
### Arrays & Strings ### Arrays, Strings, and Dictionaries: Method style (preferred)
```go ```go
len([1, 2, 3]); // 3 [1, 2, 3].len(); // 3
len("hello"); // 5 "hello".len(); // 5
push(array, value); // Add to end var a = [1, 2]; a.push(3); // a is now [1, 2, 3]
pop(array); // Remove from end var v = a.pop(); // v == 3
var d = {"a": 1, "b": 2};
d.len(); // 2
d.keys(); // ["a", "b"]
d.values(); // [1, 2]
d.has("a"); // true
``` ```
### Dictionaries Note: Global forms like `len(x)`, `push(arr, ...)`, `pop(arr)`, `keys(dict)`, `values(dict)`, `has(dict, key)` have been removed. Use method style.
### Numbers
```go ```go
keys(dict); // Array of keys toInt(3.9); // 3 (global)
values(dict); // Array of values (3.9).toInt(); // 3 (method on number)
has(dict, "key"); // Check if key exists
``` ```
### Utility ### Utility
@ -253,7 +260,7 @@ has(dict, "key"); // Check if key exists
assert(condition, "message"); // Testing assert(condition, "message"); // Testing
time(); // Current time in microseconds time(); // Current time in microseconds
sleep(1.5); // Sleep for 1.5 seconds sleep(1.5); // Sleep for 1.5 seconds
random(); // Random number 0-1 rand.random(); // Random number 0-1
eval("print('Hello');"); // Execute string as code eval("print('Hello');"); // Execute string as code
exit(0); // Exit program exit(0); // Exit program
``` ```
@ -266,8 +273,93 @@ var lines = readLines("config.txt");
var exists = fileExists("test.txt"); var exists = fileExists("test.txt");
``` ```
## Standard Library Reference
The following built-ins are available by default. Unless specified, functions throw on invalid argument counts/types.
- print(x): prints x with newline
- printRaw(x): prints x without newline
- input(prompt?): reads a line from stdin (optional prompt)
- toString(x): returns string representation
- toNumber(s): parses string to number or returns none
- toInt(n): truncates number to integer
- toBoolean(x): converts to boolean using truthiness rules
- type(x): returns the type name as string
- len(x) / x.len(): length of array/string/dict
- push(arr, ...values) / arr.push(...values): appends values to array in place, returns arr
- pop(arr) / arr.pop(): removes and returns last element
- keys(dict) / dict.keys(): returns array of keys
- values(dict) / dict.values(): returns array of values
- has(dict, key) / dict.has(key): returns true if key exists
- readFile(path): returns entire file contents as string
- writeFile(path, content): writes content to file
- readLines(path): returns array of lines
- fileExists(path): boolean
- time(): microseconds since Unix epoch
- sleep(seconds): pauses execution
- rand.random(): float in [0,1)
- eval(code): executes code string in current environment
- exit(code?): terminates the program
Notes:
- Arrays support properties: length, first, last, empty
- Dicts support properties: length, empty, keys, values
- Method-style builtins on arrays/strings/dicts are preferred; global forms remain for compatibility.
## Advanced Features ## Advanced Features
### Classes (Phase 1)
```go
// Declare a class with fields and methods
class Person {
var name;
var age;
// Methods can use implicit `this`
func setName(n) { this.name = n; }
func greet() { print("Hi, I'm " + this.name); }
}
// Construct via the class name
var p = Person();
p.setName("Bob");
p.greet();
// Fields are stored on the instance (a dictionary under the hood)
p.age = 30;
```
Notes:
- Instances are plain dictionaries; methods are shared functions placed on the instance.
- On a property call like `obj.method(...)`, the interpreter injects `this = obj` into the call frame (no argument injection).
- Taking a method reference and calling it later does not autobind `this`; call via `obj.method(...)` when needed.
### Extensions (Builtins and Classes)
Extend existing types (including builtins) with new methods:
```go
extension array {
func sum() {
var i = 0; var s = 0;
while (i < len(this)) { s = s + this[i]; i = i + 1; }
return s;
}
}
extension dict { func size() { return len(this); } }
extension string { func shout() { return toString(this) + "!"; } }
extension any { func tag() { return "<" + type(this) + ">"; } }
assert([1,2,3].sum() == 6);
assert({"a":1,"b":2}.size() == 2);
assert("hi".shout() == "hi!");
assert(42.tag() == "<number>");
```
Notes:
- Lookup order for `obj.method(...)`: instance dictionary → class extensions (for user classes) → builtin extensions (string/array/dict) → `any`.
- `this` is injected for property calls.
### String Interpolation ### String Interpolation
```go ```go
var name = "Alice"; var name = "Alice";
@ -349,7 +441,7 @@ var people = [
{"name": "Bob", "age": 25} {"name": "Bob", "age": 25}
]; ];
for (var i = 0; i < len(people); i = i + 1) { for (var i = 0; i < people.len(); i = i + 1) {
var person = people[i]; var person = people[i];
print(person["name"] + " is " + person["age"] + " years old"); print(person["name"] + " is " + person["age"] + " years old");
} }
@ -360,17 +452,17 @@ for (var i = 0; i < len(people); i = i + 1) {
var lines = readLines("data.txt"); var lines = readLines("data.txt");
var processed = []; var processed = [];
for (var i = 0; i < len(lines); i = i + 1) { for (var i = 0; i < lines.len(); i = i + 1) {
var line = lines[i]; var line = lines[i];
if (len(line) > 0) { if (line.len() > 0) {
push(processed, "Processed: " + line); processed.push("Processed: " + line);
} }
} }
var output = ""; var output = "";
for (var i = 0; i < len(processed); i = i + 1) { for (var i = 0; i < processed.len(); i = i + 1) {
output = output + processed[i]; output = output + processed[i];
if (i < len(processed) - 1) { if (i < processed.len() - 1) {
output = output + "\n"; output = output + "\n";
} }
} }

113
Reference/EMBEDDING.md Normal file
View File

@ -0,0 +1,113 @@
Embedding Bob: Public API Guide
================================
This document explains how to embed the Bob interpreter in your C++ application, register custom modules, and control sandbox policies.
Quick Start
-----------
```cpp
#include "cli/bob.h"
#include "ModuleRegistry.h"
int main() {
Bob bob;
// Optional: configure policies or modules before first use
bob.setBuiltinModulePolicy(true); // allow builtin modules (default)
bob.setBuiltinModuleDenyList({/* e.g., "sys" */});
// Register a custom builtin module called "demo"
bob.registerModule("demo", [](ModuleRegistry::ModuleBuilder& m) {
m.fn("hello", [](std::vector<Value> args, int, int) -> Value {
std::string who = (args.size() >= 1 && args[0].isString()) ? args[0].asString() : "world";
return Value(std::string("hello ") + who);
});
m.val("meaning", Value(42.0));
});
// Evaluate code from a string
bob.evalString("import demo; print(demo.hello(\"Bob\"));", "<host>");
// Evaluate a file (imports inside resolve relative to the file's directory)
bob.evalFile("script.bob");
}
```
API Overview
------------
Bob exposes a single high-level object with a minimal, consistent API. It self-manages an internal interpreter and applies configuration on first use.
- Program execution
- `bool evalString(const std::string& code, const std::string& filename = "<eval>")`
- `bool evalFile(const std::string& path)`
- `void runFile(const std::string& path)` (CLI convenience delegates to `evalFile`)
- `void runPrompt()` (interactive CLI delegates each line to `evalString`)
- Module registration and sandboxing
- `void registerModule(const std::string& name, std::function<void(ModuleRegistry::ModuleBuilder&)> init)`
- `void setBuiltinModulePolicy(bool allow)`
- `void setBuiltinModuleAllowList(const std::vector<std::string>& allowed)`
- `void setBuiltinModuleDenyList(const std::vector<std::string>& denied)`
- Global environment helpers
- `bool defineGlobal(const std::string& name, const Value& value)`
- `bool tryGetGlobal(const std::string& name, Value& out) const`
All configuration calls are safe to use before any evaluation they are queued and applied automatically when the interpreter is first created.
Registering Custom Builtin Modules
----------------------------------
Use the builder convenience to create a module:
```cpp
bob.registerModule("raylib", [](ModuleRegistry::ModuleBuilder& m) {
m.fn("init", [](std::vector<Value> args, int line, int col) -> Value {
// call into your library here; validate args, return Value
return NONE_VALUE;
});
m.val("VERSION", Value(std::string("5.0")));
});
```
At runtime:
```bob
import raylib;
raylib.init();
print(raylib.VERSION);
```
Notes
-----
- Modules are immutable, first-class objects. `type(module)` is "module" and `toString(module)` prints `<module 'name'>`.
- Reassigning a module binding or setting module properties throws an error.
Builtin Modules and Sandboxing
------------------------------
- Builtin modules (e.g., `sys`) are registered during interpreter construction.
- File imports are always resolved relative to the importing file's directory.
- Policies:
- `setBuiltinModulePolicy(bool allow)` enable/disable all builtin modules.
- `setBuiltinModuleAllowList(vector<string>)` allow only listed modules (deny others).
- `setBuiltinModuleDenyList(vector<string>)` explicitly deny listed modules.
- Denied/disabled modules are cloaked: `import name` reports "Module not found".
Error Reporting
---------------
- `evalString`/`evalFile` set file context for error reporting so line/column references point to the real source.
- Both return `true` on success and `false` if execution failed (errors are reported via the internal error reporter).
CLI vs Embedding
----------------
- CLI builds include `main.cpp` (entry point), which uses `Bob::runFile` or `Bob::runPrompt`.
- Embedded hosts do not use `main.cpp`; instead they instantiate `Bob` and call `evalString`/`evalFile` directly.

View File

@ -56,7 +56,7 @@ Bob is a mature, working programming language with a modern architecture and com
- **Type System**: `type()`, `toString()`, `toNumber()`, `toInt()`, `toBoolean()` - **Type System**: `type()`, `toString()`, `toNumber()`, `toInt()`, `toBoolean()`
- **Testing**: `assert()` with custom error messages - **Testing**: `assert()` with custom error messages
- **Timing**: `time()` (microsecond precision), `sleep()` - **Timing**: `time()` (microsecond precision), `sleep()`
- **Utility**: `random()` (properly seeded), `eval()`, `exit()` - **Utility**: `rand.random()` (properly seeded), `eval.eval()`, `sys.exit()`
- **Data Structure**: `len()`, `push()`, `pop()`, `keys()`, `values()`, `has()` - **Data Structure**: `len()`, `push()`, `pop()`, `keys()`, `values()`, `has()`
- **File I/O**: `readFile()`, `writeFile()`, `readLines()`, `fileExists()` - **File I/O**: `readFile()`, `writeFile()`, `readLines()`, `fileExists()`

View File

@ -17,11 +17,14 @@ This extension provides syntax highlighting and language support for the Bob pro
- Control flow: `if`, `else`, `while`, `for`, `break`, `continue`, `return` - Control flow: `if`, `else`, `while`, `for`, `break`, `continue`, `return`
- Variable declaration: `var` - Variable declaration: `var`
- Function declaration: `func` - Function declaration: `func`
- Classes and OOP: `class`, `extends`, `extension`, `this`, `super`
- Logical operators: `and`, `or`, `not` - Logical operators: `and`, `or`, `not`
### Built-in Functions ### Built-in Functions
- `print()`, `assert()`, `input()`, `type()`, `toString()`, `toNumber()`, `time()` - `print()`, `assert()`, `input()`, `type()`, `toString()`, `toNumber()`, `toInt()`, `time()`, `sleep()`, `printRaw()`
- `sleep()`, `printRaw()`, `len()`, `push()`, `pop()`, `random()`, `eval()` - Arrays/Dictionaries (preferred method style): `arr.len()`, `arr.push(...)`, `arr.pop()`, `dict.len()`, `dict.keys()`, `dict.values()`, `dict.has()`
- Global forms still available: `len(x)`, `push(arr, ...)`, `pop(arr)`, `keys(dict)`, `values(dict)`, `has(dict, key)`
- Misc: `rand.random()`, `eval.eval()`
### Data Types ### Data Types
- Numbers (integers, floats, binary `0b1010`, hex `0xFF`) - Numbers (integers, floats, binary `0b1010`, hex `0xFF`)
@ -92,9 +95,11 @@ Files with the `.bob` extension will automatically be recognized as Bob language
var message = "Hello, Bob!"; var message = "Hello, Bob!";
print(message); print(message);
// Array operations // Array operations (method style)
var numbers = [1, 2, 3, 4, 5]; var numbers = [1, 2, 3, 4, 5];
print("Array length: " + len(numbers)); print("Array length: " + numbers.len());
numbers.push(6);
print("Popped: " + numbers.pop());
print("First element: " + numbers[0]); print("First element: " + numbers[0]);
// Function with ternary operator // Function with ternary operator

Binary file not shown.

View File

@ -20,12 +20,12 @@ print("Pi: " + toString(pi));
var numbers = [1, 2, 3, 4, 5]; var numbers = [1, 2, 3, 4, 5];
var fruits = ["apple", "banana", "cherry"]; var fruits = ["apple", "banana", "cherry"];
print("Array length: " + len(numbers)); print("Array length: " + numbers.len());
print("First element: " + numbers[0]); print("First element: " + numbers[0]);
numbers[2] = 99; // Array assignment numbers[2] = 99; // Array assignment
push(numbers, 6); // Add element numbers.push(6); // Add element
var lastElement = pop(numbers); // Remove and get last element var lastElement = numbers.pop(); // Remove and get last element
// Function definition // Function definition
func factorial(n) { func factorial(n) {
@ -91,7 +91,7 @@ value *= 2;
value -= 3; value -= 3;
// New built-in functions // New built-in functions
var randomValue = random(); import rand; var randomValue = rand.random();
sleep(100); // Sleep for 100ms sleep(100); // Sleep for 100ms
printRaw("No newline here"); printRaw("No newline here");
eval("print('Dynamic code execution!');"); eval("print('Dynamic code execution!');");

View File

@ -23,7 +23,7 @@
["'", "'"] ["'", "'"]
], ],
"indentationRules": { "indentationRules": {
"increaseIndentPattern": "\\{[^}]*$|\\b(func|if|else|while|for)\\b.*$", "increaseIndentPattern": "\\{[^}]*$|\\b(func|if|else|while|for|class|extension)\\b.*$",
"decreaseIndentPattern": "^\\s*[})]" "decreaseIndentPattern": "^\\s*[})]"
}, },
"folding": { "folding": {

View File

@ -2,7 +2,7 @@
"name": "bob-language", "name": "bob-language",
"displayName": "Bob Language", "displayName": "Bob Language",
"description": "Syntax highlighting and language support for the Bob programming language - featuring arrays, dictionaries, auto-truncating float indices, increment/decrement operators, cross-type comparisons, and comprehensive built-in functions", "description": "Syntax highlighting and language support for the Bob programming language - featuring arrays, dictionaries, auto-truncating float indices, increment/decrement operators, cross-type comparisons, and comprehensive built-in functions",
"version": "0.4.0", "version": "0.5.0",
"engines": { "engines": {
"vscode": "^1.60.0" "vscode": "^1.60.0"
}, },

View File

@ -53,6 +53,32 @@
], ],
"description": "Declare a variable" "description": "Declare a variable"
}, },
"Class Declaration": {
"prefix": "class",
"body": [
"class ${1:ClassName} ${2:extends ${3:Parent}} {",
" var ${4:field} = ${5:none};",
" func init(${6:args}) {",
" this.${4:field} = ${7:value};",
" }",
" func ${8:method}(${9:params}) {",
" $0",
" }",
"}"
],
"description": "Create a class with optional extends, fields, init, and method"
},
"Extension Block": {
"prefix": "extension",
"body": [
"extension ${1:Target} {",
" func ${2:method}(${3:params}) {",
" $0",
" }",
"}"
],
"description": "Create an extension for a class or builtin (string, array, dict, number, any)"
}
"Print Statement": { "Print Statement": {
"prefix": "print", "prefix": "print",
"body": [ "body": [
@ -274,7 +300,7 @@
"Random Number": { "Random Number": {
"prefix": "random", "prefix": "random",
"body": [ "body": [
"var randomValue = random();" "import rand; var randomValue = rand.random();"
], ],
"description": "Generate random number" "description": "Generate random number"
}, },

View File

@ -153,7 +153,7 @@
"patterns": [ "patterns": [
{ {
"name": "keyword.control.bob", "name": "keyword.control.bob",
"match": "\\b(if|else|while|do|for|break|continue|return|var|func)\\b" "match": "\\b(if|else|while|do|for|break|continue|return|var|func|class|extends|extension|this|super)\\b"
}, },
{ {
"name": "keyword.operator.bob", "name": "keyword.operator.bob",

13
bobby.bob Normal file
View File

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

View File

@ -9,14 +9,14 @@ print("Test 1: Heavy string operations");
var stringData = []; var stringData = [];
for (var i = 0; i < 100000; i++) { for (var i = 0; i < 100000; i++) {
var str = toString(i) + "_" + toString(i * 2) + "_" + toString(i * 3); var str = toString(i) + "_" + toString(i * 2) + "_" + toString(i * 3);
push(stringData, { stringData.push({
"original": str, "original": str,
"upper": str, // Bob doesn't have toUpper, but test string storage "upper": str, // Bob doesn't have toUpper, but test string storage
"length": len(str), "length": str.len(),
"type": type(str) "type": type(str)
}); });
} }
print("Created " + len(stringData) + " string operation results"); print("Created " + stringData.len() + " string operation results");
print("Memory: " + memoryUsage() + " MB"); print("Memory: " + memoryUsage() + " MB");
input("Press Enter to clear string data..."); input("Press Enter to clear string data...");
stringData = none; stringData = none;
@ -33,12 +33,12 @@ for (var i = 0; i < 200000; i++) {
var intVal = toInt(num); var intVal = toInt(num);
var boolVal = toBoolean(i % 2); var boolVal = toBoolean(i % 2);
push(conversions, [ conversions.push([
num, str, backToNum, intVal, boolVal, num, str, backToNum, intVal, boolVal,
type(num), type(str), type(backToNum), type(intVal), type(boolVal) type(num), type(str), type(backToNum), type(intVal), type(boolVal)
]); ]);
} }
print("Created " + len(conversions) + " type conversion results"); print("Created " + conversions.len() + " type conversion results");
print("Memory: " + memoryUsage() + " MB"); print("Memory: " + memoryUsage() + " MB");
input("Press Enter to clear conversions..."); input("Press Enter to clear conversions...");
conversions = []; conversions = [];
@ -53,15 +53,15 @@ for (var i = 0; i < 50000; i++) {
var dict = {"a": i, "b": i+1, "c": i+2}; var dict = {"a": i, "b": i+1, "c": i+2};
// Use builtin functions heavily // Use builtin functions heavily
var arrLen = len(arr); var arrLen = arr.len();
push(arr, i+3); arr.push(i+3);
var popped = pop(arr); var popped = arr.pop();
var dictKeys = keys(dict); var dictKeys = dict.keys();
var dictValues = values(dict); var dictValues = dict.values();
var hasA = has(dict, "a"); var hasA = dict.has("a");
push(collections, { collections.push({
"array": arr, "array": arr,
"dict": dict, "dict": dict,
"arrLen": arrLen, "arrLen": arrLen,
@ -71,7 +71,7 @@ for (var i = 0; i < 50000; i++) {
"hasA": hasA "hasA": hasA
}); });
} }
print("Created " + len(collections) + " collection operation results"); print("Created " + collections.len() + " collection operation results");
print("Memory: " + memoryUsage() + " MB"); print("Memory: " + memoryUsage() + " MB");
input("Press Enter to clear collections..."); input("Press Enter to clear collections...");
collections = "cleared"; collections = "cleared";
@ -88,14 +88,14 @@ for (var i = 0; i < 10000; i++) {
var funcExpr = "func() { return " + toString(i) + "; };"; var funcExpr = "func() { return " + toString(i) + "; };";
var evalFunc = eval(funcExpr); var evalFunc = eval(funcExpr);
push(evalResults, { evalResults.push({
"expr": expression, "expr": expression,
"result": result, "result": result,
"func": evalFunc, "func": evalFunc,
"funcResult": evalFunc() "funcResult": evalFunc()
}); });
} }
print("Created " + len(evalResults) + " eval results"); print("Created " + evalResults.len() + " eval results");
print("Memory: " + memoryUsage() + " MB"); print("Memory: " + memoryUsage() + " MB");
input("Press Enter to clear eval results..."); input("Press Enter to clear eval results...");
evalResults = none; evalResults = none;
@ -121,15 +121,15 @@ for (var i = 0; i < 1000; i++) {
var readContent = readFile(filename); var readContent = readFile(filename);
var lines = readLines(filename); var lines = readLines(filename);
push(fileData, { fileData.push({
"filename": filename, "filename": filename,
"exists": exists, "exists": exists,
"content": readContent, "content": readContent,
"lines": lines, "lines": lines,
"lineCount": len(lines) "lineCount": lines.len()
}); });
} }
print("Created " + len(fileData) + " file operation results"); print("Created " + fileData.len() + " file operation results");
print("Memory: " + memoryUsage() + " MB"); print("Memory: " + memoryUsage() + " MB");
input("Press Enter to clear file data..."); input("Press Enter to clear file data...");
fileData = []; fileData = [];
@ -151,18 +151,19 @@ input("File data cleared. Check memory usage...");
print("Test 6: Random number stress"); print("Test 6: Random number stress");
var randomData = []; var randomData = [];
for (var i = 0; i < 200000; i++) { for (var i = 0; i < 200000; i++) {
var rand1 = random(); import rand as RLeak;
var rand2 = random(); var rand1 = RLeak.random();
var rand2 = RLeak.random();
var sum = rand1 + rand2; var sum = rand1 + rand2;
push(randomData, { randomData.push({
"rand1": rand1, "rand1": rand1,
"rand2": rand2, "rand2": rand2,
"sum": sum, "sum": sum,
"product": rand1 * rand2 "product": rand1 * rand2
}); });
} }
print("Created " + len(randomData) + " random number results"); print("Created " + randomData.len() + " random number results");
print("Memory: " + memoryUsage() + " MB"); print("Memory: " + memoryUsage() + " MB");
input("Press Enter to clear random data..."); input("Press Enter to clear random data...");
randomData = none; randomData = none;

View File

@ -8,13 +8,13 @@ print("Initial memory: " + memoryUsage() + " MB");
print("Test 1: Large nested arrays"); print("Test 1: Large nested arrays");
var nestedArrays = []; var nestedArrays = [];
for (var i = 0; i < 50000; i++) { for (var i = 0; i < 50000; i++) {
push(nestedArrays, [ nestedArrays.push([
[i, i+1, i+2], [i, i+1, i+2],
[i*2, i*3, i*4], [i*2, i*3, i*4],
[[i, [i+1, [i+2]]], i*5] [[i, [i+1, [i+2]]], i*5]
]); ]);
} }
print("Created " + len(nestedArrays) + " nested array structures"); print("Created " + nestedArrays.len() + " nested array structures");
print("Memory: " + memoryUsage() + " MB"); print("Memory: " + memoryUsage() + " MB");
input("Press Enter to clear nested arrays..."); input("Press Enter to clear nested arrays...");
nestedArrays = none; nestedArrays = none;
@ -25,7 +25,7 @@ input("Cleared. Check memory usage...");
print("Test 2: Large nested dictionaries"); print("Test 2: Large nested dictionaries");
var nestedDicts = []; var nestedDicts = [];
for (var i = 0; i < 50000; i++) { for (var i = 0; i < 50000; i++) {
push(nestedDicts, { nestedDicts.push({
"id": i, "id": i,
"data": { "data": {
"value": i * 2, "value": i * 2,
@ -42,7 +42,7 @@ for (var i = 0; i < 50000; i++) {
} }
}); });
} }
print("Created " + len(nestedDicts) + " nested dictionary structures"); print("Created " + nestedDicts.len() + " nested dictionary structures");
print("Memory: " + memoryUsage() + " MB"); print("Memory: " + memoryUsage() + " MB");
input("Press Enter to clear nested dicts..."); input("Press Enter to clear nested dicts...");
nestedDicts = []; nestedDicts = [];
@ -53,7 +53,7 @@ input("Cleared. Check memory usage...");
print("Test 3: Mixed array/dict structures"); print("Test 3: Mixed array/dict structures");
var mixedStructures = []; var mixedStructures = [];
for (var i = 0; i < 30000; i++) { for (var i = 0; i < 30000; i++) {
push(mixedStructures, [ mixedStructures.push([
{"arrays": [[i, i+1], [i+2, i+3]]}, {"arrays": [[i, i+1], [i+2, i+3]]},
[{"dicts": {"a": i, "b": i+1}}, {"more": [i, i+1]}], [{"dicts": {"a": i, "b": i+1}}, {"more": [i, i+1]}],
{ {
@ -64,7 +64,7 @@ for (var i = 0; i < 30000; i++) {
} }
]); ]);
} }
print("Created " + len(mixedStructures) + " mixed structures"); print("Created " + mixedStructures.len() + " mixed structures");
print("Memory: " + memoryUsage() + " MB"); print("Memory: " + memoryUsage() + " MB");
input("Press Enter to clear mixed structures..."); input("Press Enter to clear mixed structures...");
mixedStructures = "cleared"; mixedStructures = "cleared";
@ -78,11 +78,15 @@ for (var i = 0; i < 1000000; i++) {
var item = {"id": i, "value": i * 2}; var item = {"id": i, "value": i * 2};
// Create a structure that references itself // Create a structure that references itself
item["self"] = [item, {"parent": item}]; item["self"] = [item, {"parent": item}];
push(selfRef, item); selfRef.push(item);
} }
print("Created " + len(selfRef) + " self-referencing structures"); print("Created " + selfRef.len() + " self-referencing structures");
print("Memory: " + memoryUsage() + " MB"); print("Memory: " + memoryUsage() + " MB");
input("Press Enter to clear self-ref structures..."); input("Press Enter to clear self-ref structures...");
// Break cycles explicitly so reference counting can reclaim memory deterministically
for (var i = 0; i < selfRef.len(); i++) {
selfRef[i]["self"] = none;
}
selfRef = 123; selfRef = 123;
print("Memory after clear: " + memoryUsage() + " MB"); print("Memory after clear: " + memoryUsage() + " MB");
input("Cleared. Check memory usage..."); input("Cleared. Check memory usage...");
@ -95,12 +99,12 @@ for (var i = 0; i < 100000; i++) {
for (var j = 0; j < 100; j++) { for (var j = 0; j < 100; j++) {
longString = longString + "data" + i + "_" + j + " "; longString = longString + "data" + i + "_" + j + " ";
} }
push(stringCollections, { stringCollections.push({
"content": longString, "content": longString,
"words": [longString, longString + "_copy", longString + "_backup"] "words": [longString, longString + "_copy", longString + "_backup"]
}); });
} }
print("Created " + len(stringCollections) + " string collections"); print("Created " + stringCollections.len() + " string collections");
print("Memory: " + memoryUsage() + " MB"); print("Memory: " + memoryUsage() + " MB");
input("Press Enter to clear string collections..."); input("Press Enter to clear string collections...");
stringCollections = none; stringCollections = none;

View File

@ -8,7 +8,7 @@ print("Initial memory: " + memoryUsage() + " MB");
print("Test 1: Recursive function closures"); print("Test 1: Recursive function closures");
var recursiveFuncs = []; var recursiveFuncs = [];
for (var i = 0; i < 100000; i++) { for (var i = 0; i < 100000; i++) {
push(recursiveFuncs, func() { recursiveFuncs.push(func() {
var capturedI = i; var capturedI = i;
return func() { return func() {
if (capturedI > 0) { if (capturedI > 0) {
@ -18,7 +18,7 @@ for (var i = 0; i < 100000; i++) {
}; };
}); });
} }
print("Created " + len(recursiveFuncs) + " recursive closure functions"); print("Created " + recursiveFuncs.len() + " recursive closure functions");
print("Memory after creation: " + memoryUsage() + " MB"); print("Memory after creation: " + memoryUsage() + " MB");
input("Press Enter to clear recursive functions..."); input("Press Enter to clear recursive functions...");
recursiveFuncs = none; recursiveFuncs = none;
@ -29,14 +29,14 @@ input("Cleared. Check memory usage...");
print("Test 2: Function factories"); print("Test 2: Function factories");
var factories = []; var factories = [];
for (var i = 0; i < 100000; i++) { for (var i = 0; i < 100000; i++) {
push(factories, func() { factories.push(func() {
var multiplier = i; var multiplier = i;
return func(x) { return func(x) {
return x * multiplier; return x * multiplier;
}; };
}); });
} }
print("Created " + len(factories) + " function factories"); print("Created " + factories.len() + " function factories");
print("Memory: " + memoryUsage() + " MB"); print("Memory: " + memoryUsage() + " MB");
input("Press Enter to clear factories..."); input("Press Enter to clear factories...");
factories = []; factories = [];
@ -47,7 +47,7 @@ input("Cleared. Check memory usage...");
print("Test 3: Deep function nesting"); print("Test 3: Deep function nesting");
var deepNested = []; var deepNested = [];
for (var i = 0; i < 50000; i++) { for (var i = 0; i < 50000; i++) {
push(deepNested, func() { deepNested.push(func() {
return func() { return func() {
return func() { return func() {
return func() { return func() {
@ -59,7 +59,7 @@ for (var i = 0; i < 50000; i++) {
}; };
}); });
} }
print("Created " + len(deepNested) + " deeply nested functions"); print("Created " + deepNested.len() + " deeply nested functions");
print("Memory: " + memoryUsage() + " MB"); print("Memory: " + memoryUsage() + " MB");
input("Press Enter to clear deep nested..."); input("Press Enter to clear deep nested...");
deepNested = "test"; deepNested = "test";
@ -76,10 +76,10 @@ for (var i = 0; i < 1000000; i++) {
var funcB = func() { var funcB = func() {
return "B" + i; return "B" + i;
}; };
// Store both in same array element to create potential circular refs // Store both in same array element to create potential circular refs
push(circularFuncs, [funcA, funcB, func() { return funcA; }]); circularFuncs.push([funcA, funcB, func() { return funcA; }]);
} }
print("Created " + len(circularFuncs) + " circular function structures"); print("Created " + circularFuncs.len() + " circular function structures");
print("Memory: " + memoryUsage() + " MB"); print("Memory: " + memoryUsage() + " MB");
input("Press Enter to clear circular functions..."); input("Press Enter to clear circular functions...");
circularFuncs = 42; circularFuncs = 42;

View File

@ -10,16 +10,16 @@ var nestedData = [];
for (var i = 0; i < 1000; i++) { for (var i = 0; i < 1000; i++) {
var row = []; var row = [];
for (var j = 0; j < 1000; j++) { for (var j = 0; j < 1000; j++) {
push(row, { row.push({
"i": i, "i": i,
"j": j, "j": j,
"func": func() { return i * j; }, "func": func() { return i * j; },
"data": [i, j, i+j] "data": [i, j, i+j]
}); });
} }
push(nestedData, row); nestedData.push(row);
} }
print("Created " + len(nestedData) + "x" + len(nestedData[0]) + " nested structure"); print("Created " + nestedData.len() + "x" + nestedData[0].len() + " nested structure");
print("Memory: " + memoryUsage() + " MB"); print("Memory: " + memoryUsage() + " MB");
input("Press Enter to clear nested data..."); input("Press Enter to clear nested data...");
nestedData = none; nestedData = none;
@ -31,14 +31,14 @@ print("Test 2: While loop accumulation");
var accumulator = []; var accumulator = [];
var counter = 0; var counter = 0;
while (counter < 500000) { while (counter < 500000) {
push(accumulator, { accumulator.push({
"count": counter, "count": counter,
"func": func() { return counter * 2; }, "func": func() { return counter * 2; },
"meta": ["item" + counter, counter % 100] "meta": ["item" + counter, counter % 100]
}); });
counter = counter + 1; counter = counter + 1;
} }
print("Accumulated " + len(accumulator) + " items in while loop"); print("Accumulated " + accumulator.len() + " items in while loop");
print("Memory: " + memoryUsage() + " MB"); print("Memory: " + memoryUsage() + " MB");
input("Press Enter to clear accumulator..."); input("Press Enter to clear accumulator...");
accumulator = []; accumulator = [];
@ -72,7 +72,7 @@ print("Test 4: Do-while function creation");
var doWhileFuncs = []; var doWhileFuncs = [];
var dwCounter = 0; var dwCounter = 0;
do { do {
push(doWhileFuncs, func() { doWhileFuncs.push(func() {
var captured = dwCounter; var captured = dwCounter;
return func() { return func() {
return captured * captured; return captured * captured;
@ -80,7 +80,7 @@ do {
}); });
dwCounter = dwCounter + 1; dwCounter = dwCounter + 1;
} while (dwCounter < 100000); } while (dwCounter < 100000);
print("Created " + len(doWhileFuncs) + " functions in do-while"); print("Created " + doWhileFuncs.len() + " functions in do-while");
print("Memory: " + memoryUsage() + " MB"); print("Memory: " + memoryUsage() + " MB");
input("Press Enter to clear do-while functions..."); input("Press Enter to clear do-while functions...");
doWhileFuncs = "cleared"; doWhileFuncs = "cleared";
@ -97,7 +97,7 @@ for (var i = 0; i < 500000; i++) {
if (i > 400000 && i % 100 == 0) { if (i > 400000 && i % 100 == 0) {
// Create larger objects near the end // Create larger objects near the end
push(complexData, { complexData.push({
"large": [ "large": [
func() { return i; }, func() { return i; },
[i, i+1, i+2, i+3], [i, i+1, i+2, i+3],
@ -105,14 +105,14 @@ for (var i = 0; i < 500000; i++) {
] ]
}); });
} else { } else {
push(complexData, func() { return i; }); complexData.push(func() { return i; });
} }
if (i > 450000 && len(complexData) > 350000) { if (i > 450000 && complexData.len() > 350000) {
break; // Early exit break; // Early exit
} }
} }
print("Complex loop created " + len(complexData) + " items"); print("Complex loop created " + complexData.len() + " items");
print("Memory: " + memoryUsage() + " MB"); print("Memory: " + memoryUsage() + " MB");
input("Press Enter to clear complex data..."); input("Press Enter to clear complex data...");
complexData = none; complexData = none;
@ -126,7 +126,7 @@ for (var cycle = 0; cycle < 100; cycle++) {
// Create lots of data // Create lots of data
for (var i = 0; i < 10000; i++) { for (var i = 0; i < 10000; i++) {
push(churnData, [ churnData.push([
func() { return i + cycle; }, func() { return i + cycle; },
{"cycle": cycle, "item": i} {"cycle": cycle, "item": i}
]); ]);

View File

@ -11,16 +11,16 @@ for (var i = 0; i < 50000; i++) {
var capturedArray = [i, i+1, i+2, "data" + i]; var capturedArray = [i, i+1, i+2, "data" + i];
var capturedDict = {"id": i, "values": [i*2, i*3]}; var capturedDict = {"id": i, "values": [i*2, i*3]};
push(funcWithCollections, func() { funcWithCollections.push(func() {
// Capture both collections // Capture both collections
var localArray = capturedArray; var localArray = capturedArray;
var localDict = capturedDict; var localDict = capturedDict;
return func() { return func() {
return len(localArray) + len(localDict); return localArray.len() + localDict.len();
}; };
}); });
} }
print("Created " + len(funcWithCollections) + " functions with collection captures"); print("Created " + funcWithCollections.len() + " functions with collection captures");
print("Memory: " + memoryUsage() + " MB"); print("Memory: " + memoryUsage() + " MB");
input("Press Enter to clear function collections..."); input("Press Enter to clear function collections...");
funcWithCollections = []; funcWithCollections = [];
@ -31,7 +31,7 @@ input("Cleared. Check memory usage...");
print("Test 2: Collections with mixed content"); print("Test 2: Collections with mixed content");
var mixedContent = []; var mixedContent = [];
for (var i = 0; i < 30000; i++) { for (var i = 0; i < 30000; i++) {
push(mixedContent, [ mixedContent.push([
func() { return i; }, // Function func() { return i; }, // Function
[i, i+1, func() { return i*2; }], // Array with function [i, i+1, func() { return i*2; }], // Array with function
{"value": i, "func": func() { return i*3; }}, // Dict with function {"value": i, "func": func() { return i*3; }}, // Dict with function
@ -40,7 +40,7 @@ for (var i = 0; i < 30000; i++) {
i % 2 == 0 // Boolean i % 2 == 0 // Boolean
]); ]);
} }
print("Created " + len(mixedContent) + " mixed content collections"); print("Created " + mixedContent.len() + " mixed content collections");
print("Memory: " + memoryUsage() + " MB"); print("Memory: " + memoryUsage() + " MB");
input("Press Enter to clear mixed content..."); input("Press Enter to clear mixed content...");
mixedContent = none; mixedContent = none;
@ -58,9 +58,9 @@ for (var i = 0; i < 100000; i++) {
obj["array"] = [1, 2, 3]; obj["array"] = [1, 2, 3];
obj["array"][0] = func() { return i * 2; }; obj["array"][0] = func() { return i * 2; };
push(propObjects, obj); propObjects.push(obj);
} }
print("Created " + len(propObjects) + " objects with dynamic properties"); print("Created " + propObjects.len() + " objects with dynamic properties");
print("Memory: " + memoryUsage() + " MB"); print("Memory: " + memoryUsage() + " MB");
input("Press Enter to clear property objects..."); input("Press Enter to clear property objects...");
propObjects = "cleared"; propObjects = "cleared";
@ -71,20 +71,20 @@ input("Cleared. Check memory usage...");
print("Test 4: Type reassignment chains"); print("Test 4: Type reassignment chains");
var typeChains = []; var typeChains = [];
for (var i = 0; i < 200000; i++) { for (var i = 0; i < 200000; i++) {
push(typeChains, i); typeChains.push(i);
} }
print("Memory after number array: " + memoryUsage() + " MB"); print("Memory after number array: " + memoryUsage() + " MB");
input("Created number array. Press Enter to convert to functions..."); input("Created number array. Press Enter to convert to functions...");
// Reassign all elements to functions // Reassign all elements to functions
for (var i = 0; i < len(typeChains); i++) { for (var i = 0; i < typeChains.len(); i++) {
typeChains[i] = func() { return i; }; typeChains[i] = func() { return i; };
} }
print("Memory after function conversion: " + memoryUsage() + " MB"); print("Memory after function conversion: " + memoryUsage() + " MB");
input("Converted to functions. Press Enter to convert to dicts..."); input("Converted to functions. Press Enter to convert to dicts...");
// Reassign all elements to dicts // Reassign all elements to dicts
for (var i = 0; i < len(typeChains); i++) { for (var i = 0; i < typeChains.len(); i++) {
typeChains[i] = {"id": i, "func": func() { return i; }}; typeChains[i] = {"id": i, "func": func() { return i; }};
} }
print("Memory after dict conversion: " + memoryUsage() + " MB"); print("Memory after dict conversion: " + memoryUsage() + " MB");
@ -98,12 +98,12 @@ print("Test 5: Rapid allocation/deallocation");
for (var round = 0; round < 10; round++) { for (var round = 0; round < 10; round++) {
var temp = []; var temp = [];
for (var i = 0; i < 100000; i++) { for (var i = 0; i < 100000; i++) {
push(temp, [ temp.push([
func() { return i; }, func() { return i; },
{"data": [i, i+1, func() { return i*2; }]} {"data": [i, i+1, func() { return i*2; }]}
]); ]);
} }
print("Round " + round + ": Created " + len(temp) + " items, Memory: " + memoryUsage() + " MB"); print("Round " + round + ": Created " + temp.len() + " items, Memory: " + memoryUsage() + " MB");
temp = none; // Clear immediately temp = none; // Clear immediately
if (round % 2 == 1) print("After clear round " + round + ": " + memoryUsage() + " MB"); if (round % 2 == 1) print("After clear round " + round + ": " + memoryUsage() + " MB");
} }

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;
// Registers all builtin modules with the interpreter
void registerAllBuiltinModules(Interpreter& interpreter);

View File

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

View File

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

View File

@ -5,6 +5,7 @@
#include <string> #include <string>
#include "Lexer.h" #include "Lexer.h"
#include "Interpreter.h" #include "Interpreter.h"
#include "ModuleRegistry.h"
#include "helperFunctions/ShortHands.h" #include "helperFunctions/ShortHands.h"
#include "ErrorReporter.h" #include "ErrorReporter.h"
@ -20,10 +21,105 @@ public:
~Bob() = default; ~Bob() = default;
public: public:
// Embedding helpers (bridge to internal interpreter)
void registerModule(const std::string& name, std::function<void(ModuleRegistry::ModuleBuilder&)> init) {
if (interpreter) interpreter->registerModule(name, init);
else pendingConfigurators.push_back([name, init](Interpreter& I){ I.registerModule(name, init); });
}
void setBuiltinModulePolicy(bool allow) {
if (interpreter) interpreter->setBuiltinModulePolicy(allow);
else pendingConfigurators.push_back([allow](Interpreter& I){ I.setBuiltinModulePolicy(allow); });
}
void setBuiltinModuleAllowList(const std::vector<std::string>& allowed) {
if (interpreter) interpreter->setBuiltinModuleAllowList(allowed);
else pendingConfigurators.push_back([allowed](Interpreter& I){ I.setBuiltinModuleAllowList(allowed); });
}
void setBuiltinModuleDenyList(const std::vector<std::string>& denied) {
if (interpreter) interpreter->setBuiltinModuleDenyList(denied);
else pendingConfigurators.push_back([denied](Interpreter& I){ I.setBuiltinModuleDenyList(denied); });
}
bool defineGlobal(const std::string& name, const Value& v) {
if (interpreter) return interpreter->defineGlobalVar(name, v);
pendingConfigurators.push_back([name, v](Interpreter& I){ I.defineGlobalVar(name, v); });
return true;
}
bool tryGetGlobal(const std::string& name, Value& out) const { return interpreter ? interpreter->tryGetGlobalVar(name, out) : false; }
void runFile(const std::string& path); void runFile(const std::string& path);
void runPrompt(); void runPrompt();
bool evalFile(const std::string& path);
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 run(std::string source); void ensureInterpreter(bool interactive);
void applyPendingConfigs() {
if (!interpreter) return;
for (auto& f : pendingConfigurators) { f(*interpreter); }
pendingConfigurators.clear();
}
std::vector<std::function<void(Interpreter&)>> pendingConfigurators;
}; };

View File

@ -3,6 +3,7 @@
#include <iostream> #include <iostream>
#include <vector> #include <vector>
#include <bitset> #include <bitset>
#include <cctype>
inline std::vector<std::string> splitString(const std::string& input, const std::string& delimiter) { inline std::vector<std::string> splitString(const std::string& input, const std::string& delimiter) {
std::vector<std::string> tokens; std::vector<std::string> tokens;
@ -56,9 +57,9 @@ inline bool isHexDigit(char c) {
return (std::isdigit(c) || (std::isxdigit(c) && std::islower(c))); return (std::isdigit(c) || (std::isxdigit(c) && std::islower(c)));
} }
inline u_long binaryStringToLong(const std::string& binaryString) { inline unsigned long long binaryStringToLong(const std::string& binaryString) {
std::string binaryDigits = binaryString.substr(2); // Remove the '0b' prefix std::string binaryDigits = binaryString.substr(2); // Remove the '0b' prefix
u_long result = 0; unsigned long long result = 0;
for (char ch : binaryDigits) { for (char ch : binaryDigits) {
result <<= 1; result <<= 1;
result += (ch - '0'); result += (ch - '0');

View File

@ -34,6 +34,9 @@ private:
std::string currentFileName; std::string currentFileName;
std::vector<std::string> callStack; std::vector<std::string> callStack;
bool hadError = false; bool hadError = false;
// Support nested sources (e.g., eval of external files)
std::vector<std::vector<std::string>> sourceStack;
std::vector<std::string> fileNameStack;
public: public:
ErrorReporter() = default; ErrorReporter() = default;
@ -58,6 +61,11 @@ public:
void pushCallStack(const std::string& functionName); void pushCallStack(const std::string& functionName);
void popCallStack(); void popCallStack();
// Source push/pop for eval
void pushSource(const std::string& source, const std::string& fileName);
void popSource();
const std::string& getCurrentFileName() const { return currentFileName; }
private: private:
void displaySourceContext(int line, int column, const std::string& errorType, const std::string& message, const std::string& operator_ = "", bool showArrow = true); void displaySourceContext(int line, int column, const std::string& errorType, const std::string& message, const std::string& operator_ = "", bool showArrow = true);
void displayCallStack(const std::vector<std::string>& callStack); void displayCallStack(const std::vector<std::string>& callStack);

View File

@ -22,10 +22,12 @@ enum TokenType{
// Increment/decrement operators // Increment/decrement operators
PLUS_PLUS, MINUS_MINUS, PLUS_PLUS, MINUS_MINUS,
IDENTIFIER, STRING, NUMBER, BOOL, IDENTIFIER, STRING, NUMBER, KW_BOOL,
AND, OR, TRUE, FALSE, IF, ELSE, FUNCTION, FOR, AND, OR, TRUE, FALSE, IF, ELSE, FUNCTION, FOR,
WHILE, DO, VAR, CLASS, SUPER, THIS, NONE, RETURN, BREAK, CONTINUE, WHILE, DO, VAR, CLASS, EXTENDS, EXTENSION, SUPER, THIS, NONE, RETURN, BREAK, CONTINUE,
IMPORT, FROM, AS,
TRY, CATCH, FINALLY, THROW,
// Compound assignment operators // Compound assignment operators
PLUS_EQUAL, MINUS_EQUAL, STAR_EQUAL, SLASH_EQUAL, PERCENT_EQUAL, PLUS_EQUAL, MINUS_EQUAL, STAR_EQUAL, SLASH_EQUAL, PERCENT_EQUAL,
@ -51,10 +53,12 @@ inline std::string enum_mapping[] = {"OPEN_PAREN", "CLOSE_PAREN", "OPEN_BRACE",
"PLUS_PLUS", "MINUS_MINUS", "PLUS_PLUS", "MINUS_MINUS",
"IDENTIFIER", "STRING", "NUMBER", "BOOL", "IDENTIFIER", "STRING", "NUMBER", "KW_BOOL",
"AND", "OR", "TRUE", "FALSE", "IF", "ELSE", "FUNCTION", "FOR", "AND", "OR", "TRUE", "FALSE", "IF", "ELSE", "FUNCTION", "FOR",
"WHILE", "DO", "VAR", "CLASS", "SUPER", "THIS", "NONE", "RETURN", "BREAK", "CONTINUE", "WHILE", "DO", "VAR", "CLASS", "EXTENDS", "EXTENSION", "SUPER", "THIS", "NONE", "RETURN", "BREAK", "CONTINUE",
"IMPORT", "FROM", "AS",
"TRY", "CATCH", "FINALLY", "THROW",
// Compound assignment operators // Compound assignment operators
"PLUS_EQUAL", "MINUS_EQUAL", "STAR_EQUAL", "SLASH_EQUAL", "PERCENT_EQUAL", "PLUS_EQUAL", "MINUS_EQUAL", "STAR_EQUAL", "SLASH_EQUAL", "PERCENT_EQUAL",
@ -77,12 +81,21 @@ const std::map<std::string, TokenType> KEYWORDS {
{"do", DO}, {"do", DO},
{"var", VAR}, {"var", VAR},
{"class", CLASS}, {"class", CLASS},
{"extends", EXTENDS},
{"extension", EXTENSION},
{"super", SUPER}, {"super", SUPER},
{"this", THIS}, {"this", THIS},
{"none", NONE}, {"none", NONE},
{"return", RETURN}, {"return", RETURN},
{"break", BREAK}, {"break", BREAK},
{"continue", CONTINUE}, {"continue", CONTINUE},
{"import", IMPORT},
{"from", FROM},
{"as", AS},
{"try", TRY},
{"catch", CATCH},
{"finally", FINALLY},
{"throw", THROW},
}; };
struct Token struct Token

View File

@ -68,6 +68,12 @@ private:
std::shared_ptr<Stmt> continueStatement(); std::shared_ptr<Stmt> continueStatement();
std::shared_ptr<Stmt> declaration(); std::shared_ptr<Stmt> declaration();
std::shared_ptr<Stmt> classDeclaration();
std::shared_ptr<Stmt> extensionDeclaration();
std::shared_ptr<Stmt> tryStatement();
std::shared_ptr<Stmt> throwStatement();
std::shared_ptr<Stmt> importStatement();
std::shared_ptr<Stmt> fromImportStatement();
std::shared_ptr<Stmt> varDeclaration(); std::shared_ptr<Stmt> varDeclaration();

View File

@ -17,6 +17,8 @@ struct ForStmt;
struct BreakStmt; struct BreakStmt;
struct ContinueStmt; struct ContinueStmt;
struct AssignStmt; struct AssignStmt;
struct ClassStmt;
struct ExtensionStmt;
#include "ExecutionContext.h" #include "ExecutionContext.h"
@ -34,6 +36,12 @@ struct StmtVisitor
virtual void visitBreakStmt(const std::shared_ptr<BreakStmt>& stmt, ExecutionContext* context = nullptr) = 0; virtual void visitBreakStmt(const std::shared_ptr<BreakStmt>& stmt, ExecutionContext* context = nullptr) = 0;
virtual void visitContinueStmt(const std::shared_ptr<ContinueStmt>& stmt, ExecutionContext* context = nullptr) = 0; virtual void visitContinueStmt(const std::shared_ptr<ContinueStmt>& stmt, ExecutionContext* context = nullptr) = 0;
virtual void visitAssignStmt(const std::shared_ptr<AssignStmt>& stmt, ExecutionContext* context = nullptr) = 0; virtual void visitAssignStmt(const std::shared_ptr<AssignStmt>& stmt, ExecutionContext* context = nullptr) = 0;
virtual void visitClassStmt(const std::shared_ptr<ClassStmt>& stmt, ExecutionContext* context = nullptr) = 0;
virtual void visitExtensionStmt(const std::shared_ptr<ExtensionStmt>& stmt, ExecutionContext* context = nullptr) = 0;
virtual void visitTryStmt(const std::shared_ptr<struct TryStmt>& stmt, ExecutionContext* context = nullptr) = 0;
virtual void visitThrowStmt(const std::shared_ptr<struct ThrowStmt>& stmt, ExecutionContext* context = nullptr) = 0;
virtual void visitImportStmt(const std::shared_ptr<struct ImportStmt>& stmt, ExecutionContext* context = nullptr) = 0;
virtual void visitFromImportStmt(const std::shared_ptr<struct FromImportStmt>& stmt, ExecutionContext* context = nullptr) = 0;
}; };
struct Stmt : public std::enable_shared_from_this<Stmt> struct Stmt : public std::enable_shared_from_this<Stmt>
@ -43,6 +51,39 @@ struct Stmt : public std::enable_shared_from_this<Stmt>
virtual ~Stmt(){}; virtual ~Stmt(){};
}; };
struct ClassField {
Token name;
std::shared_ptr<Expr> initializer; // may be null
ClassField(Token name, std::shared_ptr<Expr> init) : name(name), initializer(init) {}
};
struct ClassStmt : Stmt {
const Token name;
bool hasParent;
Token parentName; // valid only if hasParent
std::vector<ClassField> fields;
std::vector<std::shared_ptr<FunctionStmt>> methods;
ClassStmt(Token name, bool hasParent, Token parentName, std::vector<ClassField> fields, std::vector<std::shared_ptr<FunctionStmt>> methods)
: name(name), hasParent(hasParent), parentName(parentName), fields(std::move(fields)), methods(std::move(methods)) {}
void accept(StmtVisitor* visitor, ExecutionContext* context = nullptr) override {
visitor->visitClassStmt(std::static_pointer_cast<ClassStmt>(shared_from_this()), context);
}
};
struct ExtensionStmt : Stmt {
const Token target;
std::vector<std::shared_ptr<FunctionStmt>> methods;
ExtensionStmt(Token target, std::vector<std::shared_ptr<FunctionStmt>> methods)
: target(target), methods(std::move(methods)) {}
void accept(StmtVisitor* visitor, ExecutionContext* context = nullptr) override {
visitor->visitExtensionStmt(std::static_pointer_cast<ExtensionStmt>(shared_from_this()), context);
}
};
struct BlockStmt : Stmt struct BlockStmt : Stmt
{ {
std::vector<std::shared_ptr<Stmt>> statements; std::vector<std::shared_ptr<Stmt>> statements;
@ -210,3 +251,55 @@ struct AssignStmt : Stmt
visitor->visitAssignStmt(std::static_pointer_cast<AssignStmt>(shared_from_this()), context); visitor->visitAssignStmt(std::static_pointer_cast<AssignStmt>(shared_from_this()), context);
} }
}; };
struct TryStmt : Stmt {
std::shared_ptr<Stmt> tryBlock;
Token catchVar; // IDENTIFIER or empty token if no catch
std::shared_ptr<Stmt> catchBlock; // may be null
std::shared_ptr<Stmt> finallyBlock; // may be null
TryStmt(std::shared_ptr<Stmt> t, Token cvar, std::shared_ptr<Stmt> cblk, std::shared_ptr<Stmt> fblk)
: tryBlock(t), catchVar(cvar), catchBlock(cblk), finallyBlock(fblk) {}
void accept(StmtVisitor* visitor, ExecutionContext* context = nullptr) override {
visitor->visitTryStmt(std::static_pointer_cast<TryStmt>(shared_from_this()), context);
}
};
struct ThrowStmt : Stmt {
const Token keyword;
std::shared_ptr<Expr> value;
ThrowStmt(Token kw, std::shared_ptr<Expr> v) : keyword(kw), value(v) {}
void accept(StmtVisitor* visitor, ExecutionContext* context = nullptr) override {
visitor->visitThrowStmt(std::static_pointer_cast<ThrowStmt>(shared_from_this()), context);
}
};
// import module [as alias]
struct ImportStmt : Stmt {
Token importToken; // IMPORT
Token moduleName; // IDENTIFIER
bool hasAlias = false;
Token alias; // IDENTIFIER if hasAlias
ImportStmt(Token kw, Token mod, bool ha, Token al)
: importToken(kw), moduleName(mod), hasAlias(ha), alias(al) {}
void accept(StmtVisitor* visitor, ExecutionContext* context = nullptr) override {
visitor->visitImportStmt(std::static_pointer_cast<ImportStmt>(shared_from_this()), context);
}
};
// from module import name [as alias], name2 ...
struct FromImportStmt : Stmt {
Token fromToken; // FROM
Token moduleName; // IDENTIFIER or STRING
struct ImportItem { Token name; bool hasAlias; Token alias; };
std::vector<ImportItem> items;
bool importAll = false; // true for: from module import *;
FromImportStmt(Token kw, Token mod, std::vector<ImportItem> 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<FromImportStmt>(shared_from_this()), context);
}
};

View File

@ -0,0 +1,35 @@
#pragma once
#include "Value.h"
#include "Lexer.h"
// Utility to compute the result of a compound assignment (e.g., +=, -=, etc.)
// Leaves error reporting to callers; throws std::runtime_error on unknown operator
inline Value computeCompoundAssignment(const Value& currentValue, TokenType opType, const Value& rhs) {
switch (opType) {
case PLUS_EQUAL:
return currentValue + rhs;
case MINUS_EQUAL:
return currentValue - rhs;
case STAR_EQUAL:
return currentValue * rhs;
case SLASH_EQUAL:
return currentValue / rhs;
case PERCENT_EQUAL:
return currentValue % rhs;
case BIN_AND_EQUAL:
return currentValue & rhs;
case BIN_OR_EQUAL:
return currentValue | rhs;
case BIN_XOR_EQUAL:
return currentValue ^ rhs;
case BIN_SLEFT_EQUAL:
return currentValue << rhs;
case BIN_SRIGHT_EQUAL:
return currentValue >> rhs;
default:
throw std::runtime_error("Unknown compound assignment operator");
}
}

View File

@ -3,6 +3,7 @@
#include <unordered_map> #include <unordered_map>
#include <string> #include <string>
#include <memory> #include <memory>
#include <unordered_set>
#include "Value.h" #include "Value.h"
#include "Lexer.h" #include "Lexer.h"
@ -29,6 +30,7 @@ public:
void setErrorReporter(ErrorReporter* reporter) { void setErrorReporter(ErrorReporter* reporter) {
errorReporter = reporter; errorReporter = reporter;
} }
ErrorReporter* getErrorReporter() const { return errorReporter; }
// Optimized define with inline // Optimized define with inline
inline void define(const std::string& name, const Value& value) { inline void define(const std::string& name, const Value& value) {
@ -41,23 +43,18 @@ public:
// Enhanced get with error reporting // Enhanced get with error reporting
Value get(const Token& name); Value get(const Token& name);
// Get by string name with error reporting
Value get(const std::string& name);
// Prune heavy containers in a snapshot to avoid capture cycles // Prune heavy containers in a snapshot to avoid capture cycles
void pruneForClosureCapture(); void pruneForClosureCapture();
std::shared_ptr<Environment> getParent() const { return parent; } std::shared_ptr<Environment> getParent() const { return parent; }
inline void clear() { variables.clear(); } // Export all variables (shallow copy) for module namespace
std::unordered_map<std::string, Value> getAll() const { return variables; }
// Set parent environment for TCO environment reuse
inline void setParent(std::shared_ptr<Environment> newParent) {
parent = newParent;
}
private: private:
std::unordered_map<std::string, Value> variables; std::unordered_map<std::string, Value> variables;
std::shared_ptr<Environment> parent; std::shared_ptr<Environment> parent;
ErrorReporter* errorReporter; ErrorReporter* errorReporter;
std::unordered_set<std::string> constBindings;
}; };

View File

@ -7,4 +7,8 @@ struct ExecutionContext {
Value returnValue = NONE_VALUE; Value returnValue = NONE_VALUE;
bool shouldBreak = false; bool shouldBreak = false;
bool shouldContinue = false; bool shouldContinue = false;
bool hasThrow = false;
Value thrownValue = NONE_VALUE;
int throwLine = 0;
int throwColumn = 0;
}; };

View File

@ -38,6 +38,12 @@ public:
void visitBreakStmt(const std::shared_ptr<BreakStmt>& statement, ExecutionContext* context = nullptr) override; void visitBreakStmt(const std::shared_ptr<BreakStmt>& statement, ExecutionContext* context = nullptr) override;
void visitContinueStmt(const std::shared_ptr<ContinueStmt>& statement, ExecutionContext* context = nullptr) override; void visitContinueStmt(const std::shared_ptr<ContinueStmt>& statement, ExecutionContext* context = nullptr) override;
void visitAssignStmt(const std::shared_ptr<AssignStmt>& statement, ExecutionContext* context = nullptr) override; void visitAssignStmt(const std::shared_ptr<AssignStmt>& statement, ExecutionContext* context = nullptr) override;
void visitClassStmt(const std::shared_ptr<ClassStmt>& statement, ExecutionContext* context = nullptr) override;
void visitExtensionStmt(const std::shared_ptr<ExtensionStmt>& statement, ExecutionContext* context = nullptr) override;
void visitTryStmt(const std::shared_ptr<TryStmt>& statement, ExecutionContext* context = nullptr) override;
void visitThrowStmt(const std::shared_ptr<ThrowStmt>& statement, ExecutionContext* context = nullptr) override;
void visitImportStmt(const std::shared_ptr<ImportStmt>& statement, ExecutionContext* context = nullptr) override;
void visitFromImportStmt(const std::shared_ptr<FromImportStmt>& statement, ExecutionContext* context = nullptr) override;
private: private:
void execute(const std::shared_ptr<Stmt>& statement, ExecutionContext* context); void execute(const std::shared_ptr<Stmt>& statement, ExecutionContext* context);

View File

@ -1,15 +1,4 @@
#pragma once #pragma once
#include "Expression.h"
#include "Statement.h"
#include "helperFunctions/ShortHands.h"
#include "TypeWrapper.h"
#include "Environment.h"
#include "Value.h"
#include "BobStdLib.h"
#include "ErrorReporter.h"
#include "ExecutionContext.h"
#include "RuntimeDiagnostics.h"
#include <vector> #include <vector>
#include <memory> #include <memory>
#include <unordered_map> #include <unordered_map>
@ -17,6 +6,24 @@
#include <optional> #include <optional>
#include <functional> #include <functional>
#include "Value.h"
#include "TypeWrapper.h"
#include "RuntimeDiagnostics.h"
#include "ModuleRegistry.h"
#include <unordered_set>
struct Expr;
struct Stmt;
struct Environment;
struct BuiltinFunction;
struct Function;
struct Thunk;
class ErrorReporter;
struct ExecutionContext;
struct CallExpr;
// Forward declaration // Forward declaration
class Evaluator; class Evaluator;
@ -62,17 +69,41 @@ private:
std::vector<std::shared_ptr<BuiltinFunction>> builtinFunctions; std::vector<std::shared_ptr<BuiltinFunction>> builtinFunctions;
std::vector<std::shared_ptr<Function>> functions; std::vector<std::shared_ptr<Function>> functions;
std::vector<std::shared_ptr<Thunk>> thunks; // Store thunks to prevent memory leaks std::vector<std::shared_ptr<Thunk>> thunks; // Store thunks to prevent memory leaks
// Global extension registries
std::unordered_map<std::string, std::unordered_map<std::string, std::shared_ptr<Function>>> classExtensions;
std::unordered_map<std::string, std::unordered_map<std::string, std::shared_ptr<Function>>> builtinExtensions; // keys: "string","array","dict","any"
std::unordered_map<std::string, std::string> classParents; // child -> parent
std::unordered_map<std::string, std::unordered_map<std::string, Value>> classTemplates; // className -> template dict
// Field initializers per class in source order (to evaluate across inheritance chain)
std::unordered_map<std::string, std::vector<std::pair<std::string, std::shared_ptr<Expr>>>> classFieldInitializers; // className -> [(field, expr)]
ErrorReporter* errorReporter; ErrorReporter* errorReporter;
bool inThunkExecution = false; bool inThunkExecution = false;
// Automatic cleanup tracking // Automatic cleanup tracking
int functionCreationCount = 0;
int thunkCreationCount = 0; int thunkCreationCount = 0;
static const int CLEANUP_THRESHOLD = 1000000; static const int CLEANUP_THRESHOLD = 10000;
RuntimeDiagnostics diagnostics; // Utility functions for runtime operations RuntimeDiagnostics diagnostics; // Utility functions for runtime operations
std::unique_ptr<Evaluator> evaluator; std::unique_ptr<Evaluator> evaluator;
std::unique_ptr<Executor> executor; std::unique_ptr<Executor> executor;
// Module cache: module key -> module dict value
std::unordered_map<std::string, Value> moduleCache;
// Builtin module registry
ModuleRegistry builtinModules;
// Import policy flags
bool allowFileImports = true;
bool preferFileOverBuiltin = true;
bool allowBuiltinImports = true;
std::vector<std::string> moduleSearchPaths; // e.g., BOBPATH
// Pending throw propagation from expression evaluation
bool hasPendingThrow = false;
Value pendingThrow = NONE_VALUE;
int pendingThrowLine = 0;
int pendingThrowColumn = 0;
int lastErrorLine = 0;
int lastErrorColumn = 0;
int tryDepth = 0;
bool inlineErrorReported = false;
public: public:
explicit Interpreter(bool isInteractive); explicit Interpreter(bool isInteractive);
@ -93,25 +124,75 @@ public:
bool isInteractiveMode() const; bool isInteractiveMode() const;
std::shared_ptr<Environment> getEnvironment(); std::shared_ptr<Environment> getEnvironment();
void setEnvironment(std::shared_ptr<Environment> env); void setEnvironment(std::shared_ptr<Environment> env);
void addThunk(std::shared_ptr<Thunk> thunk); ErrorReporter* getErrorReporter() const { return errorReporter; }
void addFunction(std::shared_ptr<Function> function); void addFunction(std::shared_ptr<Function> function);
void reportError(int line, int column, const std::string& errorType, const std::string& message, const std::string& lexeme = ""); void reportError(int line, int column, const std::string& errorType, const std::string& message, const std::string& lexeme = "");
void addBuiltinFunction(std::shared_ptr<BuiltinFunction> func); void addBuiltinFunction(std::shared_ptr<BuiltinFunction> func);
void cleanupUnusedFunctions(); void cleanupUnusedFunctions();
void cleanupUnusedThunks(); void cleanupUnusedThunks();
void forceCleanup(); void forceCleanup();
// Extension APIs
void registerExtension(const std::string& targetName, const std::string& methodName, std::shared_ptr<Function> fn);
std::shared_ptr<Function> lookupExtension(const std::string& targetName, const std::string& methodName);
void registerClass(const std::string& className, const std::string& parentName);
std::string getParentClass(const std::string& className) const;
void setClassTemplate(const std::string& className, const std::unordered_map<std::string, Value>& tmpl);
bool getClassTemplate(const std::string& className, std::unordered_map<std::string, Value>& out) const;
std::unordered_map<std::string, Value> buildMergedTemplate(const std::string& className) const;
// Field initializer APIs
void setClassFieldInitializers(const std::string& className, const std::vector<std::pair<std::string, std::shared_ptr<Expr>>>& inits) { classFieldInitializers[className] = inits; }
bool getClassFieldInitializers(const std::string& className, std::vector<std::pair<std::string, std::shared_ptr<Expr>>>& out) const {
auto it = classFieldInitializers.find(className);
if (it == classFieldInitializers.end()) return false; out = it->second; return true;
}
void addStdLibFunctions();
// Module APIs
Value importModule(const std::string& spec, int line, int column); // returns module dict
bool fromImport(const std::string& spec, const std::vector<std::pair<std::string, std::string>>& items, int line, int column); // name->alias
void setModulePolicy(bool allowFiles, bool preferFiles, const std::vector<std::string>& searchPaths);
void setBuiltinModulePolicy(bool allowBuiltins) { allowBuiltinImports = allowBuiltins; builtinModules.setPolicy(allowBuiltins); }
void setBuiltinModuleAllowList(const std::vector<std::string>& allowed) { builtinModules.setAllowList(allowed); }
void setBuiltinModuleDenyList(const std::vector<std::string>& denied) { builtinModules.setDenyList(denied); }
void registerBuiltinModule(const std::string& name, std::function<Value(Interpreter&)> factory) { builtinModules.registerFactory(name, std::move(factory)); }
// Simple module registration API
using ModuleBuilder = ModuleRegistry::ModuleBuilder;
void registerModule(const std::string& name, std::function<void(ModuleBuilder&)> init) {
builtinModules.registerModule(name, std::move(init));
}
// Global environment helpers
bool defineGlobalVar(const std::string& name, const Value& value);
bool tryGetGlobalVar(const std::string& name, Value& out) const;
// Throw propagation helpers
void setPendingThrow(const Value& v, int line = 0, int column = 0) { hasPendingThrow = true; pendingThrow = v; pendingThrowLine = line; pendingThrowColumn = column; }
bool consumePendingThrow(Value& out, int* lineOut = nullptr, int* colOut = nullptr) { if (!hasPendingThrow) return false; out = pendingThrow; if (lineOut) *lineOut = pendingThrowLine; if (colOut) *colOut = pendingThrowColumn; hasPendingThrow = false; pendingThrow = NONE_VALUE; pendingThrowLine = 0; pendingThrowColumn = 0; return true; }
// Try tracking
void enterTry() { tryDepth++; }
void exitTry() { if (tryDepth > 0) tryDepth--; }
bool isInTry() const { return tryDepth > 0; }
void markInlineErrorReported() { inlineErrorReported = true; }
bool hasInlineErrorReported() const { return inlineErrorReported; }
void clearInlineErrorReported() { inlineErrorReported = false; }
bool hasReportedError() const;
// Last error site tracking
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<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; }
// Function creation count management
void incrementFunctionCreationCount();
int getFunctionCreationCount() const;
void resetFunctionCreationCount();
int getCleanupThreshold() const;
// Public access for Evaluator
bool& getInThunkExecutionRef() { return inThunkExecution; }
private: private:
Value evaluateWithoutTrampoline(const std::shared_ptr<Expr>& expr);
void addStdLibFunctions();
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,14 @@
// ModuleDef.h
#pragma once
#include <memory>
#include <string>
#include <unordered_map>
#include "Value.h"
struct Module {
std::string name;
std::shared_ptr<std::unordered_map<std::string, Value>> exports;
};

View File

@ -0,0 +1,70 @@
#pragma once
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <functional>
#include <memory>
#include "TypeWrapper.h" // BuiltinFunction, Value
class Interpreter; // fwd
class ModuleRegistry {
public:
struct ModuleBuilder {
std::string moduleName;
Interpreter& interpreterRef;
std::unordered_map<std::string, Value> exports;
ModuleBuilder(const std::string& n, Interpreter& i) : moduleName(n), interpreterRef(i) {}
void fn(const std::string& name, std::function<Value(std::vector<Value>, int, int)> func) {
exports[name] = Value(std::make_shared<BuiltinFunction>(name, func));
}
void val(const std::string& name, const Value& v) { exports[name] = v; }
};
using Factory = std::function<Value(Interpreter&)>;
void registerFactory(const std::string& name, Factory factory) {
factories[name] = std::move(factory);
}
void registerModule(const std::string& name, std::function<void(ModuleBuilder&)> init) {
registerFactory(name, [name, init](Interpreter& I) -> Value {
ModuleBuilder b(name, I);
init(b);
auto m = std::make_shared<Module>(name, b.exports);
return Value(m);
});
}
bool has(const std::string& name) const {
auto it = factories.find(name);
if (it == factories.end()) return false;
// Respect policy for presence checks to optionally cloak denied modules
if (!allowBuiltins) return false;
if (!allowList.empty() && allowList.find(name) == allowList.end()) return false;
if (denyList.find(name) != denyList.end()) return false;
return true;
}
Value create(const std::string& name, Interpreter& I) const {
auto it = factories.find(name);
if (it == factories.end()) return NONE_VALUE;
if (!allowBuiltins) return NONE_VALUE;
if (!allowList.empty() && allowList.find(name) == allowList.end()) return NONE_VALUE;
if (denyList.find(name) != denyList.end()) return NONE_VALUE;
return it->second(I);
}
void setPolicy(bool allow) { allowBuiltins = allow; }
void setAllowList(const std::vector<std::string>& allowed) { allowList = std::unordered_set<std::string>(allowed.begin(), allowed.end()); }
void setDenyList(const std::vector<std::string>& denied) { denyList = std::unordered_set<std::string>(denied.begin(), denied.end()); }
private:
std::unordered_map<std::string, Factory> factories;
std::unordered_set<std::string> allowList;
std::unordered_set<std::string> denyList;
bool allowBuiltins = true;
};

View File

@ -28,8 +28,6 @@ public:
void cleanupUnusedFunctions(std::vector<std::shared_ptr<BuiltinFunction>>& functions); void cleanupUnusedFunctions(std::vector<std::shared_ptr<BuiltinFunction>>& functions);
void cleanupUnusedFunctions(std::vector<std::shared_ptr<Function>>& functions); void cleanupUnusedFunctions(std::vector<std::shared_ptr<Function>>& functions);
void cleanupUnusedThunks(std::vector<std::shared_ptr<Thunk>>& thunks); void cleanupUnusedThunks(std::vector<std::shared_ptr<Thunk>>& thunks);
void forceCleanup(std::vector<std::shared_ptr<BuiltinFunction>>& functions,
std::vector<std::shared_ptr<Thunk>>& thunks);
void forceCleanup(std::vector<std::shared_ptr<BuiltinFunction>>& builtinFunctions, void forceCleanup(std::vector<std::shared_ptr<BuiltinFunction>>& builtinFunctions,
std::vector<std::shared_ptr<Function>>& functions, std::vector<std::shared_ptr<Function>>& functions,
std::vector<std::shared_ptr<Thunk>>& thunks); std::vector<std::shared_ptr<Thunk>>& thunks);

View File

@ -10,51 +10,22 @@
struct Stmt; struct Stmt;
struct Environment; struct Environment;
struct Object struct Function
{
virtual ~Object(){};
};
struct Number : Object
{
double value;
explicit Number(double value) : value(value) {}
};
struct String : Object
{
std::string value;
explicit String(std::string str) : value(str) {}
~String(){
}
};
struct Boolean : Object
{
bool value;
explicit Boolean(bool value) : value(value) {}
};
struct None : public Object
{
};
struct Function : public Object
{ {
const std::string name; const std::string name;
const std::vector<std::string> params; const std::vector<std::string> params;
const std::vector<std::shared_ptr<Stmt>> body; const std::vector<std::shared_ptr<Stmt>> body;
const std::shared_ptr<Environment> closure; const std::shared_ptr<Environment> closure;
const std::string ownerClass; // empty for non-methods
Function(std::string name, std::vector<std::string> params, Function(std::string name, std::vector<std::string> params,
std::vector<std::shared_ptr<Stmt>> body, std::vector<std::shared_ptr<Stmt>> body,
std::shared_ptr<Environment> closure) std::shared_ptr<Environment> closure,
: name(name), params(params), body(body), closure(closure) {} std::string ownerClass = "")
: name(name), params(params), body(body), closure(closure), ownerClass(ownerClass) {}
}; };
struct BuiltinFunction : public Object struct BuiltinFunction
{ {
const std::string name; const std::string name;
const std::function<Value(std::vector<Value>, int, int)> func; const std::function<Value(std::vector<Value>, int, int)> func;

View File

@ -3,6 +3,7 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include <unordered_map> #include <unordered_map>
#include <memory>
#include <utility> #include <utility>
#include <cmath> #include <cmath>
#include <stdexcept> #include <stdexcept>
@ -13,6 +14,7 @@ struct Environment;
struct Function; struct Function;
struct BuiltinFunction; struct BuiltinFunction;
struct Thunk; struct Thunk;
struct Module;
// Type tags for the Value union // Type tags for the Value union
enum ValueType { enum ValueType {
@ -24,9 +26,12 @@ enum ValueType {
VAL_BUILTIN_FUNCTION, VAL_BUILTIN_FUNCTION,
VAL_THUNK, VAL_THUNK,
VAL_ARRAY, VAL_ARRAY,
VAL_DICT VAL_DICT,
VAL_MODULE
}; };
// (moved below Value)
// Tagged value system (like Lua) - no heap allocation for simple values // Tagged value system (like Lua) - no heap allocation for simple values
struct Value { struct Value {
union { union {
@ -37,6 +42,7 @@ struct Value {
std::string string_value; // Store strings outside the union for safety std::string string_value; // Store strings outside the union for safety
std::shared_ptr<std::vector<Value> > array_value; // Store arrays as shared_ptr for mutability std::shared_ptr<std::vector<Value> > array_value; // Store arrays as shared_ptr for mutability
std::shared_ptr<std::unordered_map<std::string, Value> > dict_value; // Store dictionaries as shared_ptr for mutability std::shared_ptr<std::unordered_map<std::string, Value> > dict_value; // Store dictionaries as shared_ptr for mutability
std::shared_ptr<Module> module_value; // Module object
// Store functions as shared_ptr for proper reference counting // Store functions as shared_ptr for proper reference counting
std::shared_ptr<Function> function; std::shared_ptr<Function> function;
@ -58,6 +64,7 @@ struct Value {
Value(std::vector<Value>&& arr) : type(ValueType::VAL_ARRAY), array_value(std::make_shared<std::vector<Value> >(std::move(arr))) {} Value(std::vector<Value>&& arr) : type(ValueType::VAL_ARRAY), array_value(std::make_shared<std::vector<Value> >(std::move(arr))) {}
Value(const std::unordered_map<std::string, Value>& dict) : type(ValueType::VAL_DICT), dict_value(std::make_shared<std::unordered_map<std::string, Value> >(dict)) {} Value(const std::unordered_map<std::string, Value>& dict) : type(ValueType::VAL_DICT), dict_value(std::make_shared<std::unordered_map<std::string, Value> >(dict)) {}
Value(std::unordered_map<std::string, Value>&& dict) : type(ValueType::VAL_DICT), dict_value(std::make_shared<std::unordered_map<std::string, Value> >(std::move(dict))) {} Value(std::unordered_map<std::string, Value>&& dict) : type(ValueType::VAL_DICT), dict_value(std::make_shared<std::unordered_map<std::string, Value> >(std::move(dict))) {}
Value(std::shared_ptr<Module> m) : type(ValueType::VAL_MODULE), module_value(std::move(m)) {}
// Destructor to clean up functions and thunks // Destructor to clean up functions and thunks
~Value() { ~Value() {
@ -70,7 +77,7 @@ struct Value {
// Move constructor // Move constructor
Value(Value&& other) noexcept Value(Value&& other) noexcept
: type(other.type), string_value(std::move(other.string_value)), array_value(std::move(other.array_value)), dict_value(std::move(other.dict_value)), : type(other.type), string_value(std::move(other.string_value)), array_value(std::move(other.array_value)), dict_value(std::move(other.dict_value)),
function(std::move(other.function)), builtin_function(std::move(other.builtin_function)), thunk(std::move(other.thunk)) { function(std::move(other.function)), builtin_function(std::move(other.builtin_function)), thunk(std::move(other.thunk)), module_value(std::move(other.module_value)) {
if (type != ValueType::VAL_STRING && type != ValueType::VAL_ARRAY && type != ValueType::VAL_DICT && if (type != ValueType::VAL_STRING && type != ValueType::VAL_ARRAY && type != ValueType::VAL_DICT &&
type != ValueType::VAL_FUNCTION && type != ValueType::VAL_BUILTIN_FUNCTION && type != ValueType::VAL_THUNK) { type != ValueType::VAL_FUNCTION && type != ValueType::VAL_BUILTIN_FUNCTION && type != ValueType::VAL_THUNK) {
number = other.number; // Copy the union number = other.number; // Copy the union
@ -94,6 +101,8 @@ struct Value {
builtin_function = std::move(other.builtin_function); builtin_function = std::move(other.builtin_function);
} else if (type == ValueType::VAL_THUNK) { } else if (type == ValueType::VAL_THUNK) {
thunk = std::move(other.thunk); thunk = std::move(other.thunk);
} else if (type == ValueType::VAL_MODULE) {
module_value = std::move(other.module_value);
} else { } else {
number = other.number; number = other.number;
} }
@ -117,6 +126,8 @@ struct Value {
builtin_function = other.builtin_function; // shared_ptr automatically handles sharing builtin_function = other.builtin_function; // shared_ptr automatically handles sharing
} else if (type == ValueType::VAL_THUNK) { } else if (type == ValueType::VAL_THUNK) {
thunk = other.thunk; // shared_ptr automatically handles sharing thunk = other.thunk; // shared_ptr automatically handles sharing
} else if (type == ValueType::VAL_MODULE) {
module_value = other.module_value; // shared module
} else { } else {
number = other.number; number = other.number;
} }
@ -146,6 +157,8 @@ struct Value {
builtin_function = other.builtin_function; // shared_ptr automatically handles sharing builtin_function = other.builtin_function; // shared_ptr automatically handles sharing
} else if (type == ValueType::VAL_THUNK) { } else if (type == ValueType::VAL_THUNK) {
thunk = other.thunk; // shared_ptr automatically handles sharing thunk = other.thunk; // shared_ptr automatically handles sharing
} else if (type == ValueType::VAL_MODULE) {
module_value = other.module_value;
} else { } else {
number = other.number; number = other.number;
} }
@ -161,6 +174,7 @@ struct Value {
inline bool isBuiltinFunction() const { return type == ValueType::VAL_BUILTIN_FUNCTION; } inline bool isBuiltinFunction() const { return type == ValueType::VAL_BUILTIN_FUNCTION; }
inline bool isArray() const { return type == ValueType::VAL_ARRAY; } inline bool isArray() const { return type == ValueType::VAL_ARRAY; }
inline bool isDict() const { return type == ValueType::VAL_DICT; } inline bool isDict() const { return type == ValueType::VAL_DICT; }
inline bool isModule() const { return type == ValueType::VAL_MODULE; }
inline bool isThunk() const { return type == ValueType::VAL_THUNK; } inline bool isThunk() const { return type == ValueType::VAL_THUNK; }
inline bool isNone() const { return type == ValueType::VAL_NONE; } inline bool isNone() const { return type == ValueType::VAL_NONE; }
@ -176,6 +190,7 @@ struct Value {
case ValueType::VAL_THUNK: return "thunk"; case ValueType::VAL_THUNK: return "thunk";
case ValueType::VAL_ARRAY: return "array"; case ValueType::VAL_ARRAY: return "array";
case ValueType::VAL_DICT: return "dict"; case ValueType::VAL_DICT: return "dict";
case ValueType::VAL_MODULE: return "module";
default: return "unknown"; default: return "unknown";
} }
} }
@ -198,6 +213,7 @@ struct Value {
inline std::unordered_map<std::string, Value>& asDict() { inline std::unordered_map<std::string, Value>& asDict() {
return *dict_value; return *dict_value;
} }
inline Module* asModule() const { return isModule() ? module_value.get() : nullptr; }
inline Function* asFunction() const { return isFunction() ? function.get() : nullptr; } inline Function* asFunction() const { return isFunction() ? function.get() : nullptr; }
inline BuiltinFunction* asBuiltinFunction() const { return isBuiltinFunction() ? builtin_function.get() : nullptr; } inline BuiltinFunction* asBuiltinFunction() const { return isBuiltinFunction() ? builtin_function.get() : nullptr; }
inline Thunk* asThunk() const { return isThunk() ? thunk.get() : nullptr; } inline Thunk* asThunk() const { return isThunk() ? thunk.get() : nullptr; }
@ -214,6 +230,7 @@ struct Value {
case ValueType::VAL_THUNK: return thunk != nullptr; case ValueType::VAL_THUNK: return thunk != nullptr;
case ValueType::VAL_ARRAY: return !array_value->empty(); case ValueType::VAL_ARRAY: return !array_value->empty();
case ValueType::VAL_DICT: return !dict_value->empty(); case ValueType::VAL_DICT: return !dict_value->empty();
case ValueType::VAL_MODULE: return module_value != nullptr;
default: return false; default: return false;
} }
} }
@ -296,6 +313,12 @@ struct Value {
result += "}"; result += "}";
return result; return result;
} }
case ValueType::VAL_MODULE: {
// Avoid accessing Module fields when it's still an incomplete type in some TUs.
// Delegate formatting to a small helper defined out-of-line in Value.cpp.
extern std::string formatModuleForToString(const std::shared_ptr<Module>&);
return formatModuleForToString(module_value);
}
default: return "unknown"; default: return "unknown";
} }
} }
@ -420,9 +443,16 @@ struct Value {
} }
}; };
// Define Module after Value so it can hold Value in exports without incomplete type issues
struct Module {
std::string name;
std::shared_ptr<std::unordered_map<std::string, Value>> exports;
Module() = default;
Module(const std::string& n, const std::unordered_map<std::string, Value>& dict)
: name(n), exports(std::make_shared<std::unordered_map<std::string, Value>>(dict)) {}
};
// Global constants for common values // Global constants for common values
extern const Value NONE_VALUE; extern const Value NONE_VALUE;
extern const Value TRUE_VALUE; extern const Value TRUE_VALUE;
extern const Value FALSE_VALUE; extern const Value FALSE_VALUE;
extern const Value ZERO_VALUE;
extern const Value ONE_VALUE;

View File

@ -1,434 +0,0 @@
#include "Evaluator.h"
#include "Interpreter.h"
#include "helperFunctions/HelperFunctions.h"
Evaluator::Evaluator(Interpreter* interpreter) : interpreter(interpreter) {}
Value Evaluator::visitLiteralExpr(const std::shared_ptr<LiteralExpr>& expr) {
if (expr->isNull) {
return NONE_VALUE;
}
if (expr->isNumber) {
double num;
if (expr->value.length() > 2 && expr->value[0] == '0' && expr->value[1] == 'b') {
num = binaryStringToLong(expr->value);
} else {
num = std::stod(expr->value);
}
return Value(num);
}
if (expr->isBoolean) {
if (expr->value == "true") return TRUE_VALUE;
if (expr->value == "false") return FALSE_VALUE;
}
return Value(expr->value);
}
Value Evaluator::visitGroupingExpr(const std::shared_ptr<GroupingExpr>& expression) {
return interpreter->evaluate(expression->expression);
}
Value Evaluator::visitUnaryExpr(const std::shared_ptr<UnaryExpr>& expression)
{
Value right = interpreter->evaluate(expression->right);
switch (expression->oper.type) {
case MINUS:
if (!right.isNumber()) {
interpreter->reportError(expression->oper.line, expression->oper.column, "Runtime Error",
"Operand must be a number when using: " + expression->oper.lexeme, expression->oper.lexeme);
throw std::runtime_error("Operand must be a number when using: " + expression->oper.lexeme);
}
return Value(-right.asNumber());
case BANG:
return Value(!interpreter->isTruthy(right));
case BIN_NOT:
if (!right.isNumber()) {
interpreter->reportError(expression->oper.line, expression->oper.column, "Runtime Error",
"Operand must be a number when using: " + expression->oper.lexeme, expression->oper.lexeme);
throw std::runtime_error("Operand must be a number when using: " + expression->oper.lexeme);
}
return Value(static_cast<double>(~(static_cast<long>(right.asNumber()))));
default:
interpreter->reportError(expression->oper.line, expression->oper.column, "Runtime Error",
"Invalid unary operator: " + expression->oper.lexeme, expression->oper.lexeme);
throw std::runtime_error("Invalid unary operator: " + expression->oper.lexeme);
}
}
Value Evaluator::visitBinaryExpr(const std::shared_ptr<BinaryExpr>& expression) {
Value left = interpreter->evaluate(expression->left);
Value right = interpreter->evaluate(expression->right);
// Handle logical operators (AND, OR) - these work with any types
if (expression->oper.type == AND) {
return interpreter->isTruthy(left) ? right : left;
}
if (expression->oper.type == OR) {
return interpreter->isTruthy(left) ? left : right;
}
// Handle equality operators - these work with any types
if (expression->oper.type == DOUBLE_EQUAL || expression->oper.type == BANG_EQUAL) {
bool equal = interpreter->isEqual(left, right);
return Value(expression->oper.type == DOUBLE_EQUAL ? equal : !equal);
}
// Handle comparison operators - only work with numbers
if (expression->oper.type == GREATER || expression->oper.type == GREATER_EQUAL ||
expression->oper.type == LESS || expression->oper.type == LESS_EQUAL) {
if (left.isNumber() && right.isNumber()) {
double leftNum = left.asNumber();
double rightNum = right.asNumber();
switch (expression->oper.type) {
case GREATER: return Value(leftNum > rightNum);
case GREATER_EQUAL: return Value(leftNum >= rightNum);
case LESS: return Value(leftNum < rightNum);
case LESS_EQUAL: return Value(leftNum <= rightNum);
default: break; // Unreachable
}
}
// Error for non-number comparisons
std::string opName;
switch (expression->oper.type) {
case GREATER: opName = ">"; break;
case GREATER_EQUAL: opName = ">="; break;
case LESS: opName = "<"; break;
case LESS_EQUAL: opName = "<="; break;
default: break; // Unreachable
}
interpreter->reportError(expression->oper.line, expression->oper.column, "Runtime Error",
ErrorUtils::makeOperatorError(opName, left.getType(), right.getType()), opName);
throw std::runtime_error(ErrorUtils::makeOperatorError(opName, left.getType(), right.getType()));
}
// Handle all other operators using Value's operator overloads
try {
switch (expression->oper.type) {
case PLUS: return left + right;
case MINUS: return left - right;
case STAR: return left * right;
case SLASH: return left / right;
case PERCENT: return left % right;
case BIN_AND: return left & right;
case BIN_OR: return left | right;
case BIN_XOR: return left ^ right;
case BIN_SLEFT: return left << right;
case BIN_SRIGHT: return left >> right;
default:
interpreter->reportError(expression->oper.line, expression->oper.column, "Runtime Error",
"Unknown operator: " + expression->oper.lexeme, expression->oper.lexeme);
throw std::runtime_error("Unknown operator: " + expression->oper.lexeme);
}
} catch (const std::runtime_error& e) {
// The Value operators provide good error messages, just add context
interpreter->reportError(expression->oper.line, expression->oper.column, "Runtime Error",
e.what(), expression->oper.lexeme);
throw;
}
}
Value Evaluator::visitVarExpr(const std::shared_ptr<VarExpr>& expression)
{
return interpreter->getEnvironment()->get(expression->name);
}
Value Evaluator::visitIncrementExpr(const std::shared_ptr<IncrementExpr>& expression) {
// Get the current value of the operand
Value currentValue = interpreter->evaluate(expression->operand);
if (!currentValue.isNumber()) {
interpreter->reportError(expression->oper.line, expression->oper.column,
"Runtime Error", "Increment/decrement can only be applied to numbers.", "");
throw std::runtime_error("Increment/decrement can only be applied to numbers.");
}
double currentNum = currentValue.asNumber();
double newValue;
// Determine the operation based on the operator
if (expression->oper.type == PLUS_PLUS) {
newValue = currentNum + 1.0;
} else if (expression->oper.type == MINUS_MINUS) {
newValue = currentNum - 1.0;
} else {
interpreter->reportError(expression->oper.line, expression->oper.column,
"Runtime Error", "Invalid increment/decrement operator.", "");
throw std::runtime_error("Invalid increment/decrement operator.");
}
// Update the variable or array element
if (auto varExpr = std::dynamic_pointer_cast<VarExpr>(expression->operand)) {
interpreter->getEnvironment()->assign(varExpr->name, Value(newValue));
} else if (auto arrayExpr = std::dynamic_pointer_cast<ArrayIndexExpr>(expression->operand)) {
// Handle array indexing increment/decrement
Value array = interpreter->evaluate(arrayExpr->array);
Value index = interpreter->evaluate(arrayExpr->index);
if (!array.isArray()) {
interpreter->reportError(expression->oper.line, expression->oper.column,
"Runtime Error", "Can only index arrays", "");
throw std::runtime_error("Can only index arrays");
}
if (!index.isNumber()) {
interpreter->reportError(expression->oper.line, expression->oper.column,
"Runtime Error", "Array index must be a number", "");
throw std::runtime_error("Array index must be a number");
}
int idx = static_cast<int>(index.asNumber());
std::vector<Value>& arr = array.asArray();
if (idx < 0 || idx >= static_cast<int>(arr.size())) {
interpreter->reportError(arrayExpr->bracket.line, arrayExpr->bracket.column,
"Runtime Error", "Array index out of bounds", "");
throw std::runtime_error("Array index out of bounds");
}
// Update the array element
arr[idx] = Value(newValue);
} else {
interpreter->reportError(expression->oper.line, expression->oper.column,
"Runtime Error", "Increment/decrement can only be applied to variables or array elements.", "");
throw std::runtime_error("Increment/decrement can only be applied to variables or array elements.");
}
// Return the appropriate value based on prefix/postfix
if (expression->isPrefix) {
return Value(newValue); // Prefix: return new value
} else {
return currentValue; // Postfix: return old value
}
}
Value Evaluator::visitAssignExpr(const std::shared_ptr<AssignExpr>& expression) {
Value value = interpreter->evaluate(expression->value);
switch (expression->op.type) {
case PLUS_EQUAL:
case MINUS_EQUAL:
case STAR_EQUAL:
case SLASH_EQUAL:
case PERCENT_EQUAL:
case BIN_AND_EQUAL:
case BIN_OR_EQUAL:
case BIN_XOR_EQUAL:
case BIN_SLEFT_EQUAL:
case BIN_SRIGHT_EQUAL: {
Value currentValue = interpreter->getEnvironment()->get(expression->name);
// ... (rest of compound assignment logic) ...
break;
}
default:
break;
}
interpreter->getEnvironment()->assign(expression->name, value);
return value;
}
Value Evaluator::visitTernaryExpr(const std::shared_ptr<TernaryExpr>& expression) {
Value condition = interpreter->evaluate(expression->condition);
if (interpreter->isTruthy(condition)) {
return interpreter->evaluate(expression->thenExpr);
} else {
return interpreter->evaluate(expression->elseExpr);
}
}
Value Evaluator::visitCallExpr(const std::shared_ptr<CallExpr>& expression) {
Value callee = expression->callee->accept(this);
std::vector<Value> arguments;
for (const auto& argument : expression->arguments) {
arguments.push_back(argument->accept(this));
}
if (callee.isFunction()) {
Function* function = callee.asFunction();
// Check arity
if (arguments.size() != function->params.size()) {
interpreter->reportError(expression->paren.line, expression->paren.column, "Runtime Error",
"Expected " + std::to_string(function->params.size()) + " arguments but got " +
std::to_string(arguments.size()) + ".", "");
throw std::runtime_error("Wrong number of arguments.");
}
// Create new environment for function call
auto environment = std::make_shared<Environment>(function->closure);
for (size_t i = 0; i < function->params.size(); i++) {
environment->define(function->params[i], arguments[i]);
}
// Execute function body
auto previous = interpreter->getEnvironment();
interpreter->setEnvironment(environment);
ExecutionContext context;
context.isFunctionBody = true;
try {
for (const auto& stmt : function->body) {
interpreter->execute(stmt, &context);
if (context.hasReturn) {
interpreter->setEnvironment(previous);
return context.returnValue;
}
}
} catch (...) {
interpreter->setEnvironment(previous);
throw;
}
interpreter->setEnvironment(previous);
return NONE_VALUE;
} else if (callee.isBuiltinFunction()) {
BuiltinFunction* builtinFunction = callee.asBuiltinFunction();
return builtinFunction->func(arguments, expression->paren.line, expression->paren.column);
} else {
interpreter->reportError(expression->paren.line, expression->paren.column, "Runtime Error",
"Can only call functions and classes.", "");
throw std::runtime_error("Can only call functions and classes.");
}
}
Value Evaluator::visitArrayLiteralExpr(const std::shared_ptr<ArrayLiteralExpr>& expr) {
std::vector<Value> elements;
for (const auto& element : expr->elements) {
elements.push_back(interpreter->evaluate(element));
}
return Value(elements);
}
Value Evaluator::visitArrayIndexExpr(const std::shared_ptr<ArrayIndexExpr>& expr) {
Value array = expr->array->accept(this);
Value index = expr->index->accept(this);
if (array.isArray()) {
// Handle array indexing
if (!index.isNumber()) {
interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error",
"Array index must be a number", "");
throw std::runtime_error("Array index must be a number");
}
int idx = static_cast<int>(index.asNumber());
const std::vector<Value>& arr = array.asArray();
if (idx < 0 || idx >= static_cast<int>(arr.size())) {
interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error",
"Array index out of bounds", "");
throw std::runtime_error("Array index out of bounds");
}
return arr[idx];
} else if (array.isDict()) {
// Handle dictionary indexing
if (!index.isString()) {
interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error",
"Dictionary key must be a string", "");
throw std::runtime_error("Dictionary key must be a string");
}
std::string key = index.asString();
const std::unordered_map<std::string, Value>& dict = array.asDict();
auto it = dict.find(key);
if (it != dict.end()) {
return it->second;
} else {
return NONE_VALUE; // Return none for missing keys
}
} else {
interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error",
"Can only index arrays and dictionaries", "");
throw std::runtime_error("Can only index arrays and dictionaries");
}
}
Value Evaluator::visitArrayAssignExpr(const std::shared_ptr<ArrayAssignExpr>& expr) {
Value array = expr->array->accept(this);
Value index = expr->index->accept(this);
Value value = expr->value->accept(this);
if (array.isArray()) {
// Handle array assignment
if (!index.isNumber()) {
interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error",
"Array index must be a number", "");
throw std::runtime_error("Array index must be a number");
}
int idx = static_cast<int>(index.asNumber());
std::vector<Value>& arr = array.asArray();
if (idx < 0 || idx >= static_cast<int>(arr.size())) {
interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error",
"Array index out of bounds", "");
throw std::runtime_error("Array index out of bounds");
}
arr[idx] = value;
return value;
} else if (array.isDict()) {
// Handle dictionary assignment
if (!index.isString()) {
interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error",
"Dictionary key must be a string", "");
throw std::runtime_error("Dictionary key must be a string");
}
std::string key = index.asString();
std::unordered_map<std::string, Value>& dict = array.asDict();
dict[key] = value;
return value;
} else {
interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error",
"Can only assign to array or dictionary elements", "");
throw std::runtime_error("Can only assign to array or dictionary elements");
}
}
Value Evaluator::visitDictLiteralExpr(const std::shared_ptr<DictLiteralExpr>& expr) {
std::unordered_map<std::string, Value> dict;
for (const auto& pair : expr->pairs) {
Value value = interpreter->evaluate(pair.second);
dict[pair.first] = value;
}
return Value(dict);
}
Value Evaluator::visitFunctionExpr(const std::shared_ptr<FunctionExpr>& expression) {
std::vector<std::string> paramNames;
for (const Token& param : expression->params) {
paramNames.push_back(param.lexeme);
}
auto function = std::make_shared<Function>("", paramNames, expression->body, interpreter->getEnvironment());
interpreter->addFunction(function);
return Value(function);
}

View File

@ -1,245 +0,0 @@
#include "Executor.h"
#include "Evaluator.h"
#include "Interpreter.h"
#include <iostream>
Executor::Executor(Interpreter* interpreter, Evaluator* evaluator)
: interpreter(interpreter), evaluator(evaluator) {}
Executor::~Executor() {}
void Executor::interpret(const std::vector<std::shared_ptr<Stmt>>& statements) {
for (const auto& statement : statements) {
execute(statement, nullptr);
}
}
void Executor::execute(const std::shared_ptr<Stmt>& statement, ExecutionContext* context) {
statement->accept(this, context);
}
void Executor::executeBlock(const std::vector<std::shared_ptr<Stmt>>& statements, std::shared_ptr<Environment> env, ExecutionContext* context) {
std::shared_ptr<Environment> previous = interpreter->getEnvironment();
interpreter->setEnvironment(env);
for (const auto& statement : statements) {
execute(statement, context);
if (context && (context->hasReturn || context->shouldBreak || context->shouldContinue)) {
interpreter->setEnvironment(previous);
return;
}
}
interpreter->setEnvironment(previous);
}
void Executor::visitBlockStmt(const std::shared_ptr<BlockStmt>& statement, ExecutionContext* context) {
auto newEnv = std::make_shared<Environment>(interpreter->getEnvironment());
executeBlock(statement->statements, newEnv, context);
}
void Executor::visitExpressionStmt(const std::shared_ptr<ExpressionStmt>& statement, ExecutionContext* context) {
Value value = statement->expression->accept(evaluator);
if (interpreter->isInteractiveMode())
std::cout << "\u001b[38;5;8m[" << interpreter->stringify(value) << "]\u001b[38;5;15m\n";
}
void Executor::visitVarStmt(const std::shared_ptr<VarStmt>& statement, ExecutionContext* context) {
Value value = NONE_VALUE;
if (statement->initializer != nullptr) {
value = statement->initializer->accept(evaluator);
}
interpreter->getEnvironment()->define(statement->name.lexeme, value);
}
void Executor::visitFunctionStmt(const std::shared_ptr<FunctionStmt>& statement, ExecutionContext* context) {
std::vector<std::string> paramNames;
for (const Token& param : statement->params) {
paramNames.push_back(param.lexeme);
}
auto function = std::make_shared<Function>(statement->name.lexeme,
paramNames,
statement->body,
interpreter->getEnvironment());
interpreter->addFunction(function);
interpreter->getEnvironment()->define(statement->name.lexeme, Value(function));
}
void Executor::visitReturnStmt(const std::shared_ptr<ReturnStmt>& statement, ExecutionContext* context) {
Value value = NONE_VALUE;
if (statement->value != nullptr) {
value = statement->value->accept(evaluator);
}
if (context && context->isFunctionBody) {
context->hasReturn = true;
context->returnValue = value;
}
}
void Executor::visitIfStmt(const std::shared_ptr<IfStmt>& statement, ExecutionContext* context) {
if (interpreter->isTruthy(statement->condition->accept(evaluator))) {
execute(statement->thenBranch, context);
} else if (statement->elseBranch != nullptr) {
execute(statement->elseBranch, context);
}
}
void Executor::visitWhileStmt(const std::shared_ptr<WhileStmt>& statement, ExecutionContext* context) {
ExecutionContext loopContext;
if (context) {
loopContext.isFunctionBody = context->isFunctionBody;
}
while (interpreter->isTruthy(statement->condition->accept(evaluator))) {
execute(statement->body, &loopContext);
if (loopContext.hasReturn) {
if (context) {
context->hasReturn = true;
context->returnValue = loopContext.returnValue;
}
break;
}
if (loopContext.shouldBreak) {
break;
}
if (loopContext.shouldContinue) {
loopContext.shouldContinue = false;
continue;
}
}
}
void Executor::visitDoWhileStmt(const std::shared_ptr<DoWhileStmt>& statement, ExecutionContext* context) {
ExecutionContext loopContext;
if (context) {
loopContext.isFunctionBody = context->isFunctionBody;
}
do {
execute(statement->body, &loopContext);
if (loopContext.hasReturn) {
if (context) {
context->hasReturn = true;
context->returnValue = loopContext.returnValue;
}
break;
}
if (loopContext.shouldBreak) {
break;
}
if (loopContext.shouldContinue) {
loopContext.shouldContinue = false;
continue;
}
} while (interpreter->isTruthy(statement->condition->accept(evaluator)));
}
void Executor::visitForStmt(const std::shared_ptr<ForStmt>& statement, ExecutionContext* context) {
if (statement->initializer != nullptr) {
execute(statement->initializer, context);
}
ExecutionContext loopContext;
if (context) {
loopContext.isFunctionBody = context->isFunctionBody;
}
while (statement->condition == nullptr || interpreter->isTruthy(statement->condition->accept(evaluator))) {
execute(statement->body, &loopContext);
if (loopContext.hasReturn) {
if (context) {
context->hasReturn = true;
context->returnValue = loopContext.returnValue;
}
break;
}
if (loopContext.shouldBreak) {
break;
}
if (loopContext.shouldContinue) {
loopContext.shouldContinue = false;
if (statement->increment != nullptr) {
statement->increment->accept(evaluator);
}
continue;
}
if (statement->increment != nullptr) {
statement->increment->accept(evaluator);
}
}
}
void Executor::visitBreakStmt(const std::shared_ptr<BreakStmt>& statement, ExecutionContext* context) {
if (context) {
context->shouldBreak = true;
}
}
void Executor::visitContinueStmt(const std::shared_ptr<ContinueStmt>& statement, ExecutionContext* context) {
if (context) {
context->shouldContinue = true;
}
}
void Executor::visitAssignStmt(const std::shared_ptr<AssignStmt>& statement, ExecutionContext* context) {
Value value = statement->value->accept(evaluator);
if (statement->op.type == EQUAL) {
interpreter->getEnvironment()->assign(statement->name, value);
} else {
// Handle compound assignment operators
Value currentValue = interpreter->getEnvironment()->get(statement->name);
Value newValue;
switch (statement->op.type) {
case PLUS_EQUAL:
newValue = currentValue + value;
break;
case MINUS_EQUAL:
newValue = currentValue - value;
break;
case STAR_EQUAL:
newValue = currentValue * value;
break;
case SLASH_EQUAL:
newValue = currentValue / value;
break;
case PERCENT_EQUAL:
newValue = currentValue % value;
break;
case BIN_AND_EQUAL:
newValue = currentValue & value;
break;
case BIN_OR_EQUAL:
newValue = currentValue | value;
break;
case BIN_XOR_EQUAL:
newValue = currentValue ^ value;
break;
case BIN_SLEFT_EQUAL:
newValue = currentValue << value;
break;
case BIN_SRIGHT_EQUAL:
newValue = currentValue >> value;
break;
default:
interpreter->reportError(statement->op.line, statement->op.column, "Runtime Error",
"Unknown assignment operator: " + statement->op.lexeme, "");
throw std::runtime_error("Unknown assignment operator: " + statement->op.lexeme);
}
interpreter->getEnvironment()->assign(statement->name, newValue);
}
}

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,132 @@
#include "os.h"
#include "Interpreter.h"
#include "Lexer.h"
#include <vector>
#include <string>
#include <filesystem>
#if defined(_WIN32)
#include <windows.h>
#include <direct.h>
#include <limits.h>
#include <io.h>
#ifndef PATH_MAX
#define PATH_MAX MAX_PATH
#endif
#else
#include <unistd.h>
#include <sys/types.h>
#include <dirent.h>
#endif
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 defined(_WIN32)
if (_getcwd(buf, sizeof(buf))) return Value(std::string(buf));
#else
if (getcwd(buf, sizeof(buf))) return Value(std::string(buf));
#endif
return NONE_VALUE;
});
m.fn("chdir", [](std::vector<Value> a, int, int) -> Value {
if (a.size() != 1 || !a[0].isString()) return Value(false);
#if defined(_WIN32)
int rc = ::_chdir(a[0].asString().c_str());
#else
int rc = ::chdir(a[0].asString().c_str());
#endif
return Value(rc == 0);
});
m.fn("getpid", [](std::vector<Value>, int, int) -> Value {
#if defined(_WIN32)
return Value(static_cast<double>(GetCurrentProcessId()));
#else
return Value(static_cast<double>(getpid()));
#endif
});
m.fn("getppid", [](std::vector<Value>, int, int) -> Value {
#if defined(_WIN32)
return NONE_VALUE; // not directly available; could use Toolhelp32Snapshot if needed
#else
return Value(static_cast<double>(getppid()));
#endif
});
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,63 @@
#include "path_module.h"
#include "Interpreter.h"
#include <filesystem>
#include <cctype>
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();
}
static bool isabs_impl(const std::string& s) {
#if defined(_WIN32)
if (s.size() >= 2 && (s[0] == '/' || s[0] == '\\')) return true; // root-relative on current drive
if (s.rfind("\\\\", 0) == 0) return true; // UNC path
if (s.size() >= 3 && std::isalpha(static_cast<unsigned char>(s[0])) && s[1] == ':' && (s[2] == '/' || s[2] == '\\')) return true; // C:\ or C:/
return false;
#else
return !s.empty() && s[0] == '/';
#endif
}
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());
std::string ext = p.has_extension() ? p.extension().generic_string() : std::string("");
fs::path basePath = p.has_extension() ? (p.parent_path() / p.stem()) : p;
return Value(std::vector<Value>{ Value(basePath.generic_string()), Value(ext) });
});
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(isabs_impl(a[0].asString()));
});
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

@ -0,0 +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
}

View File

@ -0,0 +1,99 @@
#include "sys.h"
#include "Interpreter.h"
#include "Environment.h"
#include "Lexer.h" // for Token and IDENTIFIER
#if defined(_WIN32)
#include <windows.h>
#else
#include <unistd.h>
#endif
#include <limits.h>
#include <cstdlib>
#include <cstring>
#include <vector>
// Platform-specific includes for memoryUsage()
#if defined(__APPLE__) && defined(__MACH__)
#include <mach/mach.h>
#elif defined(__linux__)
#include <fstream>
#include <sstream>
#elif defined(_WIN32)
#define NOMINMAX
#include <windows.h>
#include <psapi.h>
#endif
void registerSysModule(Interpreter& interpreter) {
interpreter.registerModule("sys", [](Interpreter::ModuleBuilder& m) {
Interpreter& I = m.interpreterRef;
m.fn("platform", [](std::vector<Value>, int, int) -> Value {
#if defined(_WIN32)
return Value(std::string("win32"));
#elif defined(__APPLE__)
return Value(std::string("darwin"));
#elif defined(__linux__)
return Value(std::string("linux"));
#else
return Value(std::string("unknown"));
#endif
});
m.fn("version", [](std::vector<Value>, int, int) -> Value { return Value(std::string("0.0.3")); });
// argv(): array of strings
m.fn("argv", [&I](std::vector<Value>, int, int) -> Value {
std::vector<Value> out;
for (const auto& s : I.getArgv()) out.push_back(Value(s));
return Value(out);
});
// executable(): absolute path to the running binary (host-provided)
m.fn("executable", [&I](std::vector<Value>, int, int) -> Value { return Value(I.getExecutablePath()); });
// modules(): read-only snapshot of module cache
m.fn("modules", [&I](std::vector<Value>, int, int) -> Value {
Value dictVal = Value(std::unordered_map<std::string, Value>{});
auto snapshot = I.getModuleCacheSnapshot();
return Value(snapshot);
});
// memoryUsage(): process RSS in MB (best effort per-platform)
m.fn("memoryUsage", [](std::vector<Value> a, int, int) -> Value {
if (!a.empty()) return NONE_VALUE;
size_t memoryBytes = 0;
#if defined(__APPLE__) && defined(__MACH__)
// macOS
struct mach_task_basic_info info;
mach_msg_type_number_t infoCount = MACH_TASK_BASIC_INFO_COUNT;
if (task_info(mach_task_self(), MACH_TASK_BASIC_INFO, (task_info_t)&info, &infoCount) == KERN_SUCCESS) {
memoryBytes = info.resident_size;
}
#elif defined(__linux__)
// Linux
std::ifstream statusFile("/proc/self/status");
std::string line;
while (std::getline(statusFile, line)) {
if (line.substr(0, 6) == "VmRSS:") {
std::istringstream iss(line);
std::string label, value, unit;
iss >> label >> value >> unit;
memoryBytes = std::stoull(value) * 1024; // KB -> bytes
break;
}
}
#elif defined(_WIN32)
// Windows
PROCESS_MEMORY_COUNTERS pmc;
if (GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof(pmc))) {
memoryBytes = pmc.WorkingSetSize;
}
#endif
double memoryMB = static_cast<double>(memoryBytes) / (1024.0 * 1024.0);
return Value(memoryMB);
});
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;
});
// 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

@ -3,35 +3,23 @@
#include "bob.h" #include "bob.h"
#include "Parser.h" #include "Parser.h"
void Bob::ensureInterpreter(bool interactive) {
if (!interpreter) interpreter = msptr(Interpreter)(interactive);
applyPendingConfigs();
}
void Bob::runFile(const std::string& path) void Bob::runFile(const std::string& path)
{ {
this->interpreter = msptr(Interpreter)(false); ensureInterpreter(false);
std::ifstream file = std::ifstream(path); interpreter->addStdLibFunctions();
if (!evalFile(path)) {
std::string source; std::cout << "Execution failed\n";
if(file.is_open()){
source = std::string(std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>());
} }
else
{
std::cout << "File not found\n";
return;
}
// Load source code into error reporter for context
errorReporter.loadSource(source, path);
// Connect error reporter to interpreter
interpreter->setErrorReporter(&errorReporter);
this->run(source);
} }
void Bob::runPrompt() void Bob::runPrompt()
{ {
this->interpreter = msptr(Interpreter)(true); ensureInterpreter(true);
std::cout << "Bob v" << VERSION << ", 2025\n"; std::cout << "Bob v" << VERSION << ", 2025\n";
while(true) while(true)
{ {
@ -46,50 +34,40 @@ void Bob::runPrompt()
// Reset error state before each REPL command // Reset error state before each REPL command
errorReporter.resetErrorState(); errorReporter.resetErrorState();
interpreter->addStdLibFunctions();
(void)evalString(line, "REPL");
}
}
// Load source code into error reporter for context bool Bob::evalFile(const std::string& path) {
errorReporter.loadSource(line, "REPL"); std::ifstream file(path);
if (!file.is_open()) return false;
// Connect error reporter to interpreter std::string src((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
errorReporter.loadSource(src, path);
interpreter->setErrorReporter(&errorReporter); interpreter->setErrorReporter(&errorReporter);
this->run(line);
}
}
void Bob::run(std::string source)
{
try { try {
// Connect error reporter to lexer
lexer.setErrorReporter(&errorReporter); lexer.setErrorReporter(&errorReporter);
auto tokens = lexer.Tokenize(src);
std::vector<Token> tokens = lexer.Tokenize(std::move(source));
Parser p(tokens); Parser p(tokens);
// Connect error reporter to parser
p.setErrorReporter(&errorReporter); p.setErrorReporter(&errorReporter);
auto statements = p.parse();
std::vector<sptr(Stmt)> statements = p.parse();
interpreter->interpret(statements); interpreter->interpret(statements);
} return true;
catch(std::exception &e) } catch (...) { return false; }
{ }
// Only suppress errors that have already been reported by the error reporter
if (errorReporter.hasReportedError()) {
return;
}
// For errors that weren't reported (like parser errors, undefined variables, etc.) bool Bob::evalString(const std::string& code, const std::string& filename) {
// print them normally errorReporter.loadSource(code, filename);
std::cout << "Error: " << e.what() << '\n'; interpreter->setErrorReporter(&errorReporter);
return; try {
} lexer.setErrorReporter(&errorReporter);
catch(const std::exception& e) auto tokens = lexer.Tokenize(code);
{ Parser p(tokens);
// Unknown error - report it since it wasn't handled by the interpreter p.setErrorReporter(&errorReporter);
errorReporter.reportError(0, 0, "Unknown Error", "An unknown error occurred: " + std::string(e.what())); auto statements = p.parse();
return; interpreter->interpret(statements);
} return true;
} catch (...) { return false; }
} }

View File

@ -2,13 +2,27 @@
// //
#include "bob.h" #include "bob.h"
#include "Interpreter.h"
int main(int argc, char* argv[]){ int main(int argc, char* argv[]){
Bob bobLang; Bob bobLang;
// Enable open preset (all builtins, file imports allowed)
bobLang.setSafetyPreset("open");
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 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

@ -56,6 +56,29 @@ void ErrorReporter::loadSource(const std::string& source, const std::string& fil
} }
} }
void ErrorReporter::pushSource(const std::string& source, const std::string& fileName) {
// Save current
sourceStack.push_back(sourceLines);
fileNameStack.push_back(currentFileName);
// Load new
loadSource(source, fileName);
}
void ErrorReporter::popSource() {
if (!sourceStack.empty()) {
sourceLines = sourceStack.back();
sourceStack.pop_back();
} else {
sourceLines.clear();
}
if (!fileNameStack.empty()) {
currentFileName = fileNameStack.back();
fileNameStack.pop_back();
} else {
currentFileName.clear();
}
}
void ErrorReporter::reportError(int line, int column, const std::string& errorType, const std::string& message, const std::string& operator_, bool showArrow) { void ErrorReporter::reportError(int line, int column, const std::string& errorType, const std::string& message, const std::string& operator_, bool showArrow) {
hadError = true; hadError = true;
displaySourceContext(line, column, errorType, message, operator_, showArrow); displaySourceContext(line, column, errorType, message, operator_, showArrow);
@ -75,6 +98,8 @@ void ErrorReporter::reportErrorWithContext(const ErrorContext& context) {
if (!context.fileName.empty()) { if (!context.fileName.empty()) {
std::cout << colorize("File: ", Colors::BOLD) << colorize(context.fileName, Colors::CYAN) << "\n"; std::cout << colorize("File: ", Colors::BOLD) << colorize(context.fileName, Colors::CYAN) << "\n";
} else if (!currentFileName.empty()) {
std::cout << colorize("File: ", Colors::BOLD) << colorize(currentFileName, Colors::CYAN) << "\n";
} }
std::cout << colorize("Location: ", Colors::BOLD) << colorize("Line " + std::to_string(context.line) + std::cout << colorize("Location: ", Colors::BOLD) << colorize("Line " + std::to_string(context.line) +

View File

@ -363,18 +363,14 @@ std::vector<Token> Lexer::Tokenize(std::string source){
} }
if(!isNotation) { if(!isNotation) {
if (!src.empty() && src[0] == '.') { // Only treat '.' as part of the number if followed by a digit
advance(); if (src.size() > 1 && src[0] == '.' && std::isdigit(src[1])) {
if (!src.empty() && std::isdigit(src[0])) { advance(); // consume '.'
num += '.'; num += '.';
while (!src.empty() && std::isdigit(src[0])) { while (!src.empty() && std::isdigit(src[0])) {
num += src[0]; num += src[0];
advance(); advance();
} }
} else {
throw std::runtime_error("LEXER: malformed number at: " + std::to_string(this->line));
}
} }
} }
else else

View File

@ -252,21 +252,35 @@ sptr(Expr) Parser::postfix()
{ {
sptr(Expr) expr = primary(); sptr(Expr) expr = primary();
// Check for postfix increment/decrement while (true) {
if (match({OPEN_PAREN})) {
expr = finishCall(expr);
continue;
}
if (match({OPEN_BRACKET})) {
expr = finishArrayIndex(expr);
continue;
}
if (match({DOT})) {
Token name = consume(IDENTIFIER, "Expected property name after '.'.");
expr = msptr(PropertyExpr)(expr, name);
continue;
}
if (match({PLUS_PLUS, MINUS_MINUS})) { if (match({PLUS_PLUS, MINUS_MINUS})) {
Token oper = previous(); Token oper = previous();
// Ensure the expression is a variable or array indexing
if (!std::dynamic_pointer_cast<VarExpr>(expr) && if (!std::dynamic_pointer_cast<VarExpr>(expr) &&
!std::dynamic_pointer_cast<ArrayIndexExpr>(expr)) { !std::dynamic_pointer_cast<ArrayIndexExpr>(expr) &&
!std::dynamic_pointer_cast<PropertyExpr>(expr)) {
if (errorReporter) { if (errorReporter) {
errorReporter->reportError(oper.line, oper.column, "Parse Error", errorReporter->reportError(oper.line, oper.column, "Parse Error",
"Postfix increment/decrement can only be applied to variables or array elements", ""); "Postfix increment/decrement can only be applied to variables or array elements", "");
} }
throw std::runtime_error("Postfix increment/decrement can only be applied to variables or array elements."); throw std::runtime_error("Postfix increment/decrement can only be applied to variables or array elements.");
} }
expr = msptr(IncrementExpr)(expr, oper, false);
return msptr(IncrementExpr)(expr, oper, false); // false = postfix continue;
}
break;
} }
return expr; return expr;
@ -281,8 +295,15 @@ sptr(Expr) Parser::primary()
if(match({NUMBER})) return msptr(LiteralExpr)(previous().lexeme, true, false, false); if(match({NUMBER})) return msptr(LiteralExpr)(previous().lexeme, true, false, false);
if(match({STRING})) return msptr(LiteralExpr)(previous().lexeme, false, false, false); if(match({STRING})) return msptr(LiteralExpr)(previous().lexeme, false, false, false);
if(match( {IDENTIFIER})) { if(match( {IDENTIFIER, THIS, SUPER})) {
return call(); Token ident = previous();
if (ident.type == THIS) {
return msptr(VarExpr)(Token{IDENTIFIER, "this", ident.line, ident.column});
}
if (ident.type == SUPER) {
return msptr(VarExpr)(Token{IDENTIFIER, "super", ident.line, ident.column});
}
return msptr(VarExpr)(ident);
} }
if(match({OPEN_PAREN})) if(match({OPEN_PAREN}))
@ -360,7 +381,13 @@ sptr(Expr) Parser::dictLiteral()
sptr(Expr) Parser::call() sptr(Expr) Parser::call()
{ {
sptr(Expr) expr = msptr(VarExpr)(previous()); Token ident = previous();
sptr(Expr) expr;
if (ident.type == THIS) {
expr = msptr(VarExpr)(Token{IDENTIFIER, "this", ident.line, ident.column});
} else {
expr = msptr(VarExpr)(ident);
}
while (true) { while (true) {
if (match({OPEN_PAREN})) { if (match({OPEN_PAREN})) {
@ -396,6 +423,8 @@ sptr(Stmt) Parser::declaration()
try{ try{
if(match({VAR})) return varDeclaration(); if(match({VAR})) return varDeclaration();
if(match({FUNCTION})) return functionDeclaration(); if(match({FUNCTION})) return functionDeclaration();
if(match({CLASS})) return classDeclaration();
if(match({EXTENSION})) return extensionDeclaration();
return statement(); return statement();
} }
catch(std::runtime_error& e) catch(std::runtime_error& e)
@ -405,6 +434,86 @@ sptr(Stmt) Parser::declaration()
} }
} }
sptr(Stmt) Parser::classDeclaration() {
Token name = consume(IDENTIFIER, "Expected class name.");
bool hasParent = false;
Token parentName{};
if (match({EXTENDS})) {
hasParent = true;
parentName = consume(IDENTIFIER, "Expected parent class name after 'extends'.");
}
consume(OPEN_BRACE, "Expected '{' after class declaration.");
std::vector<ClassField> fields;
std::vector<std::shared_ptr<FunctionStmt>> methods;
while (!check(CLOSE_BRACE) && !isAtEnd()) {
if (match({VAR})) {
Token fieldName = consume(IDENTIFIER, "Expected field name.");
std::shared_ptr<Expr> init = nullptr;
if (match({EQUAL})) {
init = expression();
}
consume(SEMICOLON, "Expected ';' after field declaration.");
fields.emplace_back(fieldName, init);
} else if (match({FUNCTION})) {
Token methodName = consume(IDENTIFIER, "Expected method name.");
consume(OPEN_PAREN, "Expected '(' after method name.");
std::vector<Token> parameters;
if (!check(CLOSE_PAREN)) {
do {
parameters.push_back(consume(IDENTIFIER, "Expected parameter name."));
} while (match({COMMA}));
}
consume(CLOSE_PAREN, "Expected ')' after parameters.");
consume(OPEN_BRACE, "Expected '{' before method body.");
enterFunction();
std::vector<std::shared_ptr<Stmt>> body = block();
exitFunction();
methods.push_back(msptr(FunctionStmt)(methodName, parameters, body));
} else {
if (errorReporter) {
errorReporter->reportError(peek().line, peek().column, "Parse Error", "Expected 'var' or 'func' in class body", "");
}
throw std::runtime_error("Invalid class member");
}
}
consume(CLOSE_BRACE, "Expected '}' after class body.");
return msptr(ClassStmt)(name, hasParent, parentName, fields, methods);
}
sptr(Stmt) Parser::extensionDeclaration() {
Token target = consume(IDENTIFIER, "Expected extension target (class/builtin/any).");
consume(OPEN_BRACE, "Expected '{' after extension target.");
std::vector<std::shared_ptr<FunctionStmt>> methods;
while (!check(CLOSE_BRACE) && !isAtEnd()) {
if (match({FUNCTION})) {
Token methodName = consume(IDENTIFIER, "Expected method name.");
consume(OPEN_PAREN, "Expected '(' after method name.");
std::vector<Token> parameters;
if (!check(CLOSE_PAREN)) {
do {
parameters.push_back(consume(IDENTIFIER, "Expected parameter name."));
} while (match({COMMA}));
}
consume(CLOSE_PAREN, "Expected ')' after parameters.");
consume(OPEN_BRACE, "Expected '{' before method body.");
enterFunction();
std::vector<std::shared_ptr<Stmt>> body = block();
exitFunction();
methods.push_back(msptr(FunctionStmt)(methodName, parameters, body));
} else {
if (errorReporter) {
errorReporter->reportError(peek().line, peek().column, "Parse Error", "Expected 'func' in extension body", "");
}
throw std::runtime_error("Invalid extension member");
}
}
consume(CLOSE_BRACE, "Expected '}' after extension body.");
return msptr(ExtensionStmt)(target, methods);
}
sptr(Stmt) Parser::varDeclaration() sptr(Stmt) Parser::varDeclaration()
{ {
Token name = consume(IDENTIFIER, "Expected variable name."); Token name = consume(IDENTIFIER, "Expected variable name.");
@ -477,6 +586,13 @@ std::shared_ptr<Expr> Parser::functionExpression() {
sptr(Stmt) Parser::statement() sptr(Stmt) Parser::statement()
{ {
if(match({RETURN})) return returnStatement(); if(match({RETURN})) return returnStatement();
if(match({IMPORT})) return importStatement();
// Fallback if lexer didn't classify keyword: detect by lexeme
if (check(IDENTIFIER) && peek().lexeme == "import") { advance(); return importStatement(); }
if(match({FROM})) return fromImportStatement();
if (check(IDENTIFIER) && peek().lexeme == "from") { advance(); return fromImportStatement(); }
if(match({TRY})) return tryStatement();
if(match({THROW})) return throwStatement();
if(match({IF})) return ifStatement(); if(match({IF})) return ifStatement();
if(match({DO})) return doWhileStatement(); if(match({DO})) return doWhileStatement();
if(match({WHILE})) return whileStatement(); if(match({WHILE})) return whileStatement();
@ -486,7 +602,7 @@ sptr(Stmt) Parser::statement()
if(match({OPEN_BRACE})) return msptr(BlockStmt)(block()); if(match({OPEN_BRACE})) return msptr(BlockStmt)(block());
// Check for assignment statement - simplified approach // Check for assignment statement - simplified approach
if(check(IDENTIFIER)) { if(check(IDENTIFIER) || check(THIS)) {
// Try to parse as assignment expression first // Try to parse as assignment expression first
int currentPos = current; int currentPos = current;
try { try {
@ -511,6 +627,43 @@ sptr(Stmt) Parser::statement()
return expressionStatement(); return expressionStatement();
} }
std::shared_ptr<Stmt> Parser::importStatement() {
Token importTok = previous();
// import Name [as Alias] | import "path"
bool isString = check(STRING);
Token mod = isString ? advance() : consume(IDENTIFIER, "Expected module name or path string after 'import'.");
// Keep IDENTIFIER for name-based imports; resolver will try file and then builtin
bool hasAlias = false; Token alias;
if (match({AS})) {
hasAlias = true;
alias = consume(IDENTIFIER, "Expected alias identifier after 'as'.");
}
consume(SEMICOLON, "Expected ';' after import statement.");
return msptr(ImportStmt)(importTok, mod, hasAlias, alias);
}
std::shared_ptr<Stmt> Parser::fromImportStatement() {
Token fromTok = previous();
bool isString = check(STRING);
Token mod = isString ? advance() : consume(IDENTIFIER, "Expected module name or path string after 'from'.");
// Keep IDENTIFIER for name-based from-imports
consume(IMPORT, "Expected 'import' after module name.");
// 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;
do {
Token name = consume(IDENTIFIER, "Expected name to import.");
bool hasAlias = false; Token alias;
if (match({AS})) { hasAlias = true; alias = consume(IDENTIFIER, "Expected alias identifier after 'as'."); }
items.push_back({name, hasAlias, alias});
} while (match({COMMA}));
consume(SEMICOLON, "Expected ';' after from-import statement.");
return msptr(FromImportStmt)(fromTok, mod, items);
}
sptr(Stmt) Parser::assignmentStatement() sptr(Stmt) Parser::assignmentStatement()
{ {
Token name = consume(IDENTIFIER, "Expected variable name for assignment."); Token name = consume(IDENTIFIER, "Expected variable name for assignment.");
@ -676,6 +829,35 @@ std::vector<sptr(Stmt)> Parser::block()
return statements; return statements;
} }
std::shared_ptr<Stmt> Parser::tryStatement() {
// try { ... } (catch (e) { ... })? (finally { ... })?
auto tryBlock = statement();
Token catchVar{IDENTIFIER, "", previous().line, previous().column};
std::shared_ptr<Stmt> catchBlock = nullptr;
std::shared_ptr<Stmt> finallyBlock = nullptr;
if (match({CATCH})) {
consume(OPEN_PAREN, "Expected '(' after 'catch'.");
// Allow optional identifier: catch() or catch(e)
if (!check(CLOSE_PAREN)) {
Token var = consume(IDENTIFIER, "Expected identifier for catch variable.");
catchVar = var;
}
consume(CLOSE_PAREN, "Expected ')' after catch variable.");
catchBlock = statement();
}
if (match({FINALLY})) {
finallyBlock = statement();
}
return msptr(TryStmt)(tryBlock, catchVar, catchBlock, finallyBlock);
}
std::shared_ptr<Stmt> Parser::throwStatement() {
Token kw = previous();
auto val = expression();
consume(SEMICOLON, "Expected ';' after throw expression.");
return msptr(ThrowStmt)(kw, val);
}
sptr(Expr) Parser::finishCall(sptr(Expr) callee) { sptr(Expr) Parser::finishCall(sptr(Expr) callee) {
std::vector<sptr(Expr)> arguments; std::vector<sptr(Expr)> arguments;

View File

@ -2,6 +2,15 @@
#include "ErrorReporter.h" #include "ErrorReporter.h"
void Environment::assign(const Token& name, const Value& value) { void Environment::assign(const Token& name, const Value& value) {
// Disallow reassignment of module bindings (immutability of module variable)
auto itv = variables.find(name.lexeme);
if (itv != variables.end() && itv->second.isModule()) {
if (errorReporter) {
errorReporter->reportError(name.line, name.column, "Import Error",
"Cannot reassign module binding '" + name.lexeme + "'", "");
}
throw std::runtime_error("Cannot reassign module binding '" + name.lexeme + "'");
}
auto it = variables.find(name.lexeme); auto it = variables.find(name.lexeme);
if (it != variables.end()) { if (it != variables.end()) {
it->second = value; it->second = value;
@ -13,7 +22,9 @@ void Environment::assign(const Token& name, const Value& value) {
return; return;
} }
// Report only if not within a try; otherwise let try/catch handle
if (errorReporter) { if (errorReporter) {
// We cannot check tryDepth here directly; rely on Executor to suppress double-reporting
errorReporter->reportError(name.line, name.column, "Runtime Error", errorReporter->reportError(name.line, name.column, "Runtime Error",
"Undefined variable '" + name.lexeme + "'", ""); "Undefined variable '" + name.lexeme + "'", "");
} }
@ -37,27 +48,14 @@ Value Environment::get(const Token& name) {
throw std::runtime_error("Undefined variable '" + name.lexeme + "'"); throw std::runtime_error("Undefined variable '" + name.lexeme + "'");
} }
Value Environment::get(const std::string& name) {
auto it = variables.find(name);
if (it != variables.end()) {
return it->second;
}
if (parent != nullptr) {
return parent->get(name);
}
throw std::runtime_error("Undefined variable '" + name + "'");
}
void Environment::pruneForClosureCapture() { void Environment::pruneForClosureCapture() {
for (auto &entry : variables) { for (auto &entry : variables) {
Value &v = entry.second; Value &v = entry.second;
if (v.isArray()) { if (v.isArray()) {
// Replace with a new empty array to avoid mutating original shared storage
entry.second = Value(std::vector<Value>{}); entry.second = Value(std::vector<Value>{});
} else if (v.isDict()) { } else if (v.isDict()) {
// Replace with a new empty dict to avoid mutating original shared storage
entry.second = Value(std::unordered_map<std::string, Value>{}); entry.second = Value(std::unordered_map<std::string, Value>{});
} }
} }

View File

@ -1,5 +1,7 @@
#include "Evaluator.h" #include "Evaluator.h"
#include "Interpreter.h" #include "Interpreter.h"
#include "Environment.h"
#include "AssignmentUtils.h"
#include "helperFunctions/HelperFunctions.h" #include "helperFunctions/HelperFunctions.h"
Evaluator::Evaluator(Interpreter* interpreter) : interpreter(interpreter) {} Evaluator::Evaluator(Interpreter* interpreter) : interpreter(interpreter) {}
@ -35,8 +37,6 @@ Value Evaluator::visitUnaryExpr(const std::shared_ptr<UnaryExpr>& expression)
switch (expression->oper.type) { switch (expression->oper.type) {
case MINUS: case MINUS:
if (!right.isNumber()) { if (!right.isNumber()) {
interpreter->reportError(expression->oper.line, expression->oper.column, "Runtime Error",
"Operand must be a number when using: " + expression->oper.lexeme, expression->oper.lexeme);
throw std::runtime_error("Operand must be a number when using: " + expression->oper.lexeme); throw std::runtime_error("Operand must be a number when using: " + expression->oper.lexeme);
} }
return Value(-right.asNumber()); return Value(-right.asNumber());
@ -46,8 +46,6 @@ Value Evaluator::visitUnaryExpr(const std::shared_ptr<UnaryExpr>& expression)
case BIN_NOT: case BIN_NOT:
if (!right.isNumber()) { if (!right.isNumber()) {
interpreter->reportError(expression->oper.line, expression->oper.column, "Runtime Error",
"Operand must be a number when using: " + expression->oper.lexeme, expression->oper.lexeme);
throw std::runtime_error("Operand must be a number when using: " + expression->oper.lexeme); throw std::runtime_error("Operand must be a number when using: " + expression->oper.lexeme);
} }
return Value(static_cast<double>(~(static_cast<long>(right.asNumber())))); return Value(static_cast<double>(~(static_cast<long>(right.asNumber()))));
@ -104,8 +102,6 @@ Value Evaluator::visitBinaryExpr(const std::shared_ptr<BinaryExpr>& expression)
default: break; // Unreachable default: break; // Unreachable
} }
interpreter->reportError(expression->oper.line, expression->oper.column, "Runtime Error",
ErrorUtils::makeOperatorError(opName, left.getType(), right.getType()), opName);
throw std::runtime_error(ErrorUtils::makeOperatorError(opName, left.getType(), right.getType())); throw std::runtime_error(ErrorUtils::makeOperatorError(opName, left.getType(), right.getType()));
} }
@ -115,8 +111,23 @@ Value Evaluator::visitBinaryExpr(const std::shared_ptr<BinaryExpr>& expression)
case PLUS: return left + right; case PLUS: return left + right;
case MINUS: return left - right; case MINUS: return left - right;
case STAR: return left * right; case STAR: return left * right;
case SLASH: return left / right; case SLASH: {
case PERCENT: return left % right; if (right.isNumber() && right.asNumber() == 0.0) {
// Report precise site for division by zero
interpreter->reportError(expression->oper.line, expression->oper.column,
"Runtime Error", "Division by zero", "/");
throw std::runtime_error("Division by zero");
}
return left / right;
}
case PERCENT: {
if (right.isNumber() && right.asNumber() == 0.0) {
interpreter->reportError(expression->oper.line, expression->oper.column,
"Runtime Error", "Modulo by zero", "%");
throw std::runtime_error("Modulo by zero");
}
return left % right;
}
case BIN_AND: return left & right; case BIN_AND: return left & right;
case BIN_OR: return left | right; case BIN_OR: return left | right;
case BIN_XOR: return left ^ right; case BIN_XOR: return left ^ right;
@ -128,10 +139,7 @@ Value Evaluator::visitBinaryExpr(const std::shared_ptr<BinaryExpr>& expression)
throw std::runtime_error("Unknown operator: " + expression->oper.lexeme); throw std::runtime_error("Unknown operator: " + expression->oper.lexeme);
} }
} catch (const std::runtime_error& e) { } catch (const std::runtime_error& e) {
// The Value operators provide good error messages, just add context throw; // Propagate to statement driver (try/catch) without reporting here
interpreter->reportError(expression->oper.line, expression->oper.column, "Runtime Error",
e.what(), expression->oper.lexeme);
throw;
} }
} }
@ -164,7 +172,7 @@ Value Evaluator::visitIncrementExpr(const std::shared_ptr<IncrementExpr>& expres
throw std::runtime_error("Invalid increment/decrement operator."); throw std::runtime_error("Invalid increment/decrement operator.");
} }
// Update the variable or array element // Update the variable, array element, or object property
if (auto varExpr = std::dynamic_pointer_cast<VarExpr>(expression->operand)) { if (auto varExpr = std::dynamic_pointer_cast<VarExpr>(expression->operand)) {
interpreter->getEnvironment()->assign(varExpr->name, Value(newValue)); interpreter->getEnvironment()->assign(varExpr->name, Value(newValue));
} else if (auto arrayExpr = std::dynamic_pointer_cast<ArrayIndexExpr>(expression->operand)) { } else if (auto arrayExpr = std::dynamic_pointer_cast<ArrayIndexExpr>(expression->operand)) {
@ -195,6 +203,14 @@ Value Evaluator::visitIncrementExpr(const std::shared_ptr<IncrementExpr>& expres
// Update the array element // Update the array element
arr[idx] = Value(newValue); arr[idx] = Value(newValue);
} else if (auto propExpr = std::dynamic_pointer_cast<PropertyExpr>(expression->operand)) {
// obj.prop++ / obj.prop--
Value object = interpreter->evaluate(propExpr->object);
if (!object.isDict()) {
throw std::runtime_error("Can only increment/decrement properties on objects");
}
std::unordered_map<std::string, Value>& dict = object.asDict();
dict[propExpr->name.lexeme] = Value(newValue);
} else { } else {
interpreter->reportError(expression->oper.line, expression->oper.column, interpreter->reportError(expression->oper.line, expression->oper.column,
"Runtime Error", "Increment/decrement can only be applied to variables or array elements.", ""); "Runtime Error", "Increment/decrement can only be applied to variables or array elements.", "");
@ -214,74 +230,24 @@ Value Evaluator::visitAssignExpr(const std::shared_ptr<AssignExpr>& expression)
Value value = interpreter->evaluate(expression->value); Value value = interpreter->evaluate(expression->value);
if (expression->op.type == EQUAL) { if (expression->op.type == EQUAL) {
try {
// Check if the variable existed and whether it held a collection
bool existed = false;
bool wasCollection = false;
try {
Value oldValue = interpreter->getEnvironment()->get(expression->name);
existed = true;
wasCollection = oldValue.isArray() || oldValue.isDict();
} catch (...) {
existed = false;
}
// Assign first to release references held by the old values // Assign first to release references held by the old values
interpreter->getEnvironment()->assign(expression->name, value); interpreter->getEnvironment()->assign(expression->name, value);
// Perform cleanup on any reassignment
// Now that the old values are released, perform cleanup on any reassignment
interpreter->forceCleanup(); interpreter->forceCleanup();
} catch (const std::exception& e) { return value;
std::cerr << "Error during assignment: " << e.what() << std::endl;
throw; // Re-throw to see the full stack trace
} }
} else {
// Handle compound assignment operators // Compound assignment operators
Value currentValue = interpreter->getEnvironment()->get(expression->name); Value currentValue = interpreter->getEnvironment()->get(expression->name);
Value newValue; try {
Value newValue = computeCompoundAssignment(currentValue, expression->op.type, value);
switch (expression->op.type) {
case PLUS_EQUAL:
newValue = currentValue + value;
break;
case MINUS_EQUAL:
newValue = currentValue - value;
break;
case STAR_EQUAL:
newValue = currentValue * value;
break;
case SLASH_EQUAL:
newValue = currentValue / value;
break;
case PERCENT_EQUAL:
newValue = currentValue % value;
break;
case BIN_AND_EQUAL:
newValue = currentValue & value;
break;
case BIN_OR_EQUAL:
newValue = currentValue | value;
break;
case BIN_XOR_EQUAL:
newValue = currentValue ^ value;
break;
case BIN_SLEFT_EQUAL:
newValue = currentValue << value;
break;
case BIN_SRIGHT_EQUAL:
newValue = currentValue >> value;
break;
default:
interpreter->reportError(expression->op.line, expression->op.column, "Runtime Error",
"Unknown assignment operator: " + expression->op.lexeme, "");
throw std::runtime_error("Unknown assignment operator: " + expression->op.lexeme);
}
interpreter->getEnvironment()->assign(expression->name, newValue); interpreter->getEnvironment()->assign(expression->name, newValue);
return newValue; return newValue;
} catch (const std::runtime_error&) {
interpreter->reportError(expression->op.line, expression->op.column, "Runtime Error",
"Unknown assignment operator: " + expression->op.lexeme, "");
throw;
} }
return value;
} }
Value Evaluator::visitTernaryExpr(const std::shared_ptr<TernaryExpr>& expression) { Value Evaluator::visitTernaryExpr(const std::shared_ptr<TernaryExpr>& expression) {
@ -318,6 +284,7 @@ Value Evaluator::visitArrayIndexExpr(const std::shared_ptr<ArrayIndexExpr>& expr
if (!index.isNumber()) { if (!index.isNumber()) {
interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error", interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error",
"Array index must be a number", ""); "Array index must be a number", "");
interpreter->markInlineErrorReported();
throw std::runtime_error("Array index must be a number"); throw std::runtime_error("Array index must be a number");
} }
@ -327,6 +294,7 @@ Value Evaluator::visitArrayIndexExpr(const std::shared_ptr<ArrayIndexExpr>& expr
if (idx < 0 || idx >= static_cast<int>(arr.size())) { if (idx < 0 || idx >= static_cast<int>(arr.size())) {
interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error", interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error",
"Array index out of bounds", ""); "Array index out of bounds", "");
interpreter->markInlineErrorReported();
throw std::runtime_error("Array index out of bounds"); throw std::runtime_error("Array index out of bounds");
} }
@ -337,6 +305,7 @@ Value Evaluator::visitArrayIndexExpr(const std::shared_ptr<ArrayIndexExpr>& expr
if (!index.isString()) { if (!index.isString()) {
interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error", interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error",
"Dictionary key must be a string", ""); "Dictionary key must be a string", "");
interpreter->markInlineErrorReported();
throw std::runtime_error("Dictionary key must be a string"); throw std::runtime_error("Dictionary key must be a string");
} }
@ -353,6 +322,7 @@ Value Evaluator::visitArrayIndexExpr(const std::shared_ptr<ArrayIndexExpr>& expr
} else { } else {
interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error", interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error",
"Can only index arrays and dictionaries", ""); "Can only index arrays and dictionaries", "");
interpreter->markInlineErrorReported();
throw std::runtime_error("Can only index arrays and dictionaries"); throw std::runtime_error("Can only index arrays and dictionaries");
} }
} }
@ -361,13 +331,142 @@ Value Evaluator::visitPropertyExpr(const std::shared_ptr<PropertyExpr>& expr) {
Value object = expr->object->accept(this); Value object = expr->object->accept(this);
std::string propertyName = expr->name.lexeme; std::string propertyName = expr->name.lexeme;
if (object.isDict()) { if (object.isModule()) {
return getDictProperty(object, propertyName); // Forward to module exports
auto* mod = object.asModule();
if (mod && mod->exports) {
auto it = mod->exports->find(propertyName);
if (it != mod->exports->end()) return it->second;
}
return NONE_VALUE;
} else if (object.isDict()) {
Value v = getDictProperty(object, propertyName);
if (!v.isNone()) {
// If this is an inherited inline method, prefer a current-class extension override
if (v.isFunction()) {
const auto& d = object.asDict();
std::string curCls;
auto itc = d.find("__class");
if (itc != d.end() && itc->second.isString()) curCls = itc->second.asString();
Function* f = v.asFunction();
if (f && !curCls.empty() && !f->ownerClass.empty() && f->ownerClass != curCls) {
if (auto ext = interpreter->lookupExtension(curCls, propertyName)) {
return Value(ext);
}
}
}
return v;
}
// Fallback to class extensions with inheritance walk
const auto& d = object.asDict();
std::string cls = "";
auto it = d.find("__class");
if (it != d.end() && it->second.isString()) cls = it->second.asString();
if (!cls.empty()) {
std::string cur = cls;
while (!cur.empty()) {
if (auto fn = interpreter->lookupExtension(cur, propertyName)) return Value(fn);
cur = interpreter->getParentClass(cur);
}
}
// Provide method-style builtins on dict
if (propertyName == "len") {
auto bf = std::make_shared<BuiltinFunction>("dict.len", [object](std::vector<Value>, int, int){
return Value(static_cast<double>(object.asDict().size()));
});
return Value(bf);
} else if (propertyName == "keys") {
auto bf = std::make_shared<BuiltinFunction>("dict.keys", [object](std::vector<Value>, int, int){
std::vector<Value> keys; const auto& m = object.asDict();
for (const auto& kv : m) keys.push_back(Value(kv.first));
return Value(keys);
});
return Value(bf);
} else if (propertyName == "values") {
auto bf = std::make_shared<BuiltinFunction>("dict.values", [object](std::vector<Value>, int, int){
std::vector<Value> vals; const auto& m = object.asDict();
for (const auto& kv : m) vals.push_back(kv.second);
return Value(vals);
});
return Value(bf);
} else if (propertyName == "has") {
auto bf = std::make_shared<BuiltinFunction>("dict.has", [object](std::vector<Value> args, int, int){
if (args.size() != 1 || !args[0].isString()) return Value(false);
const auto& m = object.asDict();
return Value(m.find(args[0].asString()) != m.end());
});
return Value(bf);
}
// Fallback to dict and any extensions
if (auto fn = interpreter->lookupExtension("dict", propertyName)) return Value(fn);
if (auto anyFn = interpreter->lookupExtension("any", propertyName)) return Value(anyFn);
return NONE_VALUE;
} else if (object.isArray()) { } else if (object.isArray()) {
return getArrayProperty(object, propertyName); Value v = getArrayProperty(object, propertyName);
if (!v.isNone()) return v;
// Provide method-style builtins on array
if (propertyName == "len") {
auto bf = std::make_shared<BuiltinFunction>("array.len", [object](std::vector<Value>, int, int){
return Value(static_cast<double>(object.asArray().size()));
});
return Value(bf);
} else if (propertyName == "push") {
auto bf = std::make_shared<BuiltinFunction>("array.push", [object](std::vector<Value> args, int, int){
std::vector<Value>& arr = const_cast<std::vector<Value>&>(object.asArray());
for (size_t i = 0; i < args.size(); ++i) arr.push_back(args[i]);
return object;
});
return Value(bf);
} else if (propertyName == "pop") {
auto bf = std::make_shared<BuiltinFunction>("array.pop", [object](std::vector<Value>, int, int){
std::vector<Value>& arr = const_cast<std::vector<Value>&>(object.asArray());
if (arr.empty()) return NONE_VALUE;
Value v = arr.back();
arr.pop_back();
return v;
});
return Value(bf);
}
// Fallback to array extensions
if (auto fn = interpreter->lookupExtension("array", propertyName)) return Value(fn);
if (auto anyFn = interpreter->lookupExtension("any", propertyName)) return Value(anyFn);
return NONE_VALUE;
} else { } else {
// Try extension dispatch for built-ins and any
std::string target;
if (object.isString()) target = "string";
else if (object.isNumber()) target = "number";
else if (object.isArray()) target = "array"; // handled above, but keep for completeness
else if (object.isDict()) target = "dict"; // handled above
else target = object.isModule() ? "any" : "any";
// Provide method-style builtins for string/number
if (object.isString() && propertyName == "len") {
auto bf = std::make_shared<BuiltinFunction>("string.len", [object](std::vector<Value>, int, int){
return Value(static_cast<double>(object.asString().length()));
});
return Value(bf);
}
if (object.isNumber() && propertyName == "toInt") {
auto bf = std::make_shared<BuiltinFunction>("number.toInt", [object](std::vector<Value>, int, int){
return Value(static_cast<double>(static_cast<long long>(object.asNumber())));
});
return Value(bf);
}
if (object.isModule()) {
// Modules are immutable and have no dynamic methods
return NONE_VALUE;
}
auto fn = interpreter->lookupExtension(target, propertyName);
if (!object.isModule() && fn) { return Value(fn); }
if (auto anyFn = interpreter->lookupExtension("any", propertyName)) {
return Value(anyFn);
}
interpreter->reportError(expr->name.line, expr->name.column, "Runtime Error", interpreter->reportError(expr->name.line, expr->name.column, "Runtime Error",
"Cannot access property '" + propertyName + "' on this type", ""); "Cannot access property '" + propertyName + "' on this type", "");
interpreter->markInlineErrorReported();
throw std::runtime_error("Cannot access property '" + propertyName + "' on this type"); throw std::runtime_error("Cannot access property '" + propertyName + "' on this type");
} }
} }
@ -380,8 +479,11 @@ Value Evaluator::visitArrayAssignExpr(const std::shared_ptr<ArrayAssignExpr>& ex
if (array.isArray()) { if (array.isArray()) {
// Handle array assignment // Handle array assignment
if (!index.isNumber()) { if (!index.isNumber()) {
if (!interpreter->isInTry()) {
interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error", interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error",
"Array index must be a number", ""); "Array index must be a number", "");
interpreter->markInlineErrorReported();
}
throw std::runtime_error("Array index must be a number"); throw std::runtime_error("Array index must be a number");
} }
@ -389,8 +491,11 @@ Value Evaluator::visitArrayAssignExpr(const std::shared_ptr<ArrayAssignExpr>& ex
std::vector<Value>& arr = array.asArray(); std::vector<Value>& arr = array.asArray();
if (idx < 0 || idx >= static_cast<int>(arr.size())) { if (idx < 0 || idx >= static_cast<int>(arr.size())) {
if (!interpreter->isInTry()) {
interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error", interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error",
"Array index out of bounds", ""); "Array index out of bounds", "");
interpreter->markInlineErrorReported();
}
throw std::runtime_error("Array index out of bounds"); throw std::runtime_error("Array index out of bounds");
} }
@ -400,8 +505,11 @@ Value Evaluator::visitArrayAssignExpr(const std::shared_ptr<ArrayAssignExpr>& ex
} else if (array.isDict()) { } else if (array.isDict()) {
// Handle dictionary assignment // Handle dictionary assignment
if (!index.isString()) { if (!index.isString()) {
if (!interpreter->isInTry()) {
interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error", interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error",
"Dictionary key must be a string", ""); "Dictionary key must be a string", "");
interpreter->markInlineErrorReported();
}
throw std::runtime_error("Dictionary key must be a string"); throw std::runtime_error("Dictionary key must be a string");
} }
@ -412,8 +520,11 @@ Value Evaluator::visitArrayAssignExpr(const std::shared_ptr<ArrayAssignExpr>& ex
return value; return value;
} else { } else {
if (!interpreter->isInTry()) {
interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error", interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error",
"Can only assign to array or dictionary elements", ""); "Can only assign to array or dictionary elements", "");
interpreter->markInlineErrorReported();
}
throw std::runtime_error("Can only assign to array or dictionary elements"); throw std::runtime_error("Can only assign to array or dictionary elements");
} }
} }
@ -435,14 +546,25 @@ Value Evaluator::visitPropertyAssignExpr(const std::shared_ptr<PropertyAssignExp
Value value = expr->value->accept(this); Value value = expr->value->accept(this);
std::string propertyName = expr->name.lexeme; std::string propertyName = expr->name.lexeme;
if (object.isDict()) { if (object.isModule()) {
// Modules are immutable: disallow setting properties
if (!interpreter->isInTry()) {
interpreter->reportError(expr->name.line, expr->name.column, "Import Error",
"Cannot assign property '" + propertyName + "' on module (immutable)", "");
interpreter->markInlineErrorReported();
}
throw std::runtime_error("Cannot assign property on module (immutable)");
} else if (object.isDict()) {
// Modify the dictionary in place // Modify the dictionary in place
std::unordered_map<std::string, Value>& dict = object.asDict(); std::unordered_map<std::string, Value>& dict = object.asDict();
dict[propertyName] = value; dict[propertyName] = value;
return value; // Return the assigned value return value; // Return the assigned value
} else { } else {
if (!interpreter->isInTry()) {
interpreter->reportError(expr->name.line, expr->name.column, "Runtime Error", interpreter->reportError(expr->name.line, expr->name.column, "Runtime Error",
"Cannot assign property '" + propertyName + "' on non-object", ""); "Cannot assign property '" + propertyName + "' on non-object", "");
interpreter->markInlineErrorReported();
}
throw std::runtime_error("Cannot assign property '" + propertyName + "' on non-object"); throw std::runtime_error("Cannot assign property '" + propertyName + "' on non-object");
} }
} }
@ -453,10 +575,8 @@ Value Evaluator::visitFunctionExpr(const std::shared_ptr<FunctionExpr>& expressi
paramNames.push_back(param.lexeme); paramNames.push_back(param.lexeme);
} }
// Capture a snapshot of the current environment so loop vars like 'i' are frozen per iteration
auto closureEnv = std::make_shared<Environment>(*interpreter->getEnvironment()); auto closureEnv = std::make_shared<Environment>(*interpreter->getEnvironment());
closureEnv->pruneForClosureCapture(); closureEnv->pruneForClosureCapture();
auto function = std::make_shared<Function>("", paramNames, expression->body, closureEnv); auto function = std::make_shared<Function>("", paramNames, expression->body, closureEnv);
return Value(function); return Value(function);
} }

View File

@ -1,6 +1,9 @@
#include "Executor.h" #include "Executor.h"
#include "Evaluator.h" #include "Evaluator.h"
#include "Interpreter.h" #include "Interpreter.h"
#include "Environment.h"
#include "Parser.h"
#include "AssignmentUtils.h"
#include <iostream> #include <iostream>
Executor::Executor(Interpreter* interpreter, Evaluator* evaluator) Executor::Executor(Interpreter* interpreter, Evaluator* evaluator)
@ -9,13 +12,47 @@ Executor::Executor(Interpreter* interpreter, Evaluator* evaluator)
Executor::~Executor() {} Executor::~Executor() {}
void Executor::interpret(const std::vector<std::shared_ptr<Stmt>>& statements) { void Executor::interpret(const std::vector<std::shared_ptr<Stmt>>& statements) {
ExecutionContext top;
for (const auto& statement : statements) { for (const auto& statement : statements) {
execute(statement, nullptr); execute(statement, &top);
if (top.hasThrow) break;
}
if (top.hasThrow) {
// If already reported inline, don't double-report here
if (!interpreter->hasInlineErrorReported()) {
std::string msg = "Uncaught exception";
if (top.thrownValue.isString()) msg = top.thrownValue.asString();
if (top.thrownValue.isDict()) {
auto& d = top.thrownValue.asDict();
auto it = d.find("message");
if (it != d.end() && it->second.isString()) msg = it->second.asString();
}
int line = top.throwLine;
int col = top.throwColumn;
if (line == 0 && col == 0) { line = interpreter->getLastErrorLine(); col = interpreter->getLastErrorColumn(); }
interpreter->reportError(line, col, "Runtime Error", msg, "");
}
// Clear inline marker after handling
interpreter->clearInlineErrorReported();
} }
} }
void Executor::execute(const std::shared_ptr<Stmt>& statement, ExecutionContext* context) { void Executor::execute(const std::shared_ptr<Stmt>& statement, ExecutionContext* context) {
try {
statement->accept(this, context); statement->accept(this, context);
} catch (const std::runtime_error& e) {
if (context) {
std::unordered_map<std::string, Value> err;
err["type"] = Value(std::string("RuntimeError"));
err["message"] = Value(std::string(e.what()));
context->hasThrow = true;
context->thrownValue = Value(err);
if (context->throwLine == 0 && context->throwColumn == 0) {
context->throwLine = interpreter->getLastErrorLine();
context->throwColumn = interpreter->getLastErrorColumn();
}
}
}
} }
void Executor::executeBlock(const std::vector<std::shared_ptr<Stmt>>& statements, std::shared_ptr<Environment> env, ExecutionContext* context) { void Executor::executeBlock(const std::vector<std::shared_ptr<Stmt>>& statements, std::shared_ptr<Environment> env, ExecutionContext* context) {
@ -24,7 +61,14 @@ void Executor::executeBlock(const std::vector<std::shared_ptr<Stmt>>& statements
for (const auto& statement : statements) { for (const auto& statement : statements) {
execute(statement, context); execute(statement, context);
if (context && (context->hasReturn || context->shouldBreak || context->shouldContinue)) { // Bridge any pending throws from expression evaluation into the context
Value pending; int pl=0, pc=0;
if (interpreter->consumePendingThrow(pending, &pl, &pc)) {
if (context) { context->hasThrow = true; context->thrownValue = pending; context->throwLine = pl; context->throwColumn = pc; }
}
// If an inline reporter already handled this error and we are at top level (no try),
// avoid reporting it again here.
if (context && (context->hasReturn || context->shouldBreak || context->shouldContinue || context->hasThrow)) {
interpreter->setEnvironment(previous); interpreter->setEnvironment(previous);
return; return;
} }
@ -39,6 +83,11 @@ void Executor::visitBlockStmt(const std::shared_ptr<BlockStmt>& statement, Execu
void Executor::visitExpressionStmt(const std::shared_ptr<ExpressionStmt>& statement, ExecutionContext* context) { void Executor::visitExpressionStmt(const std::shared_ptr<ExpressionStmt>& statement, ExecutionContext* context) {
Value value = statement->expression->accept(evaluator); Value value = statement->expression->accept(evaluator);
Value thrown; int tl=0, tc=0;
if (interpreter->consumePendingThrow(thrown, &tl, &tc)) {
if (context) { context->hasThrow = true; context->thrownValue = thrown; context->throwLine = tl; context->throwColumn = tc; }
return;
}
if (interpreter->isInteractiveMode()) if (interpreter->isInteractiveMode())
std::cout << "\u001b[38;5;8m[" << interpreter->stringify(value) << "]\u001b[38;5;15m\n"; std::cout << "\u001b[38;5;8m[" << interpreter->stringify(value) << "]\u001b[38;5;15m\n";
@ -48,6 +97,8 @@ void Executor::visitVarStmt(const std::shared_ptr<VarStmt>& statement, Execution
Value value = NONE_VALUE; Value value = NONE_VALUE;
if (statement->initializer != nullptr) { if (statement->initializer != nullptr) {
value = statement->initializer->accept(evaluator); value = statement->initializer->accept(evaluator);
Value thrownInit; int tl=0, tc=0;
if (interpreter->consumePendingThrow(thrownInit, &tl, &tc)) { if (context) { context->hasThrow = true; context->thrownValue = thrownInit; context->throwLine = tl; context->throwColumn = tc; } return; }
} }
interpreter->getEnvironment()->define(statement->name.lexeme, value); interpreter->getEnvironment()->define(statement->name.lexeme, value);
} }
@ -79,7 +130,10 @@ void Executor::visitReturnStmt(const std::shared_ptr<ReturnStmt>& statement, Exe
} }
void Executor::visitIfStmt(const std::shared_ptr<IfStmt>& statement, ExecutionContext* context) { void Executor::visitIfStmt(const std::shared_ptr<IfStmt>& statement, ExecutionContext* context) {
if (interpreter->isTruthy(statement->condition->accept(evaluator))) { Value condValIf = statement->condition->accept(evaluator);
Value thrownIf; int tl=0, tc=0;
if (interpreter->consumePendingThrow(thrownIf, &tl, &tc)) { if (context) { context->hasThrow = true; context->thrownValue = thrownIf; context->throwLine = tl; context->throwColumn = tc; } return; }
if (interpreter->isTruthy(condValIf)) {
execute(statement->thenBranch, context); execute(statement->thenBranch, context);
} else if (statement->elseBranch != nullptr) { } else if (statement->elseBranch != nullptr) {
execute(statement->elseBranch, context); execute(statement->elseBranch, context);
@ -94,7 +148,7 @@ void Executor::visitWhileStmt(const std::shared_ptr<WhileStmt>& statement, Execu
while (interpreter->isTruthy(statement->condition->accept(evaluator))) { while (interpreter->isTruthy(statement->condition->accept(evaluator))) {
execute(statement->body, &loopContext); execute(statement->body, &loopContext);
if (loopContext.hasThrow) { if (context) { context->hasThrow = true; context->thrownValue = loopContext.thrownValue; context->throwLine = loopContext.throwLine; context->throwColumn = loopContext.throwColumn; } break; }
if (loopContext.hasReturn) { if (loopContext.hasReturn) {
if (context) { if (context) {
context->hasReturn = true; context->hasReturn = true;
@ -120,26 +174,17 @@ void Executor::visitDoWhileStmt(const std::shared_ptr<DoWhileStmt>& statement, E
loopContext.isFunctionBody = context->isFunctionBody; loopContext.isFunctionBody = context->isFunctionBody;
} }
do { while (true) {
execute(statement->body, &loopContext); execute(statement->body, &loopContext);
if (loopContext.hasThrow) { if (context) { context->hasThrow = true; context->thrownValue = loopContext.thrownValue; context->throwLine = loopContext.throwLine; context->throwColumn = loopContext.throwColumn; } break; }
if (loopContext.hasReturn) { if (loopContext.hasReturn) { if (context) { context->hasReturn = true; context->returnValue = loopContext.returnValue; } break; }
if (context) { if (loopContext.shouldBreak) { break; }
context->hasReturn = true; if (loopContext.shouldContinue) { loopContext.shouldContinue = false; }
context->returnValue = loopContext.returnValue; Value c = statement->condition->accept(evaluator);
Value thrown; int tl=0, tc=0;
if (interpreter->consumePendingThrow(thrown, &tl, &tc)) { if (context) { context->hasThrow = true; context->thrownValue = thrown; context->throwLine = tl; context->throwColumn = tc; } break; }
if (!interpreter->isTruthy(c)) break;
} }
break;
}
if (loopContext.shouldBreak) {
break;
}
if (loopContext.shouldContinue) {
loopContext.shouldContinue = false;
continue;
}
} while (interpreter->isTruthy(statement->condition->accept(evaluator)));
} }
void Executor::visitForStmt(const std::shared_ptr<ForStmt>& statement, ExecutionContext* context) { void Executor::visitForStmt(const std::shared_ptr<ForStmt>& statement, ExecutionContext* context) {
@ -152,9 +197,15 @@ void Executor::visitForStmt(const std::shared_ptr<ForStmt>& statement, Execution
loopContext.isFunctionBody = context->isFunctionBody; loopContext.isFunctionBody = context->isFunctionBody;
} }
while (statement->condition == nullptr || interpreter->isTruthy(statement->condition->accept(evaluator))) { while (true) {
if (statement->condition != nullptr) {
Value c = statement->condition->accept(evaluator);
Value thrown; int tl=0, tc=0;
if (interpreter->consumePendingThrow(thrown, &tl, &tc)) { if (context) { context->hasThrow = true; context->thrownValue = thrown; context->throwLine = tl; context->throwColumn = tc; } break; }
if (!interpreter->isTruthy(c)) break;
}
execute(statement->body, &loopContext); execute(statement->body, &loopContext);
if (loopContext.hasThrow) { if (context) { context->hasThrow = true; context->thrownValue = loopContext.thrownValue; } break; }
if (loopContext.hasReturn) { if (loopContext.hasReturn) {
if (context) { if (context) {
context->hasReturn = true; context->hasReturn = true;
@ -171,12 +222,16 @@ void Executor::visitForStmt(const std::shared_ptr<ForStmt>& statement, Execution
loopContext.shouldContinue = false; loopContext.shouldContinue = false;
if (statement->increment != nullptr) { if (statement->increment != nullptr) {
statement->increment->accept(evaluator); statement->increment->accept(evaluator);
Value thrown; int tl=0, tc=0;
if (interpreter->consumePendingThrow(thrown, &tl, &tc)) { if (context) { context->hasThrow = true; context->thrownValue = thrown; context->throwLine = tl; context->throwColumn = tc; } break; }
} }
continue; continue;
} }
if (statement->increment != nullptr) { if (statement->increment != nullptr) {
statement->increment->accept(evaluator); statement->increment->accept(evaluator);
Value thrown; int tl=0, tc=0;
if (interpreter->consumePendingThrow(thrown, &tl, &tc)) { if (context) { context->hasThrow = true; context->thrownValue = thrown; context->throwLine = tl; context->throwColumn = tc; } break; }
} }
} }
} }
@ -193,67 +248,250 @@ void Executor::visitContinueStmt(const std::shared_ptr<ContinueStmt>& statement,
} }
} }
void Executor::visitTryStmt(const std::shared_ptr<TryStmt>& statement, ExecutionContext* context) {
interpreter->enterTry();
// Temporarily detach the reporter so any direct uses (e.g., in Environment) won't print inline
auto savedReporter = interpreter->getErrorReporter();
interpreter->setErrorReporter(nullptr);
ExecutionContext inner;
if (context) inner.isFunctionBody = context->isFunctionBody;
execute(statement->tryBlock, &inner);
// Also capture any pending throw signaled by expressions
Value pending; int pl=0, pc=0;
if (interpreter->consumePendingThrow(pending, &pl, &pc)) {
inner.hasThrow = true;
inner.thrownValue = pending;
inner.throwLine = pl;
inner.throwColumn = pc;
}
// If thrown, handle catch
if (inner.hasThrow && statement->catchBlock) {
auto saved = interpreter->getEnvironment();
auto env = std::make_shared<Environment>(saved);
env->setErrorReporter(nullptr);
// Bind catch var if provided
if (!statement->catchVar.lexeme.empty()) {
env->define(statement->catchVar.lexeme, inner.thrownValue);
}
interpreter->setEnvironment(env);
ExecutionContext catchCtx;
catchCtx.isFunctionBody = inner.isFunctionBody;
execute(statement->catchBlock, &catchCtx);
inner.hasThrow = catchCtx.hasThrow;
inner.thrownValue = catchCtx.thrownValue;
inner.throwLine = catchCtx.throwLine;
inner.throwColumn = catchCtx.throwColumn;
interpreter->setEnvironment(saved);
}
// finally always
if (statement->finallyBlock) {
ExecutionContext fctx;
fctx.isFunctionBody = inner.isFunctionBody;
execute(statement->finallyBlock, &fctx);
if (fctx.hasReturn) { if (context) { context->hasReturn = true; context->returnValue = fctx.returnValue; } interpreter->setErrorReporter(savedReporter); interpreter->exitTry(); return; }
if (fctx.hasThrow) { if (context) { context->hasThrow = true; context->thrownValue = fctx.thrownValue; } interpreter->setErrorReporter(savedReporter); interpreter->exitTry(); return; }
if (fctx.shouldBreak) { if (context) { context->shouldBreak = true; } interpreter->setErrorReporter(savedReporter); interpreter->exitTry(); return; }
if (fctx.shouldContinue) { if (context) { context->shouldContinue = true; } interpreter->setErrorReporter(savedReporter); interpreter->exitTry(); return; }
}
// propagate remaining control flow
if (inner.hasReturn) { if (context) { context->hasReturn = true; context->returnValue = inner.returnValue; } interpreter->setErrorReporter(savedReporter); interpreter->exitTry(); return; }
if (inner.hasThrow) { if (context) { context->hasThrow = true; context->thrownValue = inner.thrownValue; context->throwLine = inner.throwLine; context->throwColumn = inner.throwColumn; } interpreter->setErrorReporter(savedReporter); interpreter->exitTry(); return; }
if (inner.shouldBreak) { if (context) { context->shouldBreak = true; } interpreter->setErrorReporter(savedReporter); interpreter->exitTry(); return; }
if (inner.shouldContinue) { if (context) { context->shouldContinue = true; } interpreter->setErrorReporter(savedReporter); interpreter->exitTry(); return; }
interpreter->setErrorReporter(savedReporter);
interpreter->exitTry();
}
void Executor::visitThrowStmt(const std::shared_ptr<ThrowStmt>& statement, ExecutionContext* context) {
Value v = statement->value ? statement->value->accept(evaluator) : NONE_VALUE;
if (context) {
context->hasThrow = true;
context->thrownValue = v;
context->throwLine = statement->keyword.line;
context->throwColumn = statement->keyword.column;
}
}
void Executor::visitImportStmt(const std::shared_ptr<ImportStmt>& statement, ExecutionContext* context) {
// Determine spec (string literal or identifier)
std::string spec = statement->moduleName.lexeme; // already STRING with .bob from parser if name-based
Value mod = interpreter->importModule(spec, statement->importToken.line, statement->importToken.column);
std::string bindName;
if (statement->hasAlias) {
bindName = statement->alias.lexeme;
} else {
// Derive default binding name from module path: basename without extension
std::string path = statement->moduleName.lexeme;
// Strip directories
size_t pos = path.find_last_of("/\\");
std::string base = (pos == std::string::npos) ? path : path.substr(pos + 1);
// Strip .bob
if (base.size() > 4 && base.substr(base.size() - 4) == ".bob") {
base = base.substr(0, base.size() - 4);
}
bindName = base;
}
interpreter->getEnvironment()->define(bindName, mod);
}
void Executor::visitFromImportStmt(const std::shared_ptr<FromImportStmt>& statement, ExecutionContext* context) {
std::string spec = statement->moduleName.lexeme; // already STRING with .bob from parser if name-based
// 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
std::vector<std::pair<std::string,std::string>> items;
for (const auto& it : statement->items) {
items.emplace_back(it.name.lexeme, it.hasAlias ? it.alias.lexeme : it.name.lexeme);
}
if (!interpreter->fromImport(spec, items, statement->fromToken.line, statement->fromToken.column)) {
throw std::runtime_error("from-import failed");
}
}
void Executor::visitAssignStmt(const std::shared_ptr<AssignStmt>& statement, ExecutionContext* context) { void Executor::visitAssignStmt(const std::shared_ptr<AssignStmt>& statement, ExecutionContext* context) {
try {
Value value = statement->value->accept(evaluator); Value value = statement->value->accept(evaluator);
if (statement->op.type == EQUAL) { if (statement->op.type == EQUAL) {
try {
// Assign first to release references held by the old value // Assign first to release references held by the old value
interpreter->getEnvironment()->assign(statement->name, value); interpreter->getEnvironment()->assign(statement->name, value);
// Clean up on any reassignment
// Clean up on any reassignment, regardless of old/new type
interpreter->forceCleanup(); interpreter->forceCleanup();
} catch (const std::exception& e) { return;
std::cerr << "Error during assignment: " << e.what() << std::endl;
throw; // Re-throw to see the full stack trace
} }
} else {
// Handle compound assignment operators
Value currentValue = interpreter->getEnvironment()->get(statement->name);
Value newValue;
switch (statement->op.type) { // Compound assignment operators
case PLUS_EQUAL: Value currentValue = interpreter->getEnvironment()->get(statement->name);
newValue = currentValue + value; try {
break; Value newValue = computeCompoundAssignment(currentValue, statement->op.type, value);
case MINUS_EQUAL: interpreter->getEnvironment()->assign(statement->name, newValue);
newValue = currentValue - value; } catch (const std::runtime_error&) {
break;
case STAR_EQUAL:
newValue = currentValue * value;
break;
case SLASH_EQUAL:
newValue = currentValue / value;
break;
case PERCENT_EQUAL:
newValue = currentValue % value;
break;
case BIN_AND_EQUAL:
newValue = currentValue & value;
break;
case BIN_OR_EQUAL:
newValue = currentValue | value;
break;
case BIN_XOR_EQUAL:
newValue = currentValue ^ value;
break;
case BIN_SLEFT_EQUAL:
newValue = currentValue << value;
break;
case BIN_SRIGHT_EQUAL:
newValue = currentValue >> value;
break;
default:
interpreter->reportError(statement->op.line, statement->op.column, "Runtime Error", interpreter->reportError(statement->op.line, statement->op.column, "Runtime Error",
"Unknown assignment operator: " + statement->op.lexeme, ""); "Unknown assignment operator: " + statement->op.lexeme, "");
throw std::runtime_error("Unknown assignment operator: " + statement->op.lexeme); throw;
} }
}
interpreter->getEnvironment()->assign(statement->name, newValue);
} void Executor::visitClassStmt(const std::shared_ptr<ClassStmt>& statement, ExecutionContext* context) {
} catch (const std::exception& e) { std::unordered_map<std::string, Value> classDict;
std::cerr << "Error in visitAssignStmt: " << e.what() << std::endl; // If parent exists, copy parent's methods as defaults (single inheritance prototype copy)
throw; // Re-throw to see the full stack trace if (statement->hasParent) {
interpreter->registerClass(statement->name.lexeme, statement->parentName.lexeme);
} else {
interpreter->registerClass(statement->name.lexeme, "");
}
// Predefine fields as none, capture this class's field initializers, and register them for inheritance evaluation
std::vector<std::pair<std::string, std::shared_ptr<Expr>>> fieldInitializers;
for (const auto& f : statement->fields) {
classDict[f.name.lexeme] = NONE_VALUE;
fieldInitializers.emplace_back(f.name.lexeme, f.initializer);
}
// Attach methods as functions closed over a prototype env
auto protoEnv = std::make_shared<Environment>(interpreter->getEnvironment());
protoEnv->pruneForClosureCapture();
for (const auto& method : statement->methods) {
std::vector<std::string> paramNames;
for (const Token& p : method->params) paramNames.push_back(p.lexeme);
auto fn = std::make_shared<Function>(method->name.lexeme, paramNames, method->body, protoEnv, statement->name.lexeme);
classDict[method->name.lexeme] = Value(fn);
}
// Save template to interpreter so instances can include inherited fields/methods
interpreter->setClassTemplate(statement->name.lexeme, classDict);
// Register field initializers for this class
interpreter->setClassFieldInitializers(statement->name.lexeme, fieldInitializers);
// Define a constructor function Name(...) that builds an instance dict
auto ctorName = statement->name.lexeme;
auto ctor = std::make_shared<BuiltinFunction>(ctorName, [runtime=interpreter, className=statement->name.lexeme, fieldInitializers](std::vector<Value> args, int line, int col) -> Value {
Value instance(std::unordered_map<std::string, Value>{});
auto& dictRef = instance.asDict();
// Merge class template including inherited members
std::unordered_map<std::string, Value> tmpl = runtime->buildMergedTemplate(className);
for (const auto& kv : tmpl) dictRef[kv.first] = kv.second;
// Tag instance with class name for extension lookup
dictRef["__class"] = Value(className);
// Evaluate field initializers across inheritance chain (parent-to-child order)
{
// Build chain from base to current
std::vector<std::string> chain;
std::string cur = className;
while (!cur.empty()) { chain.push_back(cur); cur = runtime->getParentClass(cur); }
std::reverse(chain.begin(), chain.end());
auto saved = runtime->getEnvironment();
auto env = std::make_shared<Environment>(saved);
env->setErrorReporter(nullptr);
env->define("this", instance);
runtime->setEnvironment(env);
for (const auto& cls : chain) {
std::vector<std::pair<std::string, std::shared_ptr<Expr>>> inits;
if (runtime->getClassFieldInitializers(cls, inits)) {
for (const auto& kv : inits) {
const std::string& fieldName = kv.first;
const auto& expr = kv.second;
if (expr) {
Value v = runtime->evaluate(expr);
dictRef[fieldName] = v;
// Expose field names as locals for subsequent initializers
env->define(fieldName, v);
}
}
}
}
runtime->setEnvironment(saved);
}
// Auto-call init if present: create call env with this and params
auto it = dictRef.find("init");
if (it != dictRef.end() && it->second.isFunction()) {
Function* fn = it->second.asFunction();
// New environment from closure
std::shared_ptr<Environment> newEnv = std::make_shared<Environment>(fn->closure);
newEnv->setErrorReporter(nullptr);
newEnv->define("this", instance);
// Seed current class for proper super resolution within init
if (fn && !fn->ownerClass.empty()) {
newEnv->define("__currentClass", Value(fn->ownerClass));
}
// Bind params
size_t n = std::min(fn->params.size(), args.size());
for (size_t i = 0; i < n; ++i) {
newEnv->define(fn->params[i], args[i]);
}
// Execute body
auto envSaved = runtime->getEnvironment();
runtime->setEnvironment(newEnv);
ExecutionContext ctx; ctx.isFunctionBody = true;
for (const auto& stmt : fn->body) {
runtime->execute(stmt, &ctx);
if (ctx.hasReturn) break;
}
runtime->setEnvironment(envSaved);
}
return instance;
});
interpreter->getEnvironment()->define(statement->name.lexeme, Value(ctor));
}
void Executor::visitExtensionStmt(const std::shared_ptr<ExtensionStmt>& statement, ExecutionContext* context) {
auto target = statement->target.lexeme;
for (const auto& method : statement->methods) {
std::vector<std::string> params;
for (const Token& p : method->params) params.push_back(p.lexeme);
auto fn = std::make_shared<Function>(method->name.lexeme, params, method->body, interpreter->getEnvironment(), target);
interpreter->registerExtension(target, method->name.lexeme, fn);
} }
} }

View File

@ -1,7 +1,19 @@
#include "Interpreter.h" #include "Interpreter.h"
#include "register.h"
#include "Evaluator.h" #include "Evaluator.h"
#include "Executor.h" #include "Executor.h"
#include "BobStdLib.h" #include "BobStdLib.h"
#include "ErrorReporter.h"
#include "Environment.h"
#include "Expression.h"
#include "Parser.h"
#include <filesystem>
#if defined(_WIN32)
#include <direct.h>
#else
#include <unistd.h>
#endif
#include <fstream>
#include <iostream> #include <iostream>
Interpreter::Interpreter(bool isInteractive) Interpreter::Interpreter(bool isInteractive)
@ -9,6 +21,11 @@ Interpreter::Interpreter(bool isInteractive)
evaluator = std::make_unique<Evaluator>(this); evaluator = std::make_unique<Evaluator>(this);
executor = std::make_unique<Executor>(this, evaluator.get()); executor = std::make_unique<Executor>(this, evaluator.get());
environment = std::make_shared<Environment>(); environment = std::make_shared<Environment>();
// Default module search paths: current dir and tests
moduleSearchPaths = { ".", "tests" };
// Register all builtin modules via aggregator
registerAllBuiltinModules(*this);
} }
Interpreter::~Interpreter() = default; Interpreter::~Interpreter() = default;
@ -32,6 +49,25 @@ Value Interpreter::evaluate(const std::shared_ptr<Expr>& expr) {
} }
return runTrampoline(result); return runTrampoline(result);
} }
bool Interpreter::defineGlobalVar(const std::string& name, const Value& value) {
if (!environment) return false;
try {
environment->define(name, value);
return true;
} catch (...) { return false; }
}
bool Interpreter::tryGetGlobalVar(const std::string& name, Value& out) const {
if (!environment) return false;
try {
out = environment->get(Token{IDENTIFIER, name, 0, 0});
return true;
} catch (...) { return false; }
}
bool Interpreter::hasReportedError() const {
return inlineErrorReported;
}
Value Interpreter::runTrampoline(Value initialResult) { Value Interpreter::runTrampoline(Value initialResult) {
Value current = initialResult; Value current = initialResult;
@ -56,14 +92,169 @@ std::string Interpreter::stringify(Value object) {
void Interpreter::addStdLibFunctions() { void Interpreter::addStdLibFunctions() {
BobStdLib::addToEnvironment(environment, *this, errorReporter); BobStdLib::addToEnvironment(environment, *this, errorReporter);
} }
void Interpreter::setModulePolicy(bool allowFiles, bool preferFiles, const std::vector<std::string>& searchPaths) {
allowFileImports = allowFiles;
preferFileOverBuiltin = preferFiles;
moduleSearchPaths = searchPaths;
}
static std::string joinPath(const std::string& baseDir, const std::string& rel) {
namespace fs = std::filesystem;
fs::path p = fs::path(baseDir) / fs::path(rel);
return fs::path(p).lexically_normal().string();
}
static std::string locateModuleFile(const std::string& baseDir, const std::vector<std::string>& searchPaths, const std::string& nameDotBob) {
namespace fs = std::filesystem;
// Only search relative to the importing file's directory
// 1) baseDir/name.bob
if (!baseDir.empty()) {
std::string p = joinPath(baseDir, nameDotBob);
if (fs::exists(fs::path(p))) return p;
}
// 2) baseDir/searchPath/name.bob (search paths are relative to baseDir)
for (const auto& sp : searchPaths) {
if (!baseDir.empty()) {
std::string pb = joinPath(baseDir, joinPath(sp, nameDotBob));
if (fs::exists(fs::path(pb))) return pb;
}
}
return "";
}
Value Interpreter::importModule(const std::string& spec, int line, int column) {
// Determine if spec is a path string (heuristic: contains '/' or ends with .bob)
bool looksPath = spec.find('/') != std::string::npos || (spec.size() >= 4 && spec.rfind(".bob") == spec.size() - 4) || spec.find("..") != std::string::npos;
// Cache key resolution
std::string key = spec;
std::string baseDir = "";
if (errorReporter && !errorReporter->getCurrentFileName().empty()) {
std::filesystem::path p(errorReporter->getCurrentFileName());
baseDir = p.has_parent_path() ? p.parent_path().string() : baseDir;
}
if (looksPath) {
if (!allowFileImports) {
reportError(line, column, "Import Error", "File imports are disabled by policy", spec);
throw std::runtime_error("File imports disabled");
}
// Resolve STRING path specs:
// - Absolute: use as-is
// - Starts with ./ or ../: resolve relative to the importing file directory (baseDir)
// - Otherwise: resolve relative to current working directory
if (!spec.empty() && spec[0] == '/') {
key = spec;
} else if (spec.rfind("./", 0) == 0 || spec.rfind("../", 0) == 0) {
key = joinPath(baseDir, spec);
} else {
// Resolve all non-absolute paths relative to the importing file directory only
key = joinPath(baseDir, spec);
}
} else {
// Name import: try file in baseDir or search paths; else builtin
if (preferFileOverBuiltin && allowFileImports) {
std::string found = locateModuleFile(baseDir, moduleSearchPaths, spec + ".bob");
if (!found.empty()) { key = found; looksPath = true; }
}
if (!looksPath && allowBuiltinImports && builtinModules.has(spec)) {
key = std::string("builtin:") + spec;
}
}
// Return from cache
auto it = moduleCache.find(key);
if (it != moduleCache.end()) return it->second;
// If still not a path, it must be builtin or missing
if (!looksPath) {
if (!builtinModules.has(spec)) {
reportError(line, column, "Import Error", "Module not found: " + spec + ".bob", spec);
throw std::runtime_error("Module not found");
}
// Builtin: return from cache or construct and cache
auto itc = moduleCache.find(key);
if (itc != moduleCache.end()) return itc->second;
Value v = builtinModules.create(spec, *this);
if (v.isNone()) { // cloaked by policy
reportError(line, column, "Import Error", "Module not found: " + spec + ".bob", spec);
throw std::runtime_error("Module not found");
}
moduleCache[key] = v;
return v;
}
// File module: read and execute in isolated env
std::ifstream file(key);
if (!file.is_open()) {
reportError(line, column, "Import Error", "Could not open module file: " + key, spec);
throw std::runtime_error("Module file open failed");
}
std::string code((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
file.close();
// Prepare reporter with module source
if (errorReporter) errorReporter->pushSource(code, key);
// New lexer and parser
Lexer lx; if (errorReporter) lx.setErrorReporter(errorReporter);
std::vector<Token> toks = lx.Tokenize(code);
Parser p(toks); if (errorReporter) p.setErrorReporter(errorReporter);
std::vector<std::shared_ptr<Stmt>> stmts = p.parse();
// Isolated environment
auto saved = getEnvironment();
auto modEnv = std::make_shared<Environment>(saved);
modEnv->setErrorReporter(errorReporter);
setEnvironment(modEnv);
// Execute
executor->interpret(stmts);
// Build module object from env
std::unordered_map<std::string, Value> exported = modEnv->getAll();
// Derive module name from key basename
std::string modName = key;
size_t pos = modName.find_last_of("/\\"); if (pos != std::string::npos) modName = modName.substr(pos+1);
if (modName.size() > 4 && modName.substr(modName.size()-4) == ".bob") modName = modName.substr(0, modName.size()-4);
auto m = std::make_shared<Module>(modName, exported);
Value moduleVal(m);
// Cache
moduleCache[key] = moduleVal;
// Restore env and reporter
setEnvironment(saved);
if (errorReporter) errorReporter->popSource();
return moduleVal;
}
bool Interpreter::fromImport(const std::string& spec, const std::vector<std::pair<std::string, std::string>>& items, int line, int column) {
Value mod = importModule(spec, line, column);
if (!(mod.isModule() || mod.isDict())) {
reportError(line, column, "Import Error", "Module did not evaluate to a module", spec);
return false;
}
std::unordered_map<std::string, Value> const* src = nullptr;
std::unordered_map<std::string, Value> temp;
if (mod.isModule()) {
// Module exports
src = mod.asModule()->exports.get();
} else {
src = &mod.asDict();
}
for (const auto& [name, alias] : items) {
auto it = src->find(name);
if (it == src->end()) {
reportError(line, column, "Import Error", "Name not found in module: " + name, spec);
return false;
}
environment->define(alias, it->second);
}
return true;
}
void Interpreter::addBuiltinFunction(std::shared_ptr<BuiltinFunction> func) { void Interpreter::addBuiltinFunction(std::shared_ptr<BuiltinFunction> func) {
builtinFunctions.push_back(func); builtinFunctions.push_back(func);
} }
void Interpreter::addThunk(std::shared_ptr<Thunk> thunk) {
thunks.push_back(thunk);
}
void Interpreter::addFunction(std::shared_ptr<Function> function) { void Interpreter::addFunction(std::shared_ptr<Function> function) {
functions.push_back(function); functions.push_back(function);
@ -74,7 +265,6 @@ void Interpreter::setErrorReporter(ErrorReporter* reporter) {
if (environment) { if (environment) {
environment->setErrorReporter(reporter); environment->setErrorReporter(reporter);
} }
addStdLibFunctions();
} }
bool Interpreter::isInteractiveMode() const { bool Interpreter::isInteractiveMode() const {
@ -90,6 +280,12 @@ void Interpreter::setEnvironment(std::shared_ptr<Environment> env) {
} }
void Interpreter::reportError(int line, int column, const std::string& errorType, const std::string& message, const std::string& lexeme) { void Interpreter::reportError(int line, int column, const std::string& errorType, const std::string& message, const std::string& lexeme) {
// Always track last error site
setLastErrorSite(line, column);
// Suppress inline printing while inside try; error will propagate to catch/finally
if (isInTry()) {
return;
}
if (errorReporter) { if (errorReporter) {
errorReporter->reportError(line, column, errorType, message, lexeme); errorReporter->reportError(line, column, errorType, message, lexeme);
} }
@ -107,22 +303,277 @@ void Interpreter::forceCleanup() {
diagnostics.forceCleanup(builtinFunctions, functions, thunks); diagnostics.forceCleanup(builtinFunctions, functions, thunks);
} }
void Interpreter::registerExtension(const std::string& targetName, const std::string& methodName, std::shared_ptr<Function> fn) {
// Builtin targets routed to builtinExtensions
if (targetName == "string" || targetName == "array" || targetName == "dict" || targetName == "any" || targetName == "number") {
builtinExtensions[targetName][methodName] = fn;
return;
}
// Otherwise treat as user class name
classExtensions[targetName][methodName] = fn;
}
std::shared_ptr<Function> Interpreter::lookupExtension(const std::string& targetName, const std::string& methodName) {
// If this is a user class name, prefer class extensions
auto cit = classExtensions.find(targetName);
if (cit != classExtensions.end()) {
auto mit = cit->second.find(methodName);
if (mit != cit->second.end()) return mit->second;
// If not on class, fall through to any
auto anyIt2 = builtinExtensions.find("any");
if (anyIt2 != builtinExtensions.end()) {
auto am = anyIt2->second.find(methodName);
if (am != anyIt2->second.end()) return am->second;
}
return nullptr;
}
// Builtin targets
auto bit = builtinExtensions.find(targetName);
if (bit != builtinExtensions.end()) {
auto mit = bit->second.find(methodName);
if (mit != bit->second.end()) return mit->second;
}
// any fallback for builtins and unknowns
auto anyIt = builtinExtensions.find("any");
if (anyIt != builtinExtensions.end()) {
auto mit = anyIt->second.find(methodName);
if (mit != anyIt->second.end()) return mit->second;
}
return nullptr;
}
void Interpreter::registerClass(const std::string& className, const std::string& parentName) {
if (!parentName.empty()) {
classParents[className] = parentName;
}
}
std::string Interpreter::getParentClass(const std::string& className) const {
auto it = classParents.find(className);
if (it != classParents.end()) return it->second;
return "";
}
void Interpreter::setClassTemplate(const std::string& className, const std::unordered_map<std::string, Value>& tmpl) {
classTemplates[className] = tmpl;
}
bool Interpreter::getClassTemplate(const std::string& className, std::unordered_map<std::string, Value>& out) const {
auto it = classTemplates.find(className);
if (it == classTemplates.end()) return false;
out = it->second;
return true;
}
std::unordered_map<std::string, Value> Interpreter::buildMergedTemplate(const std::string& className) const {
std::unordered_map<std::string, Value> merged;
// Merge parent chain first
std::string cur = className;
std::vector<std::string> chain;
while (!cur.empty()) { chain.push_back(cur); cur = getParentClass(cur); }
for (auto it = chain.rbegin(); it != chain.rend(); ++it) {
auto ct = classTemplates.find(*it);
if (ct != classTemplates.end()) {
for (const auto& kv : ct->second) merged[kv.first] = kv.second;
}
}
return merged;
}
Value Interpreter::evaluateCallExprInline(const std::shared_ptr<CallExpr>& expression) { Value Interpreter::evaluateCallExprInline(const std::shared_ptr<CallExpr>& expression) {
Value callee = evaluate(expression->callee); // Direct call instead of through evaluator bool isMethodCall = false;
bool isSuperCall = false;
std::string methodName;
Value receiver = NONE_VALUE;
if (auto prop = std::dynamic_pointer_cast<PropertyExpr>(expression->callee)) {
if (auto varObj = std::dynamic_pointer_cast<VarExpr>(prop->object)) {
if (varObj->name.lexeme == "super") {
isSuperCall = true;
if (environment) {
try {
receiver = environment->get(Token{IDENTIFIER, "this", prop->name.line, prop->name.column});
} catch (...) {
receiver = NONE_VALUE;
}
}
}
}
if (!isSuperCall) {
receiver = evaluate(prop->object);
}
methodName = prop->name.lexeme;
isMethodCall = true;
}
Value callee = NONE_VALUE;
if (!isSuperCall) {
callee = evaluate(expression->callee);
}
// If property wasn't found as a callable, try extension lookup
if (isMethodCall && !(callee.isFunction() || callee.isBuiltinFunction())) {
if (isSuperCall && !receiver.isDict()) {
std::string errorMsg = "super can only be used inside class methods";
if (errorReporter) {
errorReporter->reportError(expression->paren.line, expression->paren.column, "Runtime Error",
errorMsg, "");
}
throw std::runtime_error(errorMsg);
}
if (!methodName.empty()) {
// Built-ins direct
if (isSuperCall && receiver.isDict()) {
// Resolve using current executing class context if available
std::string curClass;
if (environment) {
try {
Value cc = environment->get(Token{IDENTIFIER, "__currentClass", expression->paren.line, expression->paren.column});
if (cc.isString()) curClass = cc.asString();
} catch (...) {}
}
// If not set yet (e.g., resolving before entering callee), inspect callee ownerClass if available
if (curClass.empty()) {
// Try child class extension to determine current context
const auto& d = receiver.asDict();
auto itc = d.find("__class");
if (itc != d.end() && itc->second.isString()) {
std::string child = itc->second.asString();
if (auto childExt = lookupExtension(child, methodName)) {
curClass = child; // use child as current class for super
}
}
}
if (curClass.empty() && callee.isFunction()) {
Function* cf = callee.asFunction();
if (cf && !cf->ownerClass.empty()) curClass = cf->ownerClass;
}
if (curClass.empty()) {
const auto& d = receiver.asDict();
auto itc = d.find("__class");
if (itc != d.end() && itc->second.isString()) curClass = itc->second.asString();
}
std::string cur = getParentClass(curClass);
int guard = 0;
while (!cur.empty() && guard++ < 64) {
auto tmplIt = classTemplates.find(cur);
if (tmplIt != classTemplates.end()) {
auto vIt = tmplIt->second.find(methodName);
if (vIt != tmplIt->second.end() && vIt->second.isFunction()) {
callee = vIt->second;
break;
}
}
if (auto fn = lookupExtension(cur, methodName)) { callee = Value(fn); break; }
cur = getParentClass(cur);
}
// If still not found, try built-in fallbacks to keep behavior consistent
if (!callee.isFunction()) {
if (auto dictFn = lookupExtension("dict", methodName)) callee = Value(dictFn);
else if (auto anyFn = lookupExtension("any", methodName)) callee = Value(anyFn);
}
} else if (receiver.isArray()) {
if (auto fn = lookupExtension("array", methodName)) callee = Value(fn);
else if (methodName == "len") {
auto bf = std::make_shared<BuiltinFunction>("array.len", [receiver](std::vector<Value> args, int, int){
return Value(static_cast<double>(receiver.asArray().size()));
});
callee = Value(bf);
} else if (methodName == "push") {
auto bf = std::make_shared<BuiltinFunction>("array.push", [receiver](std::vector<Value> args, int, int){
std::vector<Value>& arr = const_cast<std::vector<Value>&>(receiver.asArray());
for (size_t i = 0; i < args.size(); ++i) arr.push_back(args[i]);
return receiver;
});
callee = Value(bf);
} else if (methodName == "pop") {
auto bf = std::make_shared<BuiltinFunction>("array.pop", [receiver](std::vector<Value> args, int, int){
std::vector<Value>& arr = const_cast<std::vector<Value>&>(receiver.asArray());
if (arr.empty()) return NONE_VALUE;
Value v = arr.back();
arr.pop_back();
return v;
});
callee = Value(bf);
} else if (auto anyFn = lookupExtension("any", methodName)) callee = Value(anyFn);
} else if (receiver.isString()) {
if (auto fn = lookupExtension("string", methodName)) callee = Value(fn);
else if (methodName == "len") {
auto bf = std::make_shared<BuiltinFunction>("string.len", [receiver](std::vector<Value> args, int, int){
return Value(static_cast<double>(receiver.asString().length()));
});
callee = Value(bf);
} else if (auto anyFn = lookupExtension("any", methodName)) callee = Value(anyFn);
} else if (receiver.isNumber()) {
if (auto fn = lookupExtension("number", methodName)) callee = Value(fn);
else if (methodName == "toInt") {
auto bf = std::make_shared<BuiltinFunction>("number.toInt", [receiver](std::vector<Value> args, int, int){
return Value(static_cast<double>(static_cast<long long>(receiver.asNumber())));
});
callee = Value(bf);
} else if (auto anyFn = lookupExtension("any", methodName)) callee = Value(anyFn);
} else if (receiver.isDict()) {
const auto& d = receiver.asDict();
std::string cls;
auto it = d.find("__class");
if (it != d.end() && it->second.isString()) cls = it->second.asString();
// Walk class chain first
std::string cur = cls;
while (!cur.empty()) {
if (auto fn = lookupExtension(cur, methodName)) { callee = Value(fn); break; }
cur = getParentClass(cur);
}
// Fallbacks
if (!callee.isFunction()) {
if (auto dictFn = lookupExtension("dict", methodName)) callee = Value(dictFn);
else if (methodName == "len") {
auto bf = std::make_shared<BuiltinFunction>("dict.len", [receiver](std::vector<Value> args, int, int){
return Value(static_cast<double>(receiver.asDict().size()));
});
callee = Value(bf);
} else if (methodName == "keys") {
auto bf = std::make_shared<BuiltinFunction>("dict.keys", [receiver](std::vector<Value> args, int, int){
std::vector<Value> keys; const auto& m = receiver.asDict();
for (const auto& kv : m) keys.push_back(Value(kv.first));
return Value(keys);
});
callee = Value(bf);
} else if (methodName == "values") {
auto bf = std::make_shared<BuiltinFunction>("dict.values", [receiver](std::vector<Value> args, int, int){
std::vector<Value> vals; const auto& m = receiver.asDict();
for (const auto& kv : m) vals.push_back(kv.second);
return Value(vals);
});
callee = Value(bf);
} else if (methodName == "has") {
auto bf = std::make_shared<BuiltinFunction>("dict.has", [receiver](std::vector<Value> args, int, int){
if (args.size() != 1 || !args[0].isString()) return Value(false);
const auto& m = receiver.asDict();
return Value(m.find(args[0].asString()) != m.end());
});
callee = Value(bf);
}
else if (auto anyFn = lookupExtension("any", methodName)) callee = Value(anyFn);
}
} else {
if (auto anyFn = lookupExtension("any", methodName)) callee = Value(anyFn);
}
}
}
if (callee.isBuiltinFunction()) { if (callee.isBuiltinFunction()) {
// Handle builtin functions with direct evaluation // Handle builtin functions with direct evaluation
std::vector<Value> arguments; std::vector<Value> arguments;
for (const auto& argument : expression->arguments) { for (const auto& argument : expression->arguments) {
arguments.push_back(evaluate(argument)); // Direct call arguments.push_back(evaluate(argument));
} }
BuiltinFunction* builtinFunction = callee.asBuiltinFunction(); BuiltinFunction* builtinFunction = callee.asBuiltinFunction();
return builtinFunction->func(arguments, expression->paren.line, expression->paren.column); return builtinFunction->func(arguments, expression->paren.line, expression->paren.column);
} }
if (!callee.isFunction()) { if (!callee.isFunction()) {
// Provide better error message with type information (like original) std::string errorMsg = isSuperCall ? ("Undefined super method '" + methodName + "'")
std::string errorMsg = "Can only call functions, got " + callee.getType(); : ("Can only call functions, got " + callee.getType());
if (errorReporter) { if (errorReporter) {
errorReporter->reportError(expression->paren.line, expression->paren.column, "Runtime Error", errorReporter->reportError(expression->paren.line, expression->paren.column, "Runtime Error",
errorMsg, ""); errorMsg, "");
@ -134,7 +585,7 @@ Value Interpreter::evaluateCallExprInline(const std::shared_ptr<CallExpr>& expre
std::vector<Value> arguments; std::vector<Value> arguments;
for (const auto& argument : expression->arguments) { for (const auto& argument : expression->arguments) {
arguments.push_back(evaluate(argument)); // Direct call instead of through evaluator arguments.push_back(evaluate(argument));
} }
// Check arity (like original) // Check arity (like original)
@ -150,12 +601,18 @@ Value Interpreter::evaluateCallExprInline(const std::shared_ptr<CallExpr>& expre
// Check if this is a tail call for inline TCO // Check if this is a tail call for inline TCO
if (expression->isTailCall) { if (expression->isTailCall) {
// Create a thunk for tail call optimization - original inline version
auto thunk = std::make_shared<Thunk>([this, function, arguments]() -> Value { auto thunk = std::make_shared<Thunk>([this, function, arguments, isMethodCall, receiver, isSuperCall]() -> Value {
// Use RAII to manage environment (exactly like original)
ScopedEnv _env(environment); ScopedEnv _env(environment);
environment = std::make_shared<Environment>(function->closure); environment = std::make_shared<Environment>(function->closure);
environment->setErrorReporter(errorReporter); environment->setErrorReporter(errorReporter);
if (isMethodCall) {
environment->define("this", receiver);
if (isSuperCall) environment->define("super", receiver);
if (function && !function->ownerClass.empty()) {
environment->define("__currentClass", Value(function->ownerClass));
}
}
for (size_t i = 0; i < function->params.size(); i++) { for (size_t i = 0; i < function->params.size(); i++) {
environment->define(function->params[i], arguments[i]); environment->define(function->params[i], arguments[i]);
@ -164,12 +621,11 @@ Value Interpreter::evaluateCallExprInline(const std::shared_ptr<CallExpr>& expre
ExecutionContext context; ExecutionContext context;
context.isFunctionBody = true; context.isFunctionBody = true;
// Use RAII to manage thunk execution flag
ScopedThunkFlag _inThunk(inThunkExecution); ScopedThunkFlag _inThunk(inThunkExecution);
// Execute function body (inline like original - direct accept for performance)
for (const auto& stmt : function->body) { for (const auto& stmt : function->body) {
stmt->accept(executor.get(), &context); // Direct call like original stmt->accept(executor.get(), &context);
if (context.hasThrow) { setPendingThrow(context.thrownValue, context.throwLine, context.throwColumn); return NONE_VALUE; }
if (context.hasReturn) { if (context.hasReturn) {
return context.returnValue; return context.returnValue;
} }
@ -178,10 +634,8 @@ Value Interpreter::evaluateCallExprInline(const std::shared_ptr<CallExpr>& expre
return context.returnValue; return context.returnValue;
}); });
// Store the thunk to keep it alive and return as Value (exactly like original)
thunks.push_back(thunk); thunks.push_back(thunk);
// Automatic cleanup check
thunkCreationCount++; thunkCreationCount++;
if (thunkCreationCount >= CLEANUP_THRESHOLD) { if (thunkCreationCount >= CLEANUP_THRESHOLD) {
cleanupUnusedThunks(); cleanupUnusedThunks();
@ -190,10 +644,16 @@ Value Interpreter::evaluateCallExprInline(const std::shared_ptr<CallExpr>& expre
return Value(thunk); return Value(thunk);
} else { } else {
// Normal function call - create new environment (exactly like original)
ScopedEnv _env(environment); ScopedEnv _env(environment);
environment = std::make_shared<Environment>(function->closure); environment = std::make_shared<Environment>(function->closure);
environment->setErrorReporter(errorReporter); environment->setErrorReporter(errorReporter);
if (isMethodCall) {
environment->define("this", receiver);
if (isSuperCall) environment->define("super", receiver);
if (function && !function->ownerClass.empty()) {
environment->define("__currentClass", Value(function->ownerClass));
}
}
for (size_t i = 0; i < function->params.size(); i++) { for (size_t i = 0; i < function->params.size(); i++) {
environment->define(function->params[i], arguments[i]); environment->define(function->params[i], arguments[i]);
@ -202,31 +662,13 @@ Value Interpreter::evaluateCallExprInline(const std::shared_ptr<CallExpr>& expre
ExecutionContext context; ExecutionContext context;
context.isFunctionBody = true; context.isFunctionBody = true;
// Execute function body (exactly like original - direct accept for performance)
for (const auto& stmt : function->body) { for (const auto& stmt : function->body) {
stmt->accept(executor.get(), &context); // Direct call like original stmt->accept(executor.get(), &context);
if (context.hasReturn) { if (context.hasThrow) { setPendingThrow(context.thrownValue, context.throwLine, context.throwColumn); return NONE_VALUE; }
return context.returnValue; if (context.hasReturn) { return context.returnValue; }
}
} }
return context.returnValue; return context.returnValue;
} }
} }
// Function creation count management
void Interpreter::incrementFunctionCreationCount() {
functionCreationCount++;
}
int Interpreter::getFunctionCreationCount() const {
return functionCreationCount;
}
void Interpreter::resetFunctionCreationCount() {
functionCreationCount = 0;
}
int Interpreter::getCleanupThreshold() const {
return 1000000; // Same as CLEANUP_THRESHOLD used for thunks
}

View File

@ -13,122 +13,19 @@
#include <algorithm> #include <algorithm>
bool RuntimeDiagnostics::isTruthy(Value object) { bool RuntimeDiagnostics::isTruthy(Value object) { return object.isTruthy(); }
if(object.isBoolean()) {
return object.asBoolean();
}
if(object.isNone()) {
return false;
}
if(object.isNumber()) {
return object.asNumber() != 0;
}
if(object.isString()) {
return object.asString().length() > 0;
}
return true;
}
bool RuntimeDiagnostics::isEqual(Value a, Value b) { bool RuntimeDiagnostics::isEqual(Value a, Value b) {
// Handle none comparisons first
if (a.isNone() || b.isNone()) {
return a.isNone() && b.isNone();
}
// Handle same type comparisons
if (a.isNumber() && b.isNumber()) {
return a.asNumber() == b.asNumber();
}
if (a.isBoolean() && b.isBoolean()) {
return a.asBoolean() == b.asBoolean();
}
if (a.isString() && b.isString()) {
return a.asString() == b.asString();
}
if (a.isArray() && b.isArray()) {
const std::vector<Value>& arrA = a.asArray();
const std::vector<Value>& arrB = b.asArray();
if (arrA.size() != arrB.size()) {
return false;
}
for (size_t i = 0; i < arrA.size(); i++) {
if (!isEqual(arrA[i], arrB[i])) {
return false;
}
}
return true;
}
if (a.isFunction() && b.isFunction()) {
// Functions are equal only if they are the same object
return a.asFunction() == b.asFunction();
}
if (a.isBuiltinFunction() && b.isBuiltinFunction()) {
// Builtin functions are equal only if they are the same object
return a.asBuiltinFunction() == b.asBuiltinFunction();
}
// Cross-type comparisons that make sense
if (a.isNumber() && b.isBoolean()) { if (a.isNumber() && b.isBoolean()) {
// Numbers and booleans: 0 and false are equal, non-zero and true are equal return b.asBoolean() ? (a.asNumber() != 0.0) : (a.asNumber() == 0.0);
if (b.asBoolean()) {
return a.asNumber() != 0.0;
} else {
return a.asNumber() == 0.0;
} }
}
if (a.isBoolean() && b.isNumber()) { if (a.isBoolean() && b.isNumber()) {
// Same as above, but reversed return a.asBoolean() ? (b.asNumber() != 0.0) : (b.asNumber() == 0.0);
if (a.asBoolean()) {
return b.asNumber() != 0.0;
} else {
return b.asNumber() == 0.0;
} }
} return a.equals(b);
// For all other type combinations, return false
return false;
} }
std::string RuntimeDiagnostics::stringify(Value object) { std::string RuntimeDiagnostics::stringify(Value object) { return object.toString(); }
if(object.isNone()) {
return "none";
}
else if(object.isNumber()) {
return formatNumber(object.asNumber());
}
else if(object.isString()) {
return object.asString();
}
else if(object.isBoolean()) {
return object.asBoolean() == 1 ? "true" : "false";
}
else if(object.isFunction()) {
return "<function " + object.asFunction()->name + ">";
}
else if(object.isBuiltinFunction()) {
return "<builtin_function " + object.asBuiltinFunction()->name + ">";
}
else if(object.isArray()) {
return formatArray(object.asArray());
}
else if(object.isDict()) {
return formatDict(object.asDict());
}
throw std::runtime_error("Could not convert object to string");
}
std::string RuntimeDiagnostics::formatNumber(double value) { std::string RuntimeDiagnostics::formatNumber(double value) {
double integral = value; double integral = value;
@ -150,31 +47,9 @@ std::string RuntimeDiagnostics::formatNumber(double value) {
} }
} }
std::string RuntimeDiagnostics::formatArray(const std::vector<Value>& arr) { std::string RuntimeDiagnostics::formatArray(const std::vector<Value>& arr) { return Value(arr).toString(); }
std::string result = "[";
for (size_t i = 0; i < arr.size(); i++) { std::string RuntimeDiagnostics::formatDict(const std::unordered_map<std::string, Value>& dict) { return Value(dict).toString(); }
if (i > 0) result += ", ";
result += stringify(arr[i]);
}
result += "]";
return result;
}
std::string RuntimeDiagnostics::formatDict(const std::unordered_map<std::string, Value>& dict) {
std::string result = "{";
bool first = true;
for (const auto& pair : dict) {
if (!first) result += ", ";
result += "\"" + pair.first + "\": " + stringify(pair.second);
first = false;
}
result += "}";
return result;
}
void RuntimeDiagnostics::cleanupUnusedFunctions(std::vector<std::shared_ptr<BuiltinFunction>>& functions) { void RuntimeDiagnostics::cleanupUnusedFunctions(std::vector<std::shared_ptr<BuiltinFunction>>& functions) {
// Only remove functions that are definitely not referenced anywhere (use_count == 1) // Only remove functions that are definitely not referenced anywhere (use_count == 1)
@ -212,25 +87,7 @@ void RuntimeDiagnostics::cleanupUnusedThunks(std::vector<std::shared_ptr<Thunk>>
); );
} }
void RuntimeDiagnostics::forceCleanup(std::vector<std::shared_ptr<BuiltinFunction>>& functions,
std::vector<std::shared_ptr<Thunk>>& thunks) {
// More aggressive cleanup when breaking array references
functions.erase(
std::remove_if(functions.begin(), functions.end(),
[](const std::shared_ptr<BuiltinFunction>& func) {
return func.use_count() <= 2; // More aggressive than == 1
}),
functions.end()
);
thunks.erase(
std::remove_if(thunks.begin(), thunks.end(),
[](const std::shared_ptr<Thunk>& thunk) {
return thunk.use_count() <= 2; // More aggressive than == 1
}),
thunks.end()
);
}
void RuntimeDiagnostics::forceCleanup(std::vector<std::shared_ptr<BuiltinFunction>>& builtinFunctions, void RuntimeDiagnostics::forceCleanup(std::vector<std::shared_ptr<BuiltinFunction>>& builtinFunctions,
std::vector<std::shared_ptr<Function>>& functions, std::vector<std::shared_ptr<Function>>& functions,

View File

@ -1,4 +1,4 @@
#include "TypeWrapper.h"
#include "TypeWrapper.h" #include "TypeWrapper.h"
#include <iostream> #include <iostream>

View File

@ -4,5 +4,12 @@
const Value NONE_VALUE = Value(); const Value NONE_VALUE = Value();
const Value TRUE_VALUE = Value(true); const Value TRUE_VALUE = Value(true);
const Value FALSE_VALUE = Value(false); const Value FALSE_VALUE = Value(false);
const Value ZERO_VALUE = Value(0.0);
const Value ONE_VALUE = Value(1.0); // Helper to format module string safely with complete type available in this TU
std::string formatModuleForToString(const std::shared_ptr<Module>& mod) {
if (mod && !mod->name.empty()) {
return std::string("<module '") + mod->name + "'>";
}
return std::string("<module>");
}

View File

@ -76,104 +76,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(printRawFunc); interpreter.addBuiltinFunction(printRawFunc);
// Create a built-in len function for arrays and strings
auto lenFunc = std::make_shared<BuiltinFunction>("len",
[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].isArray()) {
return Value(static_cast<double>(args[0].asArray().size()));
} else if (args[0].isString()) {
return Value(static_cast<double>(args[0].asString().length()));
} else if (args[0].isDict()) {
return Value(static_cast<double>(args[0].asDict().size()));
} else {
if (errorReporter) {
errorReporter->reportError(line, column, "StdLib Error",
"len() can only be used on arrays, strings, and dictionaries", "", true);
}
throw std::runtime_error("len() can only be used on arrays, strings, and dictionaries");
}
});
env->define("len", Value(lenFunc));
// Store the shared_ptr in the interpreter to keep it alive
interpreter.addBuiltinFunction(lenFunc);
// Create a built-in push function for arrays
auto pushFunc = std::make_shared<BuiltinFunction>("push",
[errorReporter](std::vector<Value> args, int line, int column) -> Value {
if (args.size() < 2) {
if (errorReporter) {
errorReporter->reportError(line, column, "StdLib Error",
"Expected at least 2 arguments but got " + std::to_string(args.size()) + ".", "", true);
}
throw std::runtime_error("Expected at least 2 arguments but got " + std::to_string(args.size()) + ".");
}
if (!args[0].isArray()) {
if (errorReporter) {
errorReporter->reportError(line, column, "StdLib Error",
"First argument to push() must be an array", "", true);
}
throw std::runtime_error("First argument to push() must be an array");
}
// Get the array and modify it in place
std::vector<Value>& arr = args[0].asArray();
// Add all arguments except the first one (which is the array)
for (size_t i = 1; i < args.size(); i++) {
arr.push_back(args[i]);
}
return args[0]; // Return the modified array
});
env->define("push", Value(pushFunc));
interpreter.addBuiltinFunction(pushFunc);
// Create a built-in pop function for arrays
auto popFunc = std::make_shared<BuiltinFunction>("pop",
[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].isArray()) {
if (errorReporter) {
errorReporter->reportError(line, column, "StdLib Error",
"pop() can only be used on arrays", "", true);
}
throw std::runtime_error("pop() can only be used on arrays");
}
std::vector<Value>& arr = args[0].asArray();
if (arr.empty()) {
if (errorReporter) {
errorReporter->reportError(line, column, "StdLib Error",
"Cannot pop from empty array", "", true);
}
throw std::runtime_error("Cannot pop from empty array");
}
// Get the last element and remove it from the array
Value lastElement = arr.back();
arr.pop_back();
return lastElement; // Return the popped element
});
env->define("pop", Value(popFunc));
interpreter.addBuiltinFunction(popFunc);
// Create a built-in assert function // Create a built-in assert function
auto assertFunc = std::make_shared<BuiltinFunction>("assert", auto assertFunc = std::make_shared<BuiltinFunction>("assert",
@ -216,27 +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 microseconds since Unix epoch) // 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()) + ".");
}
auto now = std::chrono::high_resolution_clock::now();
auto duration = now.time_since_epoch();
auto microseconds = std::chrono::duration_cast<std::chrono::microseconds>(duration).count();
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",
@ -293,6 +176,8 @@ void BobStdLib::addToEnvironment(std::shared_ptr<Environment> env, Interpreter&
typeName = "array"; typeName = "array";
} else if (args[0].isDict()) { } else if (args[0].isDict()) {
typeName = "dict"; typeName = "dict";
} else if (args[0].isModule()) {
typeName = "module";
} else { } else {
typeName = "unknown"; typeName = "unknown";
} }
@ -405,444 +290,60 @@ 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(toBooleanFunc); interpreter.addBuiltinFunction(toBooleanFunc);
// Create a built-in exit function to terminate the program // exit moved to sys module
auto exitFunc = std::make_shared<BuiltinFunction>("exit",
[](std::vector<Value> args, int line, int column) -> Value {
int exitCode = 0; // Default exit code
if (args.size() > 0) { // sleep moved into builtin time module
if (args[0].isNumber()) {
exitCode = static_cast<int>(args[0].asNumber());
}
// If not a number, just use default exit code 0
}
std::exit(exitCode); // Introspection: dir(obj) and functions(obj)
auto dirFunc = std::make_shared<BuiltinFunction>("dir",
[](std::vector<Value> args, int, int) -> Value {
if (args.size() != 1) return Value(std::vector<Value>{});
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));
}
} else if (obj.isDict()) {
const auto& d = obj.asDict();
for (const auto& kv : d) out.push_back(Value(kv.first));
}
return Value(out);
}); });
env->define("exit", Value(exitFunc)); env->define("dir", Value(dirFunc));
interpreter.addBuiltinFunction(dirFunc);
// Store the shared_ptr in the interpreter to keep it alive auto functionsFunc = std::make_shared<BuiltinFunction>("functions",
interpreter.addBuiltinFunction(exitFunc); [](std::vector<Value> args, int, int) -> Value {
if (args.size() != 1) return Value(std::vector<Value>{});
// Create a built-in sleep function for animations and timing Value obj = args[0];
auto sleepFunc = std::make_shared<BuiltinFunction>("sleep", std::vector<Value> out;
[errorReporter](std::vector<Value> args, int line, int column) -> Value { auto pushIfFn = [&out](const std::pair<const std::string, Value>& kv){
if (args.size() != 1) { if (kv.second.isFunction() || kv.second.isBuiltinFunction()) out.push_back(Value(kv.first));
if (errorReporter) { };
errorReporter->reportError(line, column, "StdLib Error", if (obj.isModule()) {
"Expected 1 argument but got " + std::to_string(args.size()) + ".", "", true); auto* mod = obj.asModule();
if (mod && mod->exports) {
for (const auto& kv : *mod->exports) pushIfFn(kv);
} }
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) pushIfFn(kv);
} }
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("functions", Value(functionsFunc));
interpreter.addBuiltinFunction(functionsFunc);
// Store the shared_ptr in the interpreter to keep it alive // random moved to rand module
interpreter.addBuiltinFunction(sleepFunc);
// Create a built-in random function // (eval and evalFile moved to eval module)
auto randomFunc = std::make_shared<BuiltinFunction>("random",
[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()) + ".");
}
// Seed the random number generator if not already done
static bool seeded = false;
if (!seeded) {
srand(static_cast<unsigned int>(time(nullptr)));
seeded = true;
}
return Value(static_cast<double>(rand()) / RAND_MAX);
});
env->define("random", Value(randomFunc));
// Store the shared_ptr in the interpreter to keep it alive // (file I/O moved to io module)
interpreter.addBuiltinFunction(randomFunc);
// Create a built-in eval function (like Python's eval) // memoryUsage moved to sys 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();
try {
// 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()));
}
});
env->define("eval", Value(evalFunc));
// Store the shared_ptr in the interpreter to keep it alive
interpreter.addBuiltinFunction(evalFunc);
// Create a built-in keys function for dictionaries
auto keysFunc = std::make_shared<BuiltinFunction>("keys",
[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].isDict()) {
if (errorReporter) {
errorReporter->reportError(line, column, "StdLib Error",
"keys() can only be used on dictionaries", "", true);
}
throw std::runtime_error("keys() can only be used on dictionaries");
}
const std::unordered_map<std::string, Value>& dict = args[0].asDict();
std::vector<Value> keys;
for (const auto& pair : dict) {
keys.push_back(Value(pair.first));
}
return Value(keys);
});
env->define("keys", Value(keysFunc));
interpreter.addBuiltinFunction(keysFunc);
// Create a built-in values function for dictionaries
auto valuesFunc = std::make_shared<BuiltinFunction>("values",
[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].isDict()) {
if (errorReporter) {
errorReporter->reportError(line, column, "StdLib Error",
"values() can only be used on dictionaries", "", true);
}
throw std::runtime_error("values() can only be used on dictionaries");
}
const std::unordered_map<std::string, Value>& dict = args[0].asDict();
std::vector<Value> values;
for (const auto& pair : dict) {
values.push_back(pair.second);
}
return Value(values);
});
env->define("values", Value(valuesFunc));
interpreter.addBuiltinFunction(valuesFunc);
// Create a built-in has function for dictionaries
auto hasFunc = std::make_shared<BuiltinFunction>("has",
[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].isDict()) {
if (errorReporter) {
errorReporter->reportError(line, column, "StdLib Error",
"First argument to has() must be a dictionary", "", true);
}
throw std::runtime_error("First argument to has() must be a dictionary");
}
if (!args[1].isString()) {
if (errorReporter) {
errorReporter->reportError(line, column, "StdLib Error",
"Second argument to has() must be a string", "", true);
}
throw std::runtime_error("Second argument to has() must be a string");
}
const std::unordered_map<std::string, Value>& dict = args[0].asDict();
std::string key = args[1].asString();
return Value(dict.find(key) != dict.end());
});
env->define("has", Value(hasFunc));
interpreter.addBuiltinFunction(hasFunc);
// Create a built-in readFile function
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)
auto memoryUsageFunc = std::make_shared<BuiltinFunction>("memoryUsage",
[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()) + ".");
}
// Platform-specific memory usage detection
size_t memoryBytes = 0;
#if defined(__APPLE__) && defined(__MACH__)
// macOS
struct mach_task_basic_info info;
mach_msg_type_number_t infoCount = MACH_TASK_BASIC_INFO_COUNT;
if (task_info(mach_task_self(), MACH_TASK_BASIC_INFO, (task_info_t)&info, &infoCount) == KERN_SUCCESS) {
memoryBytes = info.resident_size;
}
#elif defined(__linux__)
// Linux - read from /proc/self/status
std::ifstream statusFile("/proc/self/status");
std::string line;
while (std::getline(statusFile, line)) {
if (line.substr(0, 6) == "VmRSS:") {
std::istringstream iss(line);
std::string label, value, unit;
iss >> label >> value >> unit;
memoryBytes = std::stoull(value) * 1024; // Convert KB to bytes
break;
}
}
#elif defined(_WIN32)
// Windows
PROCESS_MEMORY_COUNTERS pmc;
if (GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof(pmc))) {
memoryBytes = pmc.WorkingSetSize;
}
#endif
// Return memory usage in MB for readability
double memoryMB = static_cast<double>(memoryBytes) / (1024.0 * 1024.0);
return Value(memoryMB);
});
env->define("memoryUsage", Value(memoryUsageFunc));
interpreter.addBuiltinFunction(memoryUsageFunc);
} }

View File

@ -786,9 +786,9 @@ print("Comprehensive number tests: PASS");
// TEST 29: TIME FUNCTION // TEST 29: TIME FUNCTION
// ======================================== // ========================================
print("\n--- Test 29: Time Function ---"); print("\n--- Test 29: Time Function ---");
import time;
var start_time = time(); var start_time = time.now();
var end_time = time(); var end_time = time.now();
var duration = end_time - start_time; var duration = end_time - start_time;
assert(start_time > 0, "Start time should be positive"); assert(start_time > 0, "Start time should be positive");
assert(end_time >= start_time, "End time should be >= start time"); assert(end_time >= start_time, "End time should be >= start time");
@ -2310,15 +2310,15 @@ print("\n--- Test 50: Arrays ---");
// Array creation // Array creation
var emptyArray = []; var emptyArray = [];
assert(len(emptyArray) == 0, "Empty array length"); assert(emptyArray.len() == 0, "Empty array length");
var numberArray = [1, 2, 3, 4, 5]; var numberArray = [1, 2, 3, 4, 5];
assert(len(numberArray) == 5, "Number array length"); assert(numberArray.len() == 5, "Number array length");
assert(numberArray[0] == 1, "Array indexing - first element"); assert(numberArray[0] == 1, "Array indexing - first element");
assert(numberArray[4] == 5, "Array indexing - last element"); assert(numberArray[4] == 5, "Array indexing - last element");
var mixedArray = [1, "hello", true, 3.14]; var mixedArray = [1, "hello", true, 3.14];
assert(len(mixedArray) == 4, "Mixed array length"); assert(mixedArray.len() == 4, "Mixed array length");
assert(mixedArray[0] == 1, "Mixed array - number"); assert(mixedArray[0] == 1, "Mixed array - number");
assert(mixedArray[1] == "hello", "Mixed array - string"); assert(mixedArray[1] == "hello", "Mixed array - string");
assert(mixedArray[2] == true, "Mixed array - boolean"); assert(mixedArray[2] == true, "Mixed array - boolean");
@ -2333,35 +2333,35 @@ assert(mixedArray[1] == "world", "Array string assignment");
// Array operations // Array operations
var testArray = [1, 2, 3]; var testArray = [1, 2, 3];
push(testArray, 4); testArray.push(4);
assert(len(testArray) == 4, "Array push - length"); assert(testArray.len() == 4, "Array push - length");
assert(testArray[3] == 4, "Array push - value"); assert(testArray[3] == 4, "Array push - value");
var poppedValue = pop(testArray); var poppedValue = testArray.pop();
assert(poppedValue == 4, "Array pop - returned value"); assert(poppedValue == 4, "Array pop - returned value");
assert(len(testArray) == 3, "Array pop - length"); assert(testArray.len() == 3, "Array pop - length");
// Array with nested arrays // Array with nested arrays
var nestedArray = [[1, 2], [3, 4], [5, 6]]; var nestedArray = [[1, 2], [3, 4], [5, 6]];
assert(len(nestedArray) == 3, "Nested array length"); assert(nestedArray.len() == 3, "Nested array length");
assert(nestedArray[0][0] == 1, "Nested array indexing"); assert(nestedArray[0][0] == 1, "Nested array indexing");
// Array with function calls // Array with function calls
var funcArray = [func() { return 42; }, func() { return "hello"; }]; var funcArray = [func() { return 42; }, func() { return "hello"; }];
assert(len(funcArray) == 2, "Function array length"); assert(funcArray.len() == 2, "Function array length");
assert(funcArray[0]() == 42, "Function array - first function"); assert(funcArray[0]() == 42, "Function array - first function");
assert(funcArray[1]() == "hello", "Function array - second function"); assert(funcArray[1]() == "hello", "Function array - second function");
// Array edge cases // Array edge cases
var singleElement = [42]; var singleElement = [42];
assert(len(singleElement) == 1, "Single element array"); assert(singleElement.len() == 1, "Single element array");
assert(singleElement[0] == 42, "Single element access"); assert(singleElement[0] == 42, "Single element access");
var largeArray = []; var largeArray = [];
for (var i = 0; i < 100; i = i + 1) { for (var i = 0; i < 100; i = i + 1) {
push(largeArray, i); largeArray.push(i);
} }
assert(len(largeArray) == 100, "Large array creation"); assert(largeArray.len() == 100, "Large array creation");
assert(largeArray[50] == 50, "Large array access"); assert(largeArray[50] == 50, "Large array access");
print("Arrays: PASS"); print("Arrays: PASS");
@ -2373,40 +2373,40 @@ print("\n--- Test 51: Array Built-in Functions ---");
// len() function // len() function
var testLenArray = [1, 2, 3, 4, 5]; var testLenArray = [1, 2, 3, 4, 5];
assert(len(testLenArray) == 5, "len() with array"); assert(testLenArray.len() == 5, "len() with array");
var emptyLenArray = []; var emptyLenArray = [];
assert(len(emptyLenArray) == 0, "len() with empty array"); assert(emptyLenArray.len() == 0, "len() with empty array");
// len() with strings // len() with strings
assert(len("hello") == 5, "len() with string"); assert("hello".len() == 5, "len() with string");
assert(len("") == 0, "len() with empty string"); assert("".len() == 0, "len() with empty string");
// push() function // push() function
var pushArray = [1, 2, 3]; var pushArray = [1, 2, 3];
push(pushArray, 4); pushArray.push(4);
assert(len(pushArray) == 4, "push() - length check"); assert(pushArray.len() == 4, "push() - length check");
assert(pushArray[3] == 4, "push() - value check"); assert(pushArray[3] == 4, "push() - value check");
push(pushArray, "hello"); pushArray.push("hello");
assert(len(pushArray) == 5, "push() - mixed types"); assert(pushArray.len() == 5, "push() - mixed types");
assert(pushArray[4] == "hello", "push() - string value"); assert(pushArray[4] == "hello", "push() - string value");
// pop() function // pop() function
var popArray = [1, 2, 3, 4]; var popArray = [1, 2, 3, 4];
var popped1 = pop(popArray); var popped1 = popArray.pop();
assert(popped1 == 4, "pop() - returned value"); assert(popped1 == 4, "pop() - returned value");
assert(len(popArray) == 3, "pop() - length after pop"); assert(popArray.len() == 3, "pop() - length after pop");
var popped2 = pop(popArray); var popped2 = popArray.pop();
assert(popped2 == 3, "pop() - second pop"); assert(popped2 == 3, "pop() - second pop");
assert(len(popArray) == 2, "pop() - length after second pop"); assert(popArray.len() == 2, "pop() - length after second pop");
// pop() edge cases // pop() edge cases
var singlePopArray = [42]; var singlePopArray = [42];
var singlePopped = pop(singlePopArray); var singlePopped = singlePopArray.pop();
assert(singlePopped == 42, "pop() - single element"); assert(singlePopped == 42, "pop() - single element");
assert(len(singlePopArray) == 0, "pop() - empty after single pop"); assert(singlePopArray.len() == 0, "pop() - empty after single pop");
print("Array built-in functions: PASS"); print("Array built-in functions: PASS");
@ -2416,15 +2416,17 @@ print("Array built-in functions: PASS");
print("\n--- Test 52: New Built-in Functions ---"); print("\n--- Test 52: New Built-in Functions ---");
// sleep() function // sleep() function
var startTime = time(); import time as T;
sleep(0.001); // Sleep for 1ms (much shorter for testing) var startTime = T.now();
var endTime = time(); T.sleep(0.01); // Sleep briefly to ensure elapsed time
var endTime = T.now();
assert(endTime > startTime, "sleep() - time elapsed"); assert(endTime > startTime, "sleep() - time elapsed");
// random() function - test proper seeding // random() function - test proper seeding
var random1 = random(); import rand as R;
var random2 = random(); var random1 = R.random();
var random3 = random(); var random2 = R.random();
var random3 = R.random();
assert(random1 >= 0 && random1 <= 1, "random() - range check 1"); assert(random1 >= 0 && random1 <= 1, "random() - range check 1");
assert(random2 >= 0 && random2 <= 1, "random() - range check 2"); assert(random2 >= 0 && random2 <= 1, "random() - range check 2");
assert(random3 >= 0 && random3 <= 1, "random() - range check 3"); assert(random3 >= 0 && random3 <= 1, "random() - range check 3");
@ -2432,30 +2434,44 @@ assert(random3 >= 0 && random3 <= 1, "random() - range check 3");
assert(random1 != random2 || random2 != random3 || random1 != random3, "random() - different values"); assert(random1 != random2 || random2 != random3 || random1 != random3, "random() - different values");
// Test random number generation in different ranges // Test random number generation in different ranges
var randomRange1 = random() * 10; var randomRange1 = R.random() * 10;
var randomRange2 = random() * 100; var randomRange2 = R.random() * 100;
assert(randomRange1 >= 0 && randomRange1 <= 10, "random() - range 0-10"); assert(randomRange1 >= 0 && randomRange1 <= 10, "random() - range 0-10");
assert(randomRange2 >= 0 && randomRange2 <= 100, "random() - range 0-100"); assert(randomRange2 >= 0 && randomRange2 <= 100, "random() - range 0-100");
// eval() function // eval() function (now via eval module)
eval("var evalVar = 42;"); import eval as E;
E.eval("var evalVar = 42;");
assert(evalVar == 42, "eval() - variable creation"); assert(evalVar == 42, "eval() - variable creation");
eval("print(\"eval test\");"); // Should print "eval test" E.eval("print(\"eval test\");"); // Should print "eval test"
var evalResult = eval("2 + 2;"); var evalResult = E.eval("2 + 2;");
// eval() currently returns none, so we just test it doesn't crash // eval() currently returns none, so we just test it doesn't crash
// Test eval with complex expressions // Test eval with complex expressions
eval("var complexVar = [1, 2, 3];"); E.eval("var complexVar = [1, 2, 3];");
assert(len(complexVar) == 3, "eval() - complex expression"); assert(complexVar.len() == 3, "eval() - complex expression");
// Test eval with function definitions // Test eval with function definitions
eval("func evalFunc(x) { return x * 2; }"); E.eval("func evalFunc(x) { return x * 2; }");
assert(evalFunc(5) == 10, "eval() - function definition"); assert(evalFunc(5) == 10, "eval() - function definition");
print("New built-in functions: PASS"); print("New built-in functions: PASS");
// Module tests
import io as IO;
var p1 = IO.exists("tests/test_path.bob") ? "tests/test_path.bob" : "../tests/test_path.bob";
E.evalFile(p1);
var p2 = IO.exists("tests/test_base64.bob") ? "tests/test_base64.bob" : "../tests/test_base64.bob";
E.evalFile(p2);
var p3 = IO.exists("tests/test_math.bob") ? "tests/test_math.bob" : "../tests/test_math.bob";
E.evalFile(p3);
var p4 = IO.exists("tests/test_rand.bob") ? "tests/test_rand.bob" : "../tests/test_rand.bob";
E.evalFile(p4);
var p5 = IO.exists("tests/test_time.bob") ? "tests/test_time.bob" : "../tests/test_time.bob";
E.evalFile(p5);
// ======================================== // ========================================
// TEST 52.5: EXIT FUNCTION // TEST 52.5: EXIT FUNCTION
// ======================================== // ========================================
@ -2479,9 +2495,9 @@ print("\n--- Test 52.6: Enhanced Dictionary Tests ---");
// Test dictionary with none values // Test dictionary with none values
var dictWithNone = {"name": "Bob", "age": none, "city": "SF"}; var dictWithNone = {"name": "Bob", "age": none, "city": "SF"};
assert(has(dictWithNone, "name"), "Dictionary has() - existing key"); assert(dictWithNone.has("name"), "Dictionary has() - existing key");
assert(has(dictWithNone, "age"), "Dictionary has() - none value"); assert(dictWithNone.has("age"), "Dictionary has() - none value");
assert(!has(dictWithNone, "phone"), "Dictionary has() - missing key"); assert(!dictWithNone.has("phone"), "Dictionary has() - missing key");
// Test setting keys to none // Test setting keys to none
dictWithNone["age"] = none; dictWithNone["age"] = none;
@ -2489,18 +2505,18 @@ assert(dictWithNone["age"] == none, "Dictionary - setting key to none");
// Test dictionary with all none values // Test dictionary with all none values
var allNoneDict = {"a": none, "b": none, "c": none}; var allNoneDict = {"a": none, "b": none, "c": none};
var allKeys = keys(allNoneDict); var allKeys = ({"a": none, "b": none, "c": none}).keys();
var allValues = values(allNoneDict); var allValues = ({"a": none, "b": none, "c": none}).values();
assert(len(allKeys) == 3, "Dictionary - all none keys count"); assert(allKeys.len() == 3, "Dictionary - all none keys count");
assert(len(allValues) == 3, "Dictionary - all none values count"); assert(allValues.len() == 3, "Dictionary - all none values count");
// Test dictionary stress test // Test dictionary stress test
var stressDict = {}; var stressDict = {};
for (var i = 0; i < 100; i = i + 1) { for (var i = 0; i < 100; i = i + 1) {
stressDict["key" + toString(i)] = i; stressDict["key" + toString(i)] = i;
} }
assert(len(keys(stressDict)) == 100, "Dictionary stress test - keys"); assert(stressDict.keys().len() == 100, "Dictionary stress test - keys");
assert(len(values(stressDict)) == 100, "Dictionary stress test - values"); assert(stressDict.values().len() == 100, "Dictionary stress test - values");
print("Enhanced dictionary tests: PASS"); print("Enhanced dictionary tests: PASS");
@ -2551,15 +2567,15 @@ assert(emptyDict.length == 0, "Empty dict length");
assert(emptyDict.empty == true, "Empty dict empty"); assert(emptyDict.empty == true, "Empty dict empty");
// Test dict.keys and dict.values properties // Test dict.keys and dict.values properties
assert(len(dictWithProps.keys) == 3, "Dict keys property length"); assert(dictWithProps.keys.len() == 3, "Dict keys property length");
assert(len(dictWithProps.values) == 3, "Dict values property length"); assert(dictWithProps.values.len() == 3, "Dict values property length");
assert(len(emptyDict.keys) == 0, "Empty dict keys length"); assert(emptyDict.keys.len() == 0, "Empty dict keys length");
assert(len(emptyDict.values) == 0, "Empty dict values length"); assert(emptyDict.values.len() == 0, "Empty dict values length");
// Test equivalence with old functions // Test equivalence with old functions
var testDict = {"x": 10, "y": 20}; var testDict = {"x": 10, "y": 20};
assert(len(testDict.keys) == len(keys(testDict)), "Dict keys equivalent to keys() function"); assert(testDict.keys.len() == testDict.keys.len(), "Dict keys property works");
assert(len(testDict.values) == len(values(testDict)), "Dict values equivalent to values() function"); assert(testDict.values.len() == testDict.values.len(), "Dict values property works");
print(" Dict builtin properties: PASS"); print(" Dict builtin properties: PASS");
// Nested property access and assignment // Nested property access and assignment
@ -2629,7 +2645,7 @@ print("\n--- Test 52.8: Memory Management ---");
for (var i = 0; i < 1000; i = i + 1) { for (var i = 0; i < 1000; i = i + 1) {
var largeArray = []; var largeArray = [];
for (var j = 0; j < 100; j = j + 1) { for (var j = 0; j < 100; j = j + 1) {
push(largeArray, "string" + toString(j)); largeArray.push("string" + toString(j));
} }
var largeDict = {}; var largeDict = {};
@ -2703,7 +2719,7 @@ var errorArray = [1, 2, 3];
// Test array bounds checking // Test array bounds checking
// Note: These would cause runtime errors in the actual implementation // Note: These would cause runtime errors in the actual implementation
// For now, we test that the array operations work correctly // For now, we test that the array operations work correctly
assert(len(errorArray) == 3, "Array bounds - valid length"); assert(errorArray.len() == 3, "Array bounds - valid length");
// Test with valid indices // Test with valid indices
assert(errorArray[0] == 1, "Array bounds - valid index 0"); assert(errorArray[0] == 1, "Array bounds - valid index 0");
@ -2718,15 +2734,15 @@ print("\n--- Test 54: Array Performance ---");
// Test large array operations // Test large array operations
var perfArray = []; var perfArray = [];
var startTime = time(); import time as T; var startTime = T.now();
// Create large array // Create large array
for (var i = 0; i < 1000; i = i + 1) { for (var i = 0; i < 1000; i = i + 1) {
push(perfArray, i); perfArray.push(i);
} }
var midTime = time(); var midTime = T.now();
assert(len(perfArray) == 1000, "Array performance - creation"); assert(perfArray.len() == 1000, "Array performance - creation");
// Access elements // Access elements
for (var j = 0; j < 100; j = j + 1) { for (var j = 0; j < 100; j = j + 1) {
@ -2734,7 +2750,7 @@ for (var j = 0; j < 100; j = j + 1) {
assert(value == j * 10, "Array performance - access"); assert(value == j * 10, "Array performance - access");
} }
var endTime = time(); var endTime = T.now();
assert(endTime > startTime, "Array performance - time check"); assert(endTime > startTime, "Array performance - time check");
print("Array performance: PASS"); print("Array performance: PASS");
@ -2751,7 +2767,7 @@ var funcArray2 = [
func() { return 3; } func() { return 3; }
]; ];
assert(len(funcArray2) == 3, "Function array length"); assert(funcArray2.len() == 3, "Function array length");
assert(funcArray2[0]() == 1, "Function array - call 1"); assert(funcArray2[0]() == 1, "Function array - call 1");
assert(funcArray2[1]() == 2, "Function array - call 2"); assert(funcArray2[1]() == 2, "Function array - call 2");
assert(funcArray2[2]() == 3, "Function array - call 3"); assert(funcArray2[2]() == 3, "Function array - call 3");
@ -2762,18 +2778,18 @@ func createArray() {
} }
var returnedArray = createArray(); var returnedArray = createArray();
assert(len(returnedArray) == 5, "Function returning array"); assert(returnedArray.len() == 5, "Function returning array");
assert(returnedArray[0] == 1, "Function returning array - first element"); assert(returnedArray[0] == 1, "Function returning array - first element");
// Function that modifies array // Function that modifies array
func modifyArray(arr) { func modifyArray(arr) {
push(arr, 999); arr.push(999);
return arr; return arr;
} }
var modArray = [1, 2, 3]; var modArray = [1, 2, 3];
var modifiedArray = modifyArray(modArray); var modifiedArray = modifyArray(modArray);
assert(len(modifiedArray) == 4, "Function modifying array"); assert(modifiedArray.len() == 4, "Function modifying array");
assert(modifiedArray[3] == 999, "Function modifying array - new value"); assert(modifiedArray[3] == 999, "Function modifying array - new value");
print("Array with functions: PASS"); print("Array with functions: PASS");
@ -2785,11 +2801,11 @@ print("\n--- Test 56: Array Edge Cases and Advanced Features ---");
// Array with none values // Array with none values
var noneArray = [1, none, 3, none, 5]; var noneArray = [1, none, 3, none, 5];
assert(len(noneArray) == 5, "Array with none values - length"); assert(noneArray.len() == 5, "Array with none values - length");
// Note: Bob doesn't support comparing none values with == // Note: Bob doesn't support comparing none values with ==
// We can test that the array contains the expected number of elements // We can test that the array contains the expected number of elements
var noneCount = 0; var noneCount = 0;
for (var i = 0; i < len(noneArray); i = i + 1) { for (var i = 0; i < noneArray.len(); i = i + 1) {
if (type(noneArray[i]) == "none") { if (type(noneArray[i]) == "none") {
noneCount = noneCount + 1; noneCount = noneCount + 1;
} }
@ -2798,7 +2814,7 @@ assert(noneCount == 2, "Array with none values - none count");
// Array with complex expressions // Array with complex expressions
var complexArray = [1 + 1, 2 * 3, 10 / 2, 5 - 2]; var complexArray = [1 + 1, 2 * 3, 10 / 2, 5 - 2];
assert(len(complexArray) == 4, "Complex array - length"); assert(complexArray.len() == 4, "Complex array - length");
assert(complexArray[0] == 2, "Complex array - addition"); assert(complexArray[0] == 2, "Complex array - addition");
assert(complexArray[1] == 6, "Complex array - multiplication"); assert(complexArray[1] == 6, "Complex array - multiplication");
assert(complexArray[2] == 5, "Complex array - division"); assert(complexArray[2] == 5, "Complex array - division");
@ -2806,7 +2822,7 @@ assert(complexArray[3] == 3, "Complex array - subtraction");
// Array with string operations // Array with string operations
var stringArray = ["hello" + " world", "test" * 2, "a" + "b" + "c"]; var stringArray = ["hello" + " world", "test" * 2, "a" + "b" + "c"];
assert(len(stringArray) == 3, "String array - length"); assert(stringArray.len() == 3, "String array - length");
assert(stringArray[0] == "hello world", "String array - concatenation"); assert(stringArray[0] == "hello world", "String array - concatenation");
assert(stringArray[1] == "testtest", "String array - multiplication"); assert(stringArray[1] == "testtest", "String array - multiplication");
assert(stringArray[2] == "abc", "String array - multiple concatenation"); assert(stringArray[2] == "abc", "String array - multiple concatenation");
@ -2816,14 +2832,14 @@ func getValue() { return 42; }
func getString() { return "hello"; } func getString() { return "hello"; }
var funcResultArray = [getValue(), getString(), 1 + 2]; var funcResultArray = [getValue(), getString(), 1 + 2];
assert(len(funcResultArray) == 3, "Function result array - length"); assert(funcResultArray.len() == 3, "Function result array - length");
assert(funcResultArray[0] == 42, "Function result array - function call"); assert(funcResultArray[0] == 42, "Function result array - function call");
assert(funcResultArray[1] == "hello", "Function result array - string function"); assert(funcResultArray[1] == "hello", "Function result array - string function");
assert(funcResultArray[2] == 3, "Function result array - expression"); assert(funcResultArray[2] == 3, "Function result array - expression");
// Array with ternary operators // Array with ternary operators
var ternaryArray = [true ? 1 : 0, false ? "yes" : "no", 5 > 3 ? "big" : "small"]; var ternaryArray = [true ? 1 : 0, false ? "yes" : "no", 5 > 3 ? "big" : "small"];
assert(len(ternaryArray) == 3, "Ternary array - length"); assert(ternaryArray.len() == 3, "Ternary array - length");
assert(ternaryArray[0] == 1, "Ternary array - true condition"); assert(ternaryArray[0] == 1, "Ternary array - true condition");
assert(ternaryArray[1] == "no", "Ternary array - false condition"); assert(ternaryArray[1] == "no", "Ternary array - false condition");
assert(ternaryArray[2] == "big", "Ternary array - comparison condition"); assert(ternaryArray[2] == "big", "Ternary array - comparison condition");
@ -2835,13 +2851,13 @@ assert(nestedOpArray[0][0] == 99, "Nested array operations - assignment");
// Array with push/pop in expressions // Array with push/pop in expressions
var exprArray = [1, 2, 3]; var exprArray = [1, 2, 3];
var pushResult = push(exprArray, 4); var pushResult = exprArray.push(4);
assert(len(exprArray) == 4, "Array push in expression"); assert(exprArray.len() == 4, "Array push in expression");
assert(exprArray[3] == 4, "Array push result"); assert(exprArray[3] == 4, "Array push result");
var popResult = pop(exprArray); var popResult = exprArray.pop();
assert(popResult == 4, "Array pop result"); assert(popResult == 4, "Array pop result");
assert(len(exprArray) == 3, "Array length after pop"); assert(exprArray.len() == 3, "Array length after pop");
print("Array edge cases and advanced features: PASS"); print("Array edge cases and advanced features: PASS");
@ -2852,31 +2868,31 @@ print("\n--- Test 57: Array Stress Testing ---");
// Large array creation and manipulation // Large array creation and manipulation
var stressArray = []; var stressArray = [];
var startTime = time(); import time as T2; var startTime = T2.now();
// Create large array with mixed types // Create large array with mixed types
for (var i = 0; i < 500; i = i + 1) { for (var i = 0; i < 500; i = i + 1) {
if (i % 3 == 0) { if (i % 3 == 0) {
push(stressArray, i); stressArray.push(i);
} else if (i % 3 == 1) { } else if (i % 3 == 1) {
push(stressArray, "string" + toString(i)); stressArray.push("string" + toString(i));
} else { } else {
push(stressArray, i > 250); stressArray.push(i > 250);
} }
} }
var midTime = time(); var midTime = T2.now();
assert(len(stressArray) == 500, "Stress test - array creation"); assert(stressArray.len() == 500, "Stress test - array creation");
// Access and modify elements // Access and modify elements
for (var j = 0; j < 100; j = j + 1) { for (var j = 0; j < 100; j = j + 1) {
var index = j * 5; var index = j * 5;
if (index < len(stressArray)) { if (index < stressArray.len()) {
stressArray[index] = "modified" + toString(j); stressArray[index] = "modified" + toString(j);
} }
} }
var endTime = time(); var endTime = T2.now();
assert(endTime > startTime, "Stress test - time validation"); assert(endTime > startTime, "Stress test - time validation");
// Verify modifications // Verify modifications
@ -2992,7 +3008,8 @@ assert(toInt(-0.0) == 0, "toInt(-0.0) should be 0");
// Test with random numbers // Test with random numbers
for(var i = 0; i < 5; i++) { for(var i = 0; i < 5; i++) {
var randomFloat = random() * 10; import rand as Rn;
var randomFloat = Rn.random() * 10;
var randomInt = toInt(randomFloat); var randomInt = toInt(randomFloat);
assert(randomInt >= 0 && randomInt <= 9, "toInt(random) should be in range 0-9"); assert(randomInt >= 0 && randomInt <= 9, "toInt(random) should be in range 0-9");
} }
@ -3271,5 +3288,61 @@ print("- Interactive Mode Features");
print(" * REPL functionality"); print(" * REPL functionality");
print(" * Error reporting in both modes"); print(" * Error reporting in both modes");
// Additional Tests: Classes and Extensions
print("\n--- Additional Tests: Classes and Extensions ---");
import io as IO; // for file existence checks
var path1 = IO.exists("tests/test_method_calls.bob") ? "tests/test_method_calls.bob" : "../tests/test_method_calls.bob";
E.evalFile(path1);
var path2 = IO.exists("tests/test_class_basic.bob") ? "tests/test_class_basic.bob" : "../tests/test_class_basic.bob";
E.evalFile(path2);
var path3 = IO.exists("tests/test_class_with_this.bob") ? "tests/test_class_with_this.bob" : "../tests/test_class_with_this.bob";
E.evalFile(path3);
var path4 = IO.exists("tests/test_class_init.bob") ? "tests/test_class_init.bob" : "../tests/test_class_init.bob";
E.evalFile(path4);
var path5 = IO.exists("tests/test_class_extension_user.bob") ? "tests/test_class_extension_user.bob" : "../tests/test_class_extension_user.bob";
E.evalFile(path5);
var path6 = IO.exists("tests/test_extension_methods.bob") ? "tests/test_extension_methods.bob" : "../tests/test_extension_methods.bob";
E.evalFile(path6);
var path7 = IO.exists("tests/test_class_inheritance.bob") ? "tests/test_class_inheritance.bob" : "../tests/test_class_inheritance.bob";
E.evalFile(path7);
var path8 = IO.exists("tests/test_classes_comprehensive.bob") ? "tests/test_classes_comprehensive.bob" : "../tests/test_classes_comprehensive.bob";
E.evalFile(path8);
var path9 = IO.exists("tests/test_class_super.bob") ? "tests/test_class_super.bob" : "../tests/test_class_super.bob";
E.evalFile(path9);
var path10 = IO.exists("tests/test_classes_extensive.bob") ? "tests/test_classes_extensive.bob" : "../tests/test_classes_extensive.bob";
E.evalFile(path10);
var path11 = IO.exists("tests/test_class_edge_cases.bob") ? "tests/test_class_edge_cases.bob" : "../tests/test_class_edge_cases.bob";
E.evalFile(path11);
var path12 = IO.exists("tests/test_polymorphism.bob") ? "tests/test_polymorphism.bob" : "../tests/test_polymorphism.bob";
E.evalFile(path12);
var path13 = IO.exists("tests/test_polymorphism_practical.bob") ? "tests/test_polymorphism_practical.bob" : "../tests/test_polymorphism_practical.bob";
E.evalFile(path13);
var path14 = IO.exists("tests/test_builtin_methods_style.bob") ? "tests/test_builtin_methods_style.bob" : "../tests/test_builtin_methods_style.bob";
E.evalFile(path14);
var path15 = IO.exists("tests/test_try_catch.bob") ? "tests/test_try_catch.bob" : "../tests/test_try_catch.bob";
E.evalFile(path15);
var path15a = IO.exists("tests/test_try_catch_runtime.bob") ? "tests/test_try_catch_runtime.bob" : "../tests/test_try_catch_runtime.bob";
E.evalFile(path15a);
var path15b = IO.exists("tests/test_try_catch_extensive.bob") ? "tests/test_try_catch_extensive.bob" : "../tests/test_try_catch_extensive.bob";
E.evalFile(path15b);
var path15c = IO.exists("tests/test_try_catch_edge_cases2.bob") ? "tests/test_try_catch_edge_cases2.bob" : "../tests/test_try_catch_edge_cases2.bob";
E.evalFile(path15c);
var path15d = IO.exists("tests/test_try_catch_cross_function.bob") ? "tests/test_try_catch_cross_function.bob" : "../tests/test_try_catch_cross_function.bob";
E.evalFile(path15d);
var path15e = IO.exists("tests/test_try_catch_loop_interactions.bob") ? "tests/test_try_catch_loop_interactions.bob" : "../tests/test_try_catch_loop_interactions.bob";
E.evalFile(path15e);
// Modules: basic imports suite
var pathMods = IO.exists("tests/test_imports_basic.bob") ? "tests/test_imports_basic.bob" : "../tests/test_imports_basic.bob";
E.evalFile(pathMods);
var pathModsB = IO.exists("tests/test_imports_builtin.bob") ? "tests/test_imports_builtin.bob" : "../tests/test_imports_builtin.bob";
E.evalFile(pathModsB);
var pathOs = IO.exists("tests/test_os_basic.bob") ? "tests/test_os_basic.bob" : "../tests/test_os_basic.bob";
E.evalFile(pathOs);
print("\nAll tests passed."); print("\nAll tests passed.");
print("Test suite complete."); print("Test suite complete.");

139
tests.bob
View File

@ -1,53 +1,96 @@
var a = []; // // var a = [];
for(var i = 0; i < 1000000; i++){ // // for(var i = 0; i < 1000000; i++){
print(i); // // print(i);
// Create nested structures with functions at different levels // // // Create nested structures with functions at different levels
if (i % 4 == 0) { // // if (i % 4 == 0) {
// Nested array with function // // // Nested array with function
push(a, [ // // push(a, [
func(){print("Array nested func i=" + i); return i;}, // // func(){print("Array nested func i=" + i); return i;},
[func(){return "Deep array func " + i;}], // // [func(){return "Deep array func " + i;}],
i // // i
]); // // ]);
} else if (i % 4 == 1) { // // } else if (i % 4 == 1) {
// Nested dict with function // // // Nested dict with function
push(a, { // // push(a, {
"func": func(){print("Dict func i=" + i); return i;}, // // "func": func(){print("Dict func i=" + i); return i;},
"nested": {"deepFunc": func(){return "Deep dict func " + i;}}, // // "nested": {"deepFunc": func(){return "Deep dict func " + i;}},
"value": i // // "value": i
}); // // });
} else if (i % 4 == 2) { // // } else if (i % 4 == 2) {
// Mixed nested array/dict with functions // // // Mixed nested array/dict with functions
push(a, [ // // push(a, [
{"arrayInDict": func(){return "Mixed " + i;}}, // // {"arrayInDict": func(){return "Mixed " + i;}},
[func(){return "Array in array " + i;}, {"more": func(){return i;}}], // // [func(){return "Array in array " + i;}, {"more": func(){return i;}}],
func(){print("Top level in mixed i=" + i); return i;} // // func(){print("Top level in mixed i=" + i); return i;}
]); // // ]);
} else { // // } else {
// Simple function (original test case) // // // Simple function (original test case)
push(a, func(){print("Simple func i=" + i); return toString(i);}); // // push(a, func(){print("Simple func i=" + i); return toString(i);});
} // // }
} // // }
print("Before: " + len(a)); // // print("Before: " + len(a));
print("Memory usage: " + memoryUsage() + " MB"); // // print("Memory usage: " + memoryUsage() + " MB");
// Test different types of nested function calls // // // Test different types of nested function calls
a[3691](); // Simple function // // a[3691](); // Simple function
if (len(a[3692]) > 0) { // // if (len(a[3692]) > 0) {
a[3692][0](); // Nested array function // // a[3692][0](); // Nested array function
} // // }
if (a[3693]["func"]) { // // if (a[3693]["func"]) {
a[3693]["func"](); // Nested dict function // // a[3693]["func"](); // Nested dict function
} // // }
print(a); // // //print(a);
//writeFile("array_contents.txt", toString(a)); // // //writeFile("array_contents.txt", toString(a));
print("Array contents written to array_contents.txt"); // // print("Array contents written to array_contents.txt");
print("Memory before cleanup: " + memoryUsage() + " MB"); // // print("Memory before cleanup: " + memoryUsage() + " MB");
input("Press any key to free memory"); // // input("Press any key to free memory");
a = none; // // a = none;
print("Memory after cleanup: " + memoryUsage() + " MB"); // // print("Memory after cleanup: " + memoryUsage() + " MB");
input("waiting..."); // // input("waiting...");
// class Test {
// func init() {
// print("Test init" + this.a);
// }
// var a = 10;
// func test() {
// //print(a);
// print(this.a);
// }
// }
// var arr = [];
// for(var i = 0; i < 100; i++){
// arr.push(i);
// }
// var counter = 0;
// try{
// while(true){
// print(arr[counter]);
// counter++;
// //sleep(0.01);
// }
// }catch(){}
// try{
// assert(false);
// }catch(){}
// print("done");
// 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");

5
tests/debug_any_tag.bob Normal file
View File

@ -0,0 +1,5 @@
extension any { func tag() { return "<" + toString(this) + ">"; } }
var plain = { "x": 1 };
print(plain.tag());

View File

@ -0,0 +1,7 @@
class Animal { var name; func init(n) { this.name = n; } }
extension Animal { func speak() { return this.name + "?"; } }
class Dog extends Animal { }
var d = Dog("fido");
print(d.speak());

View File

@ -0,0 +1,3 @@
class P { func init(f,l) { print(f + ":" + l); } }
var p = P("A","B");

View File

@ -0,0 +1,8 @@
class T {
var a = "";
func init(x, y) { this.a = x + y; }
}
var t = T("A","B");
print(t.a);

View File

@ -0,0 +1,6 @@
class M0 { func name() { return "M0"; } }
class M1 extends M0 {}
extension M1 { func name() { return super.name() + "*"; } }
print(M1().name());

View File

@ -0,0 +1,12 @@
class A {}
class B extends A {}
class C extends B {}
extension A { func v() { return 1; } }
extension B { func v() { return super.v() + 1; } }
extension C { func v() { return super.v() + 1; } }
var c = C();
print(c.v());

10
tests/debug_super_min.bob Normal file
View File

@ -0,0 +1,10 @@
class A {}
class B extends A {}
extension A { func v() { return 1; } }
extension B { func v() { return super.v() + 1; } }
var b = B();
print(b.v());

View File

@ -0,0 +1,5 @@
// uses mod_hello without importing it here; relies on prior import in same interpreter
var ok1 = (mod_hello.greet("Z") == "hi Z");
assert(ok1, "imported module binding visible across eval files in same interpreter");
print("import user: PASS");

3
tests/mod_hello.bob Normal file
View File

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

11
tests/test_base64.bob Normal file
View File

@ -0,0 +1,11 @@
print("\n--- Test: base64 module ---");
import base64;
var s = "hello world";
var enc = base64.encode(s);
assert(enc == "aGVsbG8gd29ybGQ=", "base64.encode");
var dec = base64.decode(enc);
assert(dec == s, "base64.decode roundtrip");
print("base64: PASS");

View File

@ -0,0 +1,24 @@
// Method-style builtins on arrays, strings, dicts, and numbers
var a = [1, 2];
assert(a.len() == 2, "array.len()");
a.push(3, 4);
assert(a.len() == 4, "array.push(...)");
assert(a.pop() == 4, "array.pop() value");
assert(a.len() == 3, "array.pop() length");
assert(a[2] == 3, "array after push/pop");
var s = "hello";
assert(s.len() == 5, "string.len()");
var d = {"x": 1, "y": 2};
assert(d.len() == 2, "dict.len()");
var ks = d.keys();
var vs = d.values();
assert(ks.len() == 2, "dict.keys().len()");
assert(vs.len() == 2, "dict.values().len()");
assert(d.has("x"), "dict.has(true)");
assert(!d.has("z"), "dict.has(false)");
var n = 3.9;
assert(n.toInt() == 3, "number.toInt()");

View File

@ -0,0 +1,77 @@
print("\n--- Class + Extension Syntax Proposal (Spec Tests) ---");
// These tests define the intended behavior and syntax. The parser/runtime
// will be implemented to satisfy them.
// Class declaration
class Person {
var name;
var age;
func init(name, age) {
this.name = name;
this.age = age;
}
func greet() {
print("Hi, I'm " + this.name + " (" + toString(this.age) + ")");
}
func birthday() {
this.age = this.age + 1;
}
}
// Instantiation + method calls
// var p = Person("Bob", 30);
// p.greet(); // "Hi, I'm Bob (30)"
// p.birthday();
// p.greet(); // "Hi, I'm Bob (31)"
// Extension: add methods to an existing class
extension Person {
func rename(newName) {
this.name = newName;
}
}
// p.rename("Robert");
// p.greet(); // "Hi, I'm Robert (31)"
// Extension on built-ins (string, array, dict are the type names)
extension string {
func repeat(n) {
var out = "";
var i = 0;
while (i < n) {
out = out + this;
i = i + 1;
}
return out;
}
}
// assert("ha".repeat(3) == "hahaha", "string.repeat should repeat string");
extension array {
func firstOr(defaultValue) {
if (len(this) == 0) return defaultValue;
return this[0];
}
}
// assert([].firstOr("none") == "none", "array.firstOr should return default on empty");
// Extension on any: adds a method to all objects
extension any {
func debug() {
return type(this) + ":" + toString(this);
}
}
// assert(42.debug() == "number:42", "any.debug should work on numbers");
// assert("x".debug() == "string:x", "any.debug should work on strings");
print("Class + Extension spec tests defined (pending parser/runtime support).");

View File

@ -0,0 +1,20 @@
print("\n--- Test: Basic Class (no init, no this) ---");
class Foo {
var x;
func ping() { print("ping"); }
}
var a = Foo();
a.ping();
// Field default should be none
assert(type(a.x) == "none", "default field value should be none");
// Property assignment on instance
a.x = 42;
assert(a.x == 42, "instance property set/get should work");
print("Basic class: PASS");

View File

@ -0,0 +1,52 @@
print("\n--- Test: Class Edge Cases ---");
// 1) 'this' inside extension should read/write fields
class E1 { var x = 1; }
extension E1 { func bump() { this.x = this.x + 1; return this.x; } }
var e1 = E1();
assert(e1.bump() == 2, "extension writes this");
assert(e1.x == 2, "field reflects extension write");
// 2) Property overshadowing method: user property wins
class ShM { func m() { return 10; } }
var shm = ShM();
shm.m = 7;
assert(shm.m == 7, "property overshadows method value lookup");
// 3) Field name same as method; property has precedence
class FvM { var id = 1; func id() { return 99; } }
var fvm = FvM();
assert(fvm.id == 1, "property shadows method with same name");
// Replace with property; still property
fvm.id = 5;
assert(fvm.id == 5, "property remains in precedence after reassignment");
// 4) Late extension overrides previous extension
class Redef { }
extension Redef { func v() { return 1; } }
assert(Redef().v() == 1, "initial ext");
extension Redef { func v() { return 2; } }
assert(Redef().v() == 2, "redefined ext");
// 5) Super in multi-level extension chain already covered; add deeper chain guard
class S0 {}
class S1 extends S0 {}
class S2 extends S1 {}
class S3 extends S2 {}
extension S0 { func v() { return 1; } }
extension S1 { func v() { return super.v() + 1; } }
extension S2 { func v() { return super.v() + 1; } }
extension S3 { func v() { return super.v() + 1; } }
assert(S3().v() == 4, "deep super chain");
// 6) Built-in type extension coexists with class extension precedence
class N1 {}
extension number { func plus1() { return this + 1; } }
extension N1 { func plus1() { return 100; } }
var n1 = N1();
assert(5.plus1() == 6, "builtin number ext works");
assert(n1.plus1() == 100, "class ext wins over any/builtin");
print("Class edge cases: PASS");

View File

@ -0,0 +1,21 @@
print("\n--- Test: User class extensions ---");
class Box {
var v;
func init(x) { this.v = x; }
func get() { return this.v; }
}
extension Box {
func inc() { this.v = this.v + 1; }
func label(s) { return s + ":" + toString(this.v); }
}
var b = Box(10);
b.inc();
assert(b.get() == 11, "Box.inc should increment value");
assert(b.label("val") == "val:11", "Box.label should return formatted string");
print("User class extensions: PASS");

View File

@ -0,0 +1,40 @@
print("\n--- Test: Class Inheritance (extensions) ---");
class Animal { var name; func init(n) { this.name = n; } }
extension Animal { func speak() { return this.name + "?"; } }
class Dog extends Animal { }
extension Dog { func bark() { return this.name + " woof"; } }
var a = Animal("crit");
var d = Dog("fido");
assert(a.speak() == "crit?", "Animal.speak works");
assert(d.speak() == "fido?", "Dog inherits Animal.speak via extension chain");
assert(d.bark() == "fido woof", "Dog.bark works");
print("Inheritance via extensions: PASS");
print("\n--- Test: Class Inheritance (inline methods) ---");
class Animal2 {
var name;
func init(n) { this.name = n; }
func speak() { return this.name + "!"; }
}
class Dog2 extends Animal2 {
func bark() { return this.name + " woof"; }
}
var a2 = Animal2("crit");
var d2 = Dog2("fido");
assert(a2.speak() == "crit!", "Animal2.speak works (inline)");
assert(d2.speak() == "fido!", "Dog2 inherits Animal2.speak (inline)");
assert(d2.bark() == "fido woof", "Dog2.bark works (inline)");
print("Inheritance via inline methods: PASS");

15
tests/test_class_init.bob Normal file
View File

@ -0,0 +1,15 @@
print("\n--- Test: Class init auto-call with args ---");
class Person {
var name;
var age;
func init(n, a) { this.name = n; this.age = a; }
func who() { return this.name + ":" + toString(this.age); }
}
var p = Person("Bob", 33);
assert(p.who() == "Bob:33", "init should set fields from constructor args");
print("Class init: PASS");

View File

@ -0,0 +1,85 @@
print("\n--- Test: Class System Stress + Memory Observation ---");
// Class with fields and methods using `this`
class Node {
var id;
var payload;
func setId(v) { this.id = v; }
func setPayload(p) { this.payload = p; }
func sum() {
if (this.payload == none) return 0;
if (this.payload.len() == 0) return 0;
var i = 0;
var s = 0;
while (i < this.payload.len()) {
s = s + this.payload[i];
i = i + 1;
}
return s;
}
func describe() {
return "Node(" + toString(this.id) + ", sum=" + toString(this.sum()) + ")";
}
}
// Basic functional checks
var a = Node();
a.setId(7);
a.setPayload([1, 2, 4]);
assert(a.sum() == 7, "sum() should sum payload");
assert(type(a.describe()) == "string", "describe() returns string");
// Memory observation before creating many objects
var mbBefore = memoryUsage();
print("Memory before allocations (MB): " + toString(mbBefore));
// Create many objects and store them
var COUNT = 40000; // adjust as needed for your machine
var nodes = [];
var i = 0;
while (i < COUNT) {
var n = Node();
n.setId(i);
// small payload to keep variety, not too heavy
n.setPayload([i % 10, (i * 2) % 10, (i * 3) % 10]);
nodes.push(n);
i = i + 1;
}
// Spot-check a few nodes
assert(nodes[0].sum() == (0 + 0 + 0), "node0 sum");
assert(nodes[1].sum() == (1 + 2 + 3), "node1 sum");
assert(type(nodes[100].describe()) == "string", "describe() type");
// Memory after allocation
var mbAfter = memoryUsage();
print("Memory after allocations (MB): " + toString(mbAfter));
// Verify each object is callable and alive: call sum() on every node
var verify = 0;
i = 0;
while (i < COUNT) {
verify = verify + nodes[i].sum();
print(nodes[i].describe());
i = i + 1;
}
print("Verification sum: " + toString(verify));
// Pause to allow manual inspection via Activity Monitor, etc.
print("Press Enter to clear references and observe memory...");
input();
// Clear references to allow GC/release by refcounts
nodes = [];
// Observe memory after clearing
var mbCleared = memoryUsage();
print("Memory after clear (MB): " + toString(mbCleared));
print("Class stress + memory observation: DONE");

View File

@ -0,0 +1,21 @@
print("\n--- Test: super calls ---");
class A { }
class B extends A { }
class C extends B { }
extension A { func v() { return 1; } }
extension B { func v() { return super.v() + 1; } }
extension C { func v() { return super.v() + 1; } }
var a = A();
var b = B();
var c = C();
assert(a.v() == 1, "A.v");
assert(b.v() == 2, "B.v via super");
assert(c.v() == 3, "C.v via super chain");
print("super calls: PASS");

View File

@ -0,0 +1,15 @@
print("\n--- Test: Class methods with this (assignment allowed) ---");
class Person {
var name;
func setName(n) { this.name = n; }
func getName() { return this.name; }
}
var p = Person();
p.setName("Bob");
assert(p.getName() == "Bob", "this-based methods should read/write instance fields");
print("Class with this: PASS");

View File

@ -0,0 +1,94 @@
print("\n--- Test: Classes Comprehensive ---");
// Basic class with field and method using this
class Person {
var name;
func init(n) { this.name = n; }
func greet() { return "Hello " + this.name; }
}
var p = Person("Bob");
assert(p.name == "Bob", "init sets name");
assert(p.greet() == "Hello Bob", "method with this works");
// Field initializers (constant)
class Box {
var width = 2;
var height = 3;
func area() { return this.width * this.height; }
}
var b = Box();
assert(b.width == 2, "field initializer width");
assert(b.height == 3, "field initializer height");
assert(b.area() == 6, "method sees initialized fields");
// Inline method precedence over extensions
class Animal {
var name;
func init(n) { this.name = n; }
}
extension Animal { func speak() { return this.name + "?"; } }
var a = Animal("crit");
assert(a.speak() == "crit?", "Animal.speak via extension");
class Dog extends Animal {
func speak() { return this.name + "!"; }
}
var d = Dog("fido");
assert(d.speak() == "fido!", "Dog inline speak overrides Animal extension");
// Inheritance chain for extensions: define on parent only
class Cat extends Animal {}
var c = Cat("mew");
assert(c.speak() == "mew?", "Cat inherits Animal.speak extension");
// Add extension later and ensure it applies retroactively
class Bird { var name; func init(n) { this.name = n; } }
var bird = Bird("tweet");
extension Bird { func speak() { return this.name + "~"; } }
assert(bird.speak() == "tweet~", "Late extension attaches to existing instances");
// any fallback extension when nothing else matches
extension any { func tag() { return "<" + toString(this) + ">"; } }
var plain = { "x": 1 };
assert(plain.tag() == "<{" + "\"x\": 1}" + ">", "any fallback extension works on dict");
// Ensure property value shadows extension lookup
class Shadow { }
extension Shadow { func value() { return 42; } }
var sh = Shadow();
sh.value = 7;
assert(sh.value == 7, "user property shadows extension method");
// Ensure instance method shadows extension
class Shadow2 { func m() { return 1; } }
extension Shadow2 { func m() { return 2; } }
var sh2 = Shadow2();
assert(sh2.m() == 1, "instance method shadows extension method");
// Method call injection of this; method reference does not auto-bind
class Ref {
var v = 5;
func get() { return this.v; }
}
var r = Ref();
var m = r.get;
// Calling m() should fail due to missing this; instead call through property again
assert(r.get() == 5, "method call via property injects this");
// Inheritance of inline methods
class Base { func id() { return 1; } }
class Child extends Base { }
var ch = Child();
assert(ch.id() == 1, "inherit inline method");
// Parent chain precedence: Child has no say(), extension on Base should work
extension Base { func say() { return "base"; } }
assert(ch.say() == "base", "inherit extension from parent");
// Verify __class tag exists for instances (check with method that reads this)
assert(Ref().get() == 5, "instance constructed correctly");
print("Classes comprehensive: PASS");

View File

@ -0,0 +1,92 @@
print("\n--- Test: Classes Extensive ---");
// Basic class with multiple fields, initializers, and methods
class Person {
var first = "Bob";
var last = "Lucero";
var full = "Hello Bob Lucero";
func name() { return this.first + " " + this.last; }
func greet() { return "Hi " + this.name(); }
}
var p = Person();
assert(p.first == "Bob", "default field");
assert(p.last == "Lucero", "default field 2");
assert(p.name() == "Bob Lucero", "method calling another method");
assert(p.full == "Hello Bob Lucero", "field initializer");
// Separate init param binding check
class PInit { var a = ""; func init(x, y) { this.a = x + y; } }
assert(PInit("Ada","L").a == "AdaL", "init binds parameters");
// Inheritance: inline methods inherited and overridden; super to parent inline
class Animal { var n; func init(n) { this.n = n; } func speak() { return this.n + ":..."; } }
class Dog extends Animal { func speak() { return super.speak() + " woof"; } }
class LoudDog extends Dog { func speak() { return super.speak() + "!"; } }
var a = Animal("thing");
var d = Dog("fido");
var ld = LoudDog("rex");
assert(a.speak() == "thing:...", "parent inline");
assert(d.speak() == "fido:... woof", "override + super inline");
assert(ld.speak() == "rex:... woof!", "super chain inline 3 levels");
// Class extensions on parent and child, resolution order and super to extension
class Base {}
class Mid extends Base {}
class Leaf extends Mid {}
extension Base { func v() { return 1; } }
extension Mid { func v() { return super.v() + 1; } }
extension Leaf { func v() { return super.v() + 1; } }
assert(Base().v() == 1, "base ext");
assert(Mid().v() == 2, "mid super->base ext");
assert(Leaf().v() == 3, "leaf super->mid->base ext");
// Instance method shadows extension; super from child instance method to parent extension
class C1 {}
class C2 extends C1 { func m() { return super.m() + 10; } }
extension C1 { func m() { return 5; } }
assert(C2().m() == 15, "instance method super to parent extension");
// User property shadows extension method
class Shadow {}
extension Shadow { func val() { return 42; } }
var sh = Shadow(); sh.val = 7; assert(sh.val == 7, "property shadows extension");
// Late extension attaches to existing instances
class Later { var x = 2; }
var l = Later();
extension Later { func dbl() { return this.x * 2; } }
assert(l.dbl() == 4, "late extension visible");
// Polymorphism array: mixed classes share method name
class PBase { func id() { return "base"; } }
class PChild extends PBase { func id() { return "child"; } }
class PLeaf extends PChild {}
var poly = [PBase(), PChild(), PLeaf()];
assert(poly[0].id() == "base", "poly base");
assert(poly[1].id() == "child", "poly override");
assert(poly[2].id() == "child", "poly inherited override");
// any fallback not taken if class or parent provides method
class AF {}
extension any { func tag() { return "<" + toString(this) + ">"; } }
extension AF { func tag() { return "af"; } }
assert(AF().tag() == "af", "prefer class extension over any");
// Built-in extensions still work
extension number { func neg() { return -this; } }
assert(5.neg() == -5, "number extension");
// Method reference semantics: not auto-bound
class Ref { var v = 9; func get() { return this.v; } }
var r = Ref();
var rf = r.get; // function reference
assert(r.get() == 9, "call via property injects this");
// Not calling rf() directly to avoid unbound-this error; contract is explicit injection via property call
print("Classes extensive: PASS");

View File

@ -0,0 +1,35 @@
print("\n--- Test: Extension methods on built-ins ---");
extension array {
func sum() {
var i = 0;
var s = 0;
while (i < this.len()) {
s = s + this[i];
i = i + 1;
}
return s;
}
}
extension dict {
func size() { return this.len(); }
}
extension string {
func shout() { return toString(this) + "!"; }
}
extension any {
func tag() { return "<" + type(this) + ">"; }
}
assert([1,2,3].sum() == 6, "array.sum should sum elements");
assert({"a":1,"b":2}.size() == 2, "dict.size should return count");
assert("hi".shout() == "hi!", "string.shout should append !");
assert(toInt(42).tag() == "<number>", "any.tag works on numbers");
assert("x".tag() == "<string>", "any.tag works on strings");
print("Extension methods on built-ins: PASS");

View File

@ -0,0 +1,10 @@
print("\n--- Test: Extension Syntax (spec only) ---");
// Parsing should fail until extension is implemented; keep as spec placeholder
// extension Foo {
// func hello() { print("hello"); }
// }
print("Extension syntax spec placeholder.");

5
tests/test_hash.bob Normal file
View File

@ -0,0 +1,5 @@
print("\n--- Test: hash module ---");
// Placeholder: implement when hash module is added
print("hash: SKIPPED");

View File

@ -0,0 +1,48 @@
print("\n--- Test: basic imports ---");
// Import by path (with alias) - relative to this file's directory
import "mod_hello.bob" as H;
assert(type(H) == "module", "module is module");
assert(H.X == 5, "exported var");
assert(H.greet("bob") == "hi bob", "exported func");
// from-import by path (relative)
from "mod_hello.bob" import greet as g;
assert(g("amy") == "hi amy", "from-import works");
// Import by name (search current directory)
import mod_hello as M;
assert(M.X == 5 && M.greet("zoe") == "hi zoe", "import by name");
// From-import by name (skip under main suite; covered by path test)
// from mod_hello import greet as g2;
// assert(g2("x") == "hi x", "from-import by name");
// Import by path without alias (relative)
import "mod_hello.bob";
assert(type(mod_hello) == "module" && mod_hello.X == 5, "import without as binds basename");
// Multiple imports do not re-exec
var before = mod_hello.X;
import "mod_hello.bob";
var after = mod_hello.X;
assert(before == after, "module executed once (cached)");
// Cross-file visibility in same interpreter: eval user file after importing here
import eval as E;
E.evalFile("tests/import_user_of_mod_hello.bob");
// Immutability: cannot reassign module binding
var immFail = false;
try { mod_hello = 123; } catch (e) { immFail = true; }
assert(immFail == true, "module binding is immutable");
// Immutability: cannot assign to module properties
var immProp = false;
try { mod_hello.newProp = 1; } catch (e) { immProp = true; }
assert(immProp == true, "module properties are immutable");
// Type should be module
assert(type(mod_hello) == "module", "module type tag");
print("basic imports: PASS");

View File

@ -0,0 +1,21 @@
print("\n--- Test: builtin imports ---");
import sys;
assert(type(sys) == "module", "sys is module");
from sys import memoryUsage as mem, platform, version;
var m = mem();
assert(type(m) == "number" || type(m) == "none", "memoryUsage returns number or none");
var plat = platform();
assert(type(plat) == "string", "platform returns string");
var ver = version();
assert(type(ver) == "string", "version returns string");
// 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");

Some files were not shown because too many files have changed in this diff Show More