Compare commits
10 Commits
f70c6abd77
...
ec4b5afa9c
| Author | SHA1 | Date | |
|---|---|---|---|
| ec4b5afa9c | |||
| 6e3379b5b8 | |||
| 7f7c6e438d | |||
| 8cdccae214 | |||
| fc63c3e46f | |||
| 227586c583 | |||
| 3138f6fb92 | |||
| 7a9c0b7ea9 | |||
| 266cca5b42 | |||
| 85d3381575 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,3 +3,4 @@ build/
|
||||
|
||||
.DS_Store
|
||||
build-ninja
|
||||
build-release
|
||||
|
||||
@ -45,16 +45,18 @@ elseif(MSVC)
|
||||
endif()
|
||||
|
||||
# Collect source files
|
||||
file(GLOB_RECURSE BOB_RUNTIME_SOURCES "src/sources/runtime/*.cpp")
|
||||
file(GLOB_RECURSE BOB_PARSING_SOURCES "src/sources/parsing/*.cpp")
|
||||
file(GLOB_RECURSE BOB_STDLIB_SOURCES "src/sources/stdlib/*.cpp")
|
||||
file(GLOB_RECURSE BOB_CLI_SOURCES "src/sources/cli/*.cpp")
|
||||
file(GLOB_RECURSE BOB_RUNTIME_SOURCES CONFIGURE_DEPENDS "src/sources/runtime/*.cpp")
|
||||
file(GLOB_RECURSE BOB_PARSING_SOURCES CONFIGURE_DEPENDS "src/sources/parsing/*.cpp")
|
||||
file(GLOB_RECURSE BOB_STDLIB_SOURCES CONFIGURE_DEPENDS "src/sources/stdlib/*.cpp")
|
||||
file(GLOB_RECURSE BOB_BUILTIN_SOURCES CONFIGURE_DEPENDS "src/sources/builtinModules/*.cpp")
|
||||
file(GLOB_RECURSE BOB_CLI_SOURCES CONFIGURE_DEPENDS "src/sources/cli/*.cpp")
|
||||
|
||||
# All source files
|
||||
set(BOB_ALL_SOURCES
|
||||
${BOB_RUNTIME_SOURCES}
|
||||
${BOB_PARSING_SOURCES}
|
||||
${BOB_STDLIB_SOURCES}
|
||||
${BOB_BUILTIN_SOURCES}
|
||||
${BOB_CLI_SOURCES}
|
||||
)
|
||||
|
||||
@ -66,6 +68,7 @@ target_include_directories(bob PRIVATE
|
||||
src/headers/runtime
|
||||
src/headers/parsing
|
||||
src/headers/stdlib
|
||||
src/headers/builtinModules
|
||||
src/headers/cli
|
||||
src/headers/common
|
||||
)
|
||||
|
||||
@ -233,19 +233,26 @@ toBoolean(1); // true
|
||||
type(42); // "number"
|
||||
```
|
||||
|
||||
### Arrays & Strings
|
||||
### Arrays, Strings, and Dictionaries: Method style (preferred)
|
||||
```go
|
||||
len([1, 2, 3]); // 3
|
||||
len("hello"); // 5
|
||||
push(array, value); // Add to end
|
||||
pop(array); // Remove from end
|
||||
[1, 2, 3].len(); // 3
|
||||
"hello".len(); // 5
|
||||
var a = [1, 2]; a.push(3); // a is now [1, 2, 3]
|
||||
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
|
||||
keys(dict); // Array of keys
|
||||
values(dict); // Array of values
|
||||
has(dict, "key"); // Check if key exists
|
||||
toInt(3.9); // 3 (global)
|
||||
(3.9).toInt(); // 3 (method on number)
|
||||
```
|
||||
|
||||
### Utility
|
||||
@ -253,7 +260,7 @@ has(dict, "key"); // Check if key exists
|
||||
assert(condition, "message"); // Testing
|
||||
time(); // Current time in microseconds
|
||||
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
|
||||
exit(0); // Exit program
|
||||
```
|
||||
@ -266,8 +273,93 @@ var lines = readLines("config.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
|
||||
|
||||
### 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 auto‑bind `this`; call via `obj.method(...)` when needed.
|
||||
|
||||
### Extensions (Built‑ins and Classes)
|
||||
Extend existing types (including built‑ins) 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) → built‑in extensions (string/array/dict) → `any`.
|
||||
- `this` is injected for property calls.
|
||||
|
||||
### String Interpolation
|
||||
```go
|
||||
var name = "Alice";
|
||||
@ -349,7 +441,7 @@ var people = [
|
||||
{"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];
|
||||
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 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];
|
||||
if (len(line) > 0) {
|
||||
push(processed, "Processed: " + line);
|
||||
if (line.len() > 0) {
|
||||
processed.push("Processed: " + line);
|
||||
}
|
||||
}
|
||||
|
||||
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];
|
||||
if (i < len(processed) - 1) {
|
||||
if (i < processed.len() - 1) {
|
||||
output = output + "\n";
|
||||
}
|
||||
}
|
||||
|
||||
113
Reference/EMBEDDING.md
Normal file
113
Reference/EMBEDDING.md
Normal file
@ -0,0 +1,113 @@
|
||||
Embedding Bob: Public API Guide
|
||||
================================
|
||||
|
||||
This document explains how to embed the Bob interpreter in your C++ application, register custom modules, and control sandbox policies.
|
||||
|
||||
Quick Start
|
||||
-----------
|
||||
|
||||
```cpp
|
||||
#include "cli/bob.h"
|
||||
#include "ModuleRegistry.h"
|
||||
|
||||
int main() {
|
||||
Bob bob;
|
||||
|
||||
// Optional: configure policies or modules before first use
|
||||
bob.setBuiltinModulePolicy(true); // allow builtin modules (default)
|
||||
bob.setBuiltinModuleDenyList({/* e.g., "sys" */});
|
||||
|
||||
// Register a custom builtin module called "demo"
|
||||
bob.registerModule("demo", [](ModuleRegistry::ModuleBuilder& m) {
|
||||
m.fn("hello", [](std::vector<Value> args, int, int) -> Value {
|
||||
std::string who = (args.size() >= 1 && args[0].isString()) ? args[0].asString() : "world";
|
||||
return Value(std::string("hello ") + who);
|
||||
});
|
||||
m.val("meaning", Value(42.0));
|
||||
});
|
||||
|
||||
// Evaluate code from a string
|
||||
bob.evalString("import demo; print(demo.hello(\"Bob\"));", "<host>");
|
||||
|
||||
// Evaluate a file (imports inside resolve relative to the file's directory)
|
||||
bob.evalFile("script.bob");
|
||||
}
|
||||
```
|
||||
|
||||
API Overview
|
||||
------------
|
||||
|
||||
Bob exposes a single high-level object with a minimal, consistent API. It self-manages an internal interpreter and applies configuration on first use.
|
||||
|
||||
- Program execution
|
||||
- `bool evalString(const std::string& code, const std::string& filename = "<eval>")`
|
||||
- `bool evalFile(const std::string& path)`
|
||||
- `void runFile(const std::string& path)` (CLI convenience – delegates to `evalFile`)
|
||||
- `void runPrompt()` (interactive CLI – delegates each line to `evalString`)
|
||||
|
||||
- Module registration and sandboxing
|
||||
- `void registerModule(const std::string& name, std::function<void(ModuleRegistry::ModuleBuilder&)> init)`
|
||||
- `void setBuiltinModulePolicy(bool allow)`
|
||||
- `void setBuiltinModuleAllowList(const std::vector<std::string>& allowed)`
|
||||
- `void setBuiltinModuleDenyList(const std::vector<std::string>& denied)`
|
||||
|
||||
- Global environment helpers
|
||||
- `bool defineGlobal(const std::string& name, const Value& value)`
|
||||
- `bool tryGetGlobal(const std::string& name, Value& out) const`
|
||||
|
||||
All configuration calls are safe to use before any evaluation – they are queued and applied automatically when the interpreter is first created.
|
||||
|
||||
Registering Custom Builtin Modules
|
||||
----------------------------------
|
||||
|
||||
Use the builder convenience to create a module:
|
||||
|
||||
```cpp
|
||||
bob.registerModule("raylib", [](ModuleRegistry::ModuleBuilder& m) {
|
||||
m.fn("init", [](std::vector<Value> args, int line, int col) -> Value {
|
||||
// call into your library here; validate args, return Value
|
||||
return NONE_VALUE;
|
||||
});
|
||||
m.val("VERSION", Value(std::string("5.0")));
|
||||
});
|
||||
```
|
||||
|
||||
At runtime:
|
||||
|
||||
```bob
|
||||
import raylib;
|
||||
raylib.init();
|
||||
print(raylib.VERSION);
|
||||
```
|
||||
|
||||
Notes
|
||||
-----
|
||||
|
||||
- Modules are immutable, first-class objects. `type(module)` is "module" and `toString(module)` prints `<module 'name'>`.
|
||||
- Reassigning a module binding or setting module properties throws an error.
|
||||
|
||||
Builtin Modules and Sandboxing
|
||||
------------------------------
|
||||
|
||||
- Builtin modules (e.g., `sys`) are registered during interpreter construction.
|
||||
- File imports are always resolved relative to the importing file's directory.
|
||||
- Policies:
|
||||
- `setBuiltinModulePolicy(bool allow)` – enable/disable all builtin modules.
|
||||
- `setBuiltinModuleAllowList(vector<string>)` – allow only listed modules (deny others).
|
||||
- `setBuiltinModuleDenyList(vector<string>)` – explicitly deny listed modules.
|
||||
- Denied/disabled modules are cloaked: `import name` reports "Module not found".
|
||||
|
||||
Error Reporting
|
||||
---------------
|
||||
|
||||
- `evalString`/`evalFile` set file context for error reporting so line/column references point to the real source.
|
||||
- Both return `true` on success and `false` if execution failed (errors are reported via the internal error reporter).
|
||||
|
||||
CLI vs Embedding
|
||||
----------------
|
||||
|
||||
- CLI builds include `main.cpp` (entry point), which uses `Bob::runFile` or `Bob::runPrompt`.
|
||||
- Embedded hosts do not use `main.cpp`; instead they instantiate `Bob` and call `evalString`/`evalFile` directly.
|
||||
|
||||
|
||||
|
||||
@ -56,7 +56,7 @@ Bob is a mature, working programming language with a modern architecture and com
|
||||
- **Type System**: `type()`, `toString()`, `toNumber()`, `toInt()`, `toBoolean()`
|
||||
- **Testing**: `assert()` with custom error messages
|
||||
- **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()`
|
||||
- **File I/O**: `readFile()`, `writeFile()`, `readLines()`, `fileExists()`
|
||||
|
||||
|
||||
@ -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`
|
||||
- Variable declaration: `var`
|
||||
- Function declaration: `func`
|
||||
- Classes and OOP: `class`, `extends`, `extension`, `this`, `super`
|
||||
- Logical operators: `and`, `or`, `not`
|
||||
|
||||
### Built-in Functions
|
||||
- `print()`, `assert()`, `input()`, `type()`, `toString()`, `toNumber()`, `time()`
|
||||
- `sleep()`, `printRaw()`, `len()`, `push()`, `pop()`, `random()`, `eval()`
|
||||
- `print()`, `assert()`, `input()`, `type()`, `toString()`, `toNumber()`, `toInt()`, `time()`, `sleep()`, `printRaw()`
|
||||
- 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
|
||||
- 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!";
|
||||
print(message);
|
||||
|
||||
// Array operations
|
||||
// Array operations (method style)
|
||||
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]);
|
||||
|
||||
// Function with ternary operator
|
||||
|
||||
BIN
bob-language-extension/bob-language-0.5.0.vsix
Normal file
BIN
bob-language-extension/bob-language-0.5.0.vsix
Normal file
Binary file not shown.
@ -20,12 +20,12 @@ print("Pi: " + toString(pi));
|
||||
var numbers = [1, 2, 3, 4, 5];
|
||||
var fruits = ["apple", "banana", "cherry"];
|
||||
|
||||
print("Array length: " + len(numbers));
|
||||
print("Array length: " + numbers.len());
|
||||
print("First element: " + numbers[0]);
|
||||
|
||||
numbers[2] = 99; // Array assignment
|
||||
push(numbers, 6); // Add element
|
||||
var lastElement = pop(numbers); // Remove and get last element
|
||||
numbers.push(6); // Add element
|
||||
var lastElement = numbers.pop(); // Remove and get last element
|
||||
|
||||
// Function definition
|
||||
func factorial(n) {
|
||||
@ -91,7 +91,7 @@ value *= 2;
|
||||
value -= 3;
|
||||
|
||||
// New built-in functions
|
||||
var randomValue = random();
|
||||
import rand; var randomValue = rand.random();
|
||||
sleep(100); // Sleep for 100ms
|
||||
printRaw("No newline here");
|
||||
eval("print('Dynamic code execution!');");
|
||||
|
||||
@ -23,7 +23,7 @@
|
||||
["'", "'"]
|
||||
],
|
||||
"indentationRules": {
|
||||
"increaseIndentPattern": "\\{[^}]*$|\\b(func|if|else|while|for)\\b.*$",
|
||||
"increaseIndentPattern": "\\{[^}]*$|\\b(func|if|else|while|for|class|extension)\\b.*$",
|
||||
"decreaseIndentPattern": "^\\s*[})]"
|
||||
},
|
||||
"folding": {
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
"name": "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",
|
||||
"version": "0.4.0",
|
||||
"version": "0.5.0",
|
||||
"engines": {
|
||||
"vscode": "^1.60.0"
|
||||
},
|
||||
|
||||
@ -53,6 +53,32 @@
|
||||
],
|
||||
"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": {
|
||||
"prefix": "print",
|
||||
"body": [
|
||||
@ -274,7 +300,7 @@
|
||||
"Random Number": {
|
||||
"prefix": "random",
|
||||
"body": [
|
||||
"var randomValue = random();"
|
||||
"import rand; var randomValue = rand.random();"
|
||||
],
|
||||
"description": "Generate random number"
|
||||
},
|
||||
|
||||
@ -153,7 +153,7 @@
|
||||
"patterns": [
|
||||
{
|
||||
"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",
|
||||
|
||||
13
bobby.bob
Normal file
13
bobby.bob
Normal file
@ -0,0 +1,13 @@
|
||||
class A {
|
||||
var inner = 10;
|
||||
|
||||
func test(){
|
||||
print(this.inner);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func hello(){
|
||||
print("hello");
|
||||
}
|
||||
|
||||
@ -9,14 +9,14 @@ print("Test 1: Heavy string operations");
|
||||
var stringData = [];
|
||||
for (var i = 0; i < 100000; i++) {
|
||||
var str = toString(i) + "_" + toString(i * 2) + "_" + toString(i * 3);
|
||||
push(stringData, {
|
||||
stringData.push({
|
||||
"original": str,
|
||||
"upper": str, // Bob doesn't have toUpper, but test string storage
|
||||
"length": len(str),
|
||||
"length": str.len(),
|
||||
"type": type(str)
|
||||
});
|
||||
}
|
||||
print("Created " + len(stringData) + " string operation results");
|
||||
print("Created " + stringData.len() + " string operation results");
|
||||
print("Memory: " + memoryUsage() + " MB");
|
||||
input("Press Enter to clear string data...");
|
||||
stringData = none;
|
||||
@ -33,12 +33,12 @@ for (var i = 0; i < 200000; i++) {
|
||||
var intVal = toInt(num);
|
||||
var boolVal = toBoolean(i % 2);
|
||||
|
||||
push(conversions, [
|
||||
conversions.push([
|
||||
num, str, backToNum, intVal, 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");
|
||||
input("Press Enter to clear conversions...");
|
||||
conversions = [];
|
||||
@ -53,15 +53,15 @@ for (var i = 0; i < 50000; i++) {
|
||||
var dict = {"a": i, "b": i+1, "c": i+2};
|
||||
|
||||
// Use builtin functions heavily
|
||||
var arrLen = len(arr);
|
||||
push(arr, i+3);
|
||||
var popped = pop(arr);
|
||||
var arrLen = arr.len();
|
||||
arr.push(i+3);
|
||||
var popped = arr.pop();
|
||||
|
||||
var dictKeys = keys(dict);
|
||||
var dictValues = values(dict);
|
||||
var hasA = has(dict, "a");
|
||||
var dictKeys = dict.keys();
|
||||
var dictValues = dict.values();
|
||||
var hasA = dict.has("a");
|
||||
|
||||
push(collections, {
|
||||
collections.push({
|
||||
"array": arr,
|
||||
"dict": dict,
|
||||
"arrLen": arrLen,
|
||||
@ -71,7 +71,7 @@ for (var i = 0; i < 50000; i++) {
|
||||
"hasA": hasA
|
||||
});
|
||||
}
|
||||
print("Created " + len(collections) + " collection operation results");
|
||||
print("Created " + collections.len() + " collection operation results");
|
||||
print("Memory: " + memoryUsage() + " MB");
|
||||
input("Press Enter to clear collections...");
|
||||
collections = "cleared";
|
||||
@ -88,14 +88,14 @@ for (var i = 0; i < 10000; i++) {
|
||||
var funcExpr = "func() { return " + toString(i) + "; };";
|
||||
var evalFunc = eval(funcExpr);
|
||||
|
||||
push(evalResults, {
|
||||
evalResults.push({
|
||||
"expr": expression,
|
||||
"result": result,
|
||||
"func": evalFunc,
|
||||
"funcResult": evalFunc()
|
||||
});
|
||||
}
|
||||
print("Created " + len(evalResults) + " eval results");
|
||||
print("Created " + evalResults.len() + " eval results");
|
||||
print("Memory: " + memoryUsage() + " MB");
|
||||
input("Press Enter to clear eval results...");
|
||||
evalResults = none;
|
||||
@ -121,15 +121,15 @@ for (var i = 0; i < 1000; i++) {
|
||||
var readContent = readFile(filename);
|
||||
var lines = readLines(filename);
|
||||
|
||||
push(fileData, {
|
||||
fileData.push({
|
||||
"filename": filename,
|
||||
"exists": exists,
|
||||
"content": readContent,
|
||||
"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");
|
||||
input("Press Enter to clear file data...");
|
||||
fileData = [];
|
||||
@ -151,18 +151,19 @@ input("File data cleared. Check memory usage...");
|
||||
print("Test 6: Random number stress");
|
||||
var randomData = [];
|
||||
for (var i = 0; i < 200000; i++) {
|
||||
var rand1 = random();
|
||||
var rand2 = random();
|
||||
import rand as RLeak;
|
||||
var rand1 = RLeak.random();
|
||||
var rand2 = RLeak.random();
|
||||
var sum = rand1 + rand2;
|
||||
|
||||
push(randomData, {
|
||||
randomData.push({
|
||||
"rand1": rand1,
|
||||
"rand2": rand2,
|
||||
"sum": sum,
|
||||
"product": rand1 * rand2
|
||||
});
|
||||
}
|
||||
print("Created " + len(randomData) + " random number results");
|
||||
print("Created " + randomData.len() + " random number results");
|
||||
print("Memory: " + memoryUsage() + " MB");
|
||||
input("Press Enter to clear random data...");
|
||||
randomData = none;
|
||||
|
||||
@ -8,13 +8,13 @@ print("Initial memory: " + memoryUsage() + " MB");
|
||||
print("Test 1: Large nested arrays");
|
||||
var nestedArrays = [];
|
||||
for (var i = 0; i < 50000; i++) {
|
||||
push(nestedArrays, [
|
||||
nestedArrays.push([
|
||||
[i, i+1, i+2],
|
||||
[i*2, i*3, i*4],
|
||||
[[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");
|
||||
input("Press Enter to clear nested arrays...");
|
||||
nestedArrays = none;
|
||||
@ -25,7 +25,7 @@ input("Cleared. Check memory usage...");
|
||||
print("Test 2: Large nested dictionaries");
|
||||
var nestedDicts = [];
|
||||
for (var i = 0; i < 50000; i++) {
|
||||
push(nestedDicts, {
|
||||
nestedDicts.push({
|
||||
"id": i,
|
||||
"data": {
|
||||
"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");
|
||||
input("Press Enter to clear nested dicts...");
|
||||
nestedDicts = [];
|
||||
@ -53,7 +53,7 @@ input("Cleared. Check memory usage...");
|
||||
print("Test 3: Mixed array/dict structures");
|
||||
var mixedStructures = [];
|
||||
for (var i = 0; i < 30000; i++) {
|
||||
push(mixedStructures, [
|
||||
mixedStructures.push([
|
||||
{"arrays": [[i, i+1], [i+2, i+3]]},
|
||||
[{"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");
|
||||
input("Press Enter to clear mixed structures...");
|
||||
mixedStructures = "cleared";
|
||||
@ -78,11 +78,15 @@ for (var i = 0; i < 1000000; i++) {
|
||||
var item = {"id": i, "value": i * 2};
|
||||
// Create a structure that references itself
|
||||
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");
|
||||
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;
|
||||
print("Memory after clear: " + memoryUsage() + " MB");
|
||||
input("Cleared. Check memory usage...");
|
||||
@ -95,12 +99,12 @@ for (var i = 0; i < 100000; i++) {
|
||||
for (var j = 0; j < 100; j++) {
|
||||
longString = longString + "data" + i + "_" + j + " ";
|
||||
}
|
||||
push(stringCollections, {
|
||||
stringCollections.push({
|
||||
"content": longString,
|
||||
"words": [longString, longString + "_copy", longString + "_backup"]
|
||||
});
|
||||
}
|
||||
print("Created " + len(stringCollections) + " string collections");
|
||||
print("Created " + stringCollections.len() + " string collections");
|
||||
print("Memory: " + memoryUsage() + " MB");
|
||||
input("Press Enter to clear string collections...");
|
||||
stringCollections = none;
|
||||
|
||||
@ -8,7 +8,7 @@ print("Initial memory: " + memoryUsage() + " MB");
|
||||
print("Test 1: Recursive function closures");
|
||||
var recursiveFuncs = [];
|
||||
for (var i = 0; i < 100000; i++) {
|
||||
push(recursiveFuncs, func() {
|
||||
recursiveFuncs.push(func() {
|
||||
var capturedI = i;
|
||||
return func() {
|
||||
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");
|
||||
input("Press Enter to clear recursive functions...");
|
||||
recursiveFuncs = none;
|
||||
@ -29,14 +29,14 @@ input("Cleared. Check memory usage...");
|
||||
print("Test 2: Function factories");
|
||||
var factories = [];
|
||||
for (var i = 0; i < 100000; i++) {
|
||||
push(factories, func() {
|
||||
factories.push(func() {
|
||||
var multiplier = i;
|
||||
return func(x) {
|
||||
return x * multiplier;
|
||||
};
|
||||
});
|
||||
}
|
||||
print("Created " + len(factories) + " function factories");
|
||||
print("Created " + factories.len() + " function factories");
|
||||
print("Memory: " + memoryUsage() + " MB");
|
||||
input("Press Enter to clear factories...");
|
||||
factories = [];
|
||||
@ -47,7 +47,7 @@ input("Cleared. Check memory usage...");
|
||||
print("Test 3: Deep function nesting");
|
||||
var deepNested = [];
|
||||
for (var i = 0; i < 50000; i++) {
|
||||
push(deepNested, func() {
|
||||
deepNested.push(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");
|
||||
input("Press Enter to clear deep nested...");
|
||||
deepNested = "test";
|
||||
@ -76,10 +76,10 @@ for (var i = 0; i < 1000000; i++) {
|
||||
var funcB = func() {
|
||||
return "B" + i;
|
||||
};
|
||||
// Store both in same array element to create potential circular refs
|
||||
push(circularFuncs, [funcA, funcB, func() { return funcA; }]);
|
||||
// Store both in same array element to create potential circular refs
|
||||
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");
|
||||
input("Press Enter to clear circular functions...");
|
||||
circularFuncs = 42;
|
||||
|
||||
@ -10,16 +10,16 @@ var nestedData = [];
|
||||
for (var i = 0; i < 1000; i++) {
|
||||
var row = [];
|
||||
for (var j = 0; j < 1000; j++) {
|
||||
push(row, {
|
||||
row.push({
|
||||
"i": i,
|
||||
"j": j,
|
||||
"func": func() { return 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");
|
||||
input("Press Enter to clear nested data...");
|
||||
nestedData = none;
|
||||
@ -31,14 +31,14 @@ print("Test 2: While loop accumulation");
|
||||
var accumulator = [];
|
||||
var counter = 0;
|
||||
while (counter < 500000) {
|
||||
push(accumulator, {
|
||||
accumulator.push({
|
||||
"count": counter,
|
||||
"func": func() { return counter * 2; },
|
||||
"meta": ["item" + counter, counter % 100]
|
||||
});
|
||||
counter = counter + 1;
|
||||
}
|
||||
print("Accumulated " + len(accumulator) + " items in while loop");
|
||||
print("Accumulated " + accumulator.len() + " items in while loop");
|
||||
print("Memory: " + memoryUsage() + " MB");
|
||||
input("Press Enter to clear accumulator...");
|
||||
accumulator = [];
|
||||
@ -72,7 +72,7 @@ print("Test 4: Do-while function creation");
|
||||
var doWhileFuncs = [];
|
||||
var dwCounter = 0;
|
||||
do {
|
||||
push(doWhileFuncs, func() {
|
||||
doWhileFuncs.push(func() {
|
||||
var captured = dwCounter;
|
||||
return func() {
|
||||
return captured * captured;
|
||||
@ -80,7 +80,7 @@ do {
|
||||
});
|
||||
dwCounter = dwCounter + 1;
|
||||
} while (dwCounter < 100000);
|
||||
print("Created " + len(doWhileFuncs) + " functions in do-while");
|
||||
print("Created " + doWhileFuncs.len() + " functions in do-while");
|
||||
print("Memory: " + memoryUsage() + " MB");
|
||||
input("Press Enter to clear do-while functions...");
|
||||
doWhileFuncs = "cleared";
|
||||
@ -97,7 +97,7 @@ for (var i = 0; i < 500000; i++) {
|
||||
|
||||
if (i > 400000 && i % 100 == 0) {
|
||||
// Create larger objects near the end
|
||||
push(complexData, {
|
||||
complexData.push({
|
||||
"large": [
|
||||
func() { return i; },
|
||||
[i, i+1, i+2, i+3],
|
||||
@ -105,14 +105,14 @@ for (var i = 0; i < 500000; i++) {
|
||||
]
|
||||
});
|
||||
} 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
|
||||
}
|
||||
}
|
||||
print("Complex loop created " + len(complexData) + " items");
|
||||
print("Complex loop created " + complexData.len() + " items");
|
||||
print("Memory: " + memoryUsage() + " MB");
|
||||
input("Press Enter to clear complex data...");
|
||||
complexData = none;
|
||||
@ -126,7 +126,7 @@ for (var cycle = 0; cycle < 100; cycle++) {
|
||||
|
||||
// Create lots of data
|
||||
for (var i = 0; i < 10000; i++) {
|
||||
push(churnData, [
|
||||
churnData.push([
|
||||
func() { return i + cycle; },
|
||||
{"cycle": cycle, "item": i}
|
||||
]);
|
||||
|
||||
@ -11,16 +11,16 @@ for (var i = 0; i < 50000; i++) {
|
||||
var capturedArray = [i, i+1, i+2, "data" + i];
|
||||
var capturedDict = {"id": i, "values": [i*2, i*3]};
|
||||
|
||||
push(funcWithCollections, func() {
|
||||
funcWithCollections.push(func() {
|
||||
// Capture both collections
|
||||
var localArray = capturedArray;
|
||||
var localDict = capturedDict;
|
||||
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");
|
||||
input("Press Enter to clear function collections...");
|
||||
funcWithCollections = [];
|
||||
@ -31,7 +31,7 @@ input("Cleared. Check memory usage...");
|
||||
print("Test 2: Collections with mixed content");
|
||||
var mixedContent = [];
|
||||
for (var i = 0; i < 30000; i++) {
|
||||
push(mixedContent, [
|
||||
mixedContent.push([
|
||||
func() { return i; }, // Function
|
||||
[i, i+1, func() { return i*2; }], // Array 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
|
||||
]);
|
||||
}
|
||||
print("Created " + len(mixedContent) + " mixed content collections");
|
||||
print("Created " + mixedContent.len() + " mixed content collections");
|
||||
print("Memory: " + memoryUsage() + " MB");
|
||||
input("Press Enter to clear mixed content...");
|
||||
mixedContent = none;
|
||||
@ -58,9 +58,9 @@ for (var i = 0; i < 100000; i++) {
|
||||
obj["array"] = [1, 2, 3];
|
||||
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");
|
||||
input("Press Enter to clear property objects...");
|
||||
propObjects = "cleared";
|
||||
@ -71,20 +71,20 @@ input("Cleared. Check memory usage...");
|
||||
print("Test 4: Type reassignment chains");
|
||||
var typeChains = [];
|
||||
for (var i = 0; i < 200000; i++) {
|
||||
push(typeChains, i);
|
||||
typeChains.push(i);
|
||||
}
|
||||
print("Memory after number array: " + memoryUsage() + " MB");
|
||||
input("Created number array. Press Enter to convert 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; };
|
||||
}
|
||||
print("Memory after function conversion: " + memoryUsage() + " MB");
|
||||
input("Converted to functions. Press Enter to convert 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; }};
|
||||
}
|
||||
print("Memory after dict conversion: " + memoryUsage() + " MB");
|
||||
@ -98,12 +98,12 @@ print("Test 5: Rapid allocation/deallocation");
|
||||
for (var round = 0; round < 10; round++) {
|
||||
var temp = [];
|
||||
for (var i = 0; i < 100000; i++) {
|
||||
push(temp, [
|
||||
temp.push([
|
||||
func() { return i; },
|
||||
{"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
|
||||
if (round % 2 == 1) print("After clear round " + round + ": " + memoryUsage() + " MB");
|
||||
}
|
||||
|
||||
8
src/headers/builtinModules/base64_module.h
Normal file
8
src/headers/builtinModules/base64_module.h
Normal file
@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
class Interpreter;
|
||||
|
||||
// Register the builtin 'base64' module
|
||||
void registerBase64Module(Interpreter& interpreter);
|
||||
|
||||
|
||||
8
src/headers/builtinModules/eval.h
Normal file
8
src/headers/builtinModules/eval.h
Normal file
@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
class Interpreter;
|
||||
|
||||
// Register the builtin 'eval' module providing eval(code) and evalFile(path)
|
||||
void registerEvalModule(Interpreter& interpreter);
|
||||
|
||||
|
||||
8
src/headers/builtinModules/io.h
Normal file
8
src/headers/builtinModules/io.h
Normal file
@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
class Interpreter;
|
||||
|
||||
// Register the builtin 'io' module (file I/O and input)
|
||||
void registerIoModule(Interpreter& interpreter);
|
||||
|
||||
|
||||
8
src/headers/builtinModules/json.h
Normal file
8
src/headers/builtinModules/json.h
Normal file
@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
class Interpreter;
|
||||
|
||||
// Register the builtin 'json' module
|
||||
void registerJsonModule(Interpreter& interpreter);
|
||||
|
||||
|
||||
8
src/headers/builtinModules/math_module.h
Normal file
8
src/headers/builtinModules/math_module.h
Normal file
@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
class Interpreter;
|
||||
|
||||
// Register the builtin 'math' module
|
||||
void registerMathModule(Interpreter& interpreter);
|
||||
|
||||
|
||||
8
src/headers/builtinModules/os.h
Normal file
8
src/headers/builtinModules/os.h
Normal file
@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
class Interpreter;
|
||||
|
||||
// Register the builtin 'os' module
|
||||
void registerOsModule(Interpreter& interpreter);
|
||||
|
||||
|
||||
8
src/headers/builtinModules/path_module.h
Normal file
8
src/headers/builtinModules/path_module.h
Normal file
@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
class Interpreter;
|
||||
|
||||
// Register the builtin 'path' module (path utilities)
|
||||
void registerPathModule(Interpreter& interpreter);
|
||||
|
||||
|
||||
8
src/headers/builtinModules/rand.h
Normal file
8
src/headers/builtinModules/rand.h
Normal file
@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
class Interpreter;
|
||||
|
||||
// Register the builtin 'rand' module
|
||||
void registerRandModule(Interpreter& interpreter);
|
||||
|
||||
|
||||
8
src/headers/builtinModules/register.h
Normal file
8
src/headers/builtinModules/register.h
Normal file
@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
class Interpreter;
|
||||
|
||||
// Registers all builtin modules with the interpreter
|
||||
void registerAllBuiltinModules(Interpreter& interpreter);
|
||||
|
||||
|
||||
8
src/headers/builtinModules/sys.h
Normal file
8
src/headers/builtinModules/sys.h
Normal file
@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
class Interpreter;
|
||||
|
||||
// Register the builtin 'sys' module
|
||||
void registerSysModule(Interpreter& interpreter);
|
||||
|
||||
|
||||
8
src/headers/builtinModules/time_module.h
Normal file
8
src/headers/builtinModules/time_module.h
Normal file
@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
class Interpreter;
|
||||
|
||||
// Register the builtin 'time' module (time functions)
|
||||
void registerTimeModule(Interpreter& interpreter);
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
#include <string>
|
||||
#include "Lexer.h"
|
||||
#include "Interpreter.h"
|
||||
#include "ModuleRegistry.h"
|
||||
#include "helperFunctions/ShortHands.h"
|
||||
#include "ErrorReporter.h"
|
||||
|
||||
@ -20,10 +21,105 @@ public:
|
||||
~Bob() = default;
|
||||
|
||||
public:
|
||||
// Embedding helpers (bridge to internal interpreter)
|
||||
void registerModule(const std::string& name, std::function<void(ModuleRegistry::ModuleBuilder&)> init) {
|
||||
if (interpreter) interpreter->registerModule(name, init);
|
||||
else pendingConfigurators.push_back([name, init](Interpreter& I){ I.registerModule(name, init); });
|
||||
}
|
||||
void setBuiltinModulePolicy(bool allow) {
|
||||
if (interpreter) interpreter->setBuiltinModulePolicy(allow);
|
||||
else pendingConfigurators.push_back([allow](Interpreter& I){ I.setBuiltinModulePolicy(allow); });
|
||||
}
|
||||
void setBuiltinModuleAllowList(const std::vector<std::string>& allowed) {
|
||||
if (interpreter) interpreter->setBuiltinModuleAllowList(allowed);
|
||||
else pendingConfigurators.push_back([allowed](Interpreter& I){ I.setBuiltinModuleAllowList(allowed); });
|
||||
}
|
||||
void setBuiltinModuleDenyList(const std::vector<std::string>& denied) {
|
||||
if (interpreter) interpreter->setBuiltinModuleDenyList(denied);
|
||||
else pendingConfigurators.push_back([denied](Interpreter& I){ I.setBuiltinModuleDenyList(denied); });
|
||||
}
|
||||
bool defineGlobal(const std::string& name, const Value& v) {
|
||||
if (interpreter) return interpreter->defineGlobalVar(name, v);
|
||||
pendingConfigurators.push_back([name, v](Interpreter& I){ I.defineGlobalVar(name, v); });
|
||||
return true;
|
||||
}
|
||||
bool tryGetGlobal(const std::string& name, Value& out) const { return interpreter ? interpreter->tryGetGlobalVar(name, out) : false; }
|
||||
void runFile(const std::string& path);
|
||||
void runPrompt();
|
||||
bool evalFile(const std::string& path);
|
||||
bool evalString(const std::string& code, const std::string& filename = "<eval>");
|
||||
|
||||
// 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:
|
||||
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;
|
||||
};
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
#include <bitset>
|
||||
#include <cctype>
|
||||
|
||||
inline std::vector<std::string> splitString(const std::string& input, const std::string& delimiter) {
|
||||
std::vector<std::string> tokens;
|
||||
@ -56,9 +57,9 @@ inline bool isHexDigit(char 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
|
||||
u_long result = 0;
|
||||
unsigned long long result = 0;
|
||||
for (char ch : binaryDigits) {
|
||||
result <<= 1;
|
||||
result += (ch - '0');
|
||||
|
||||
@ -34,6 +34,9 @@ private:
|
||||
std::string currentFileName;
|
||||
std::vector<std::string> callStack;
|
||||
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:
|
||||
ErrorReporter() = default;
|
||||
@ -58,6 +61,11 @@ public:
|
||||
void pushCallStack(const std::string& functionName);
|
||||
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:
|
||||
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);
|
||||
|
||||
@ -22,10 +22,12 @@ enum TokenType{
|
||||
// Increment/decrement operators
|
||||
PLUS_PLUS, MINUS_MINUS,
|
||||
|
||||
IDENTIFIER, STRING, NUMBER, BOOL,
|
||||
IDENTIFIER, STRING, NUMBER, KW_BOOL,
|
||||
|
||||
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
|
||||
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",
|
||||
|
||||
"IDENTIFIER", "STRING", "NUMBER", "BOOL",
|
||||
"IDENTIFIER", "STRING", "NUMBER", "KW_BOOL",
|
||||
|
||||
"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
|
||||
"PLUS_EQUAL", "MINUS_EQUAL", "STAR_EQUAL", "SLASH_EQUAL", "PERCENT_EQUAL",
|
||||
@ -77,12 +81,21 @@ const std::map<std::string, TokenType> KEYWORDS {
|
||||
{"do", DO},
|
||||
{"var", VAR},
|
||||
{"class", CLASS},
|
||||
{"extends", EXTENDS},
|
||||
{"extension", EXTENSION},
|
||||
{"super", SUPER},
|
||||
{"this", THIS},
|
||||
{"none", NONE},
|
||||
{"return", RETURN},
|
||||
{"break", BREAK},
|
||||
{"continue", CONTINUE},
|
||||
{"import", IMPORT},
|
||||
{"from", FROM},
|
||||
{"as", AS},
|
||||
{"try", TRY},
|
||||
{"catch", CATCH},
|
||||
{"finally", FINALLY},
|
||||
{"throw", THROW},
|
||||
};
|
||||
|
||||
struct Token
|
||||
|
||||
@ -68,6 +68,12 @@ private:
|
||||
std::shared_ptr<Stmt> continueStatement();
|
||||
|
||||
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();
|
||||
|
||||
|
||||
@ -17,6 +17,8 @@ struct ForStmt;
|
||||
struct BreakStmt;
|
||||
struct ContinueStmt;
|
||||
struct AssignStmt;
|
||||
struct ClassStmt;
|
||||
struct ExtensionStmt;
|
||||
|
||||
#include "ExecutionContext.h"
|
||||
|
||||
@ -34,6 +36,12 @@ struct StmtVisitor
|
||||
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 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>
|
||||
@ -43,6 +51,39 @@ struct Stmt : public std::enable_shared_from_this<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
|
||||
{
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
35
src/headers/runtime/AssignmentUtils.h
Normal file
35
src/headers/runtime/AssignmentUtils.h
Normal 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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
#include <unordered_map>
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <unordered_set>
|
||||
#include "Value.h"
|
||||
#include "Lexer.h"
|
||||
|
||||
@ -29,6 +30,7 @@ public:
|
||||
void setErrorReporter(ErrorReporter* reporter) {
|
||||
errorReporter = reporter;
|
||||
}
|
||||
ErrorReporter* getErrorReporter() const { return errorReporter; }
|
||||
|
||||
// Optimized define with inline
|
||||
inline void define(const std::string& name, const Value& value) {
|
||||
@ -41,23 +43,18 @@ public:
|
||||
// Enhanced get with error reporting
|
||||
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
|
||||
void pruneForClosureCapture();
|
||||
|
||||
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:
|
||||
std::unordered_map<std::string, Value> variables;
|
||||
std::shared_ptr<Environment> parent;
|
||||
ErrorReporter* errorReporter;
|
||||
std::unordered_set<std::string> constBindings;
|
||||
};
|
||||
|
||||
|
||||
@ -7,4 +7,8 @@ struct ExecutionContext {
|
||||
Value returnValue = NONE_VALUE;
|
||||
bool shouldBreak = false;
|
||||
bool shouldContinue = false;
|
||||
bool hasThrow = false;
|
||||
Value thrownValue = NONE_VALUE;
|
||||
int throwLine = 0;
|
||||
int throwColumn = 0;
|
||||
};
|
||||
|
||||
@ -38,6 +38,12 @@ public:
|
||||
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 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:
|
||||
void execute(const std::shared_ptr<Stmt>& statement, ExecutionContext* context);
|
||||
|
||||
@ -1,15 +1,4 @@
|
||||
#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 <memory>
|
||||
#include <unordered_map>
|
||||
@ -17,6 +6,24 @@
|
||||
#include <optional>
|
||||
#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
|
||||
class Evaluator;
|
||||
|
||||
@ -62,17 +69,41 @@ private:
|
||||
std::vector<std::shared_ptr<BuiltinFunction>> builtinFunctions;
|
||||
std::vector<std::shared_ptr<Function>> functions;
|
||||
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;
|
||||
bool inThunkExecution = false;
|
||||
|
||||
// Automatic cleanup tracking
|
||||
int functionCreationCount = 0;
|
||||
int thunkCreationCount = 0;
|
||||
static const int CLEANUP_THRESHOLD = 1000000;
|
||||
static const int CLEANUP_THRESHOLD = 10000;
|
||||
|
||||
RuntimeDiagnostics diagnostics; // Utility functions for runtime operations
|
||||
std::unique_ptr<Evaluator> evaluator;
|
||||
std::unique_ptr<Executor> executor;
|
||||
// Module cache: module key -> module dict value
|
||||
std::unordered_map<std::string, Value> moduleCache;
|
||||
// Builtin module registry
|
||||
ModuleRegistry builtinModules;
|
||||
// Import policy flags
|
||||
bool allowFileImports = true;
|
||||
bool preferFileOverBuiltin = true;
|
||||
bool allowBuiltinImports = true;
|
||||
std::vector<std::string> moduleSearchPaths; // e.g., BOBPATH
|
||||
// Pending throw propagation from expression evaluation
|
||||
bool hasPendingThrow = false;
|
||||
Value pendingThrow = NONE_VALUE;
|
||||
int pendingThrowLine = 0;
|
||||
int pendingThrowColumn = 0;
|
||||
int lastErrorLine = 0;
|
||||
int lastErrorColumn = 0;
|
||||
int tryDepth = 0;
|
||||
bool inlineErrorReported = false;
|
||||
|
||||
public:
|
||||
explicit Interpreter(bool isInteractive);
|
||||
@ -93,25 +124,75 @@ public:
|
||||
bool isInteractiveMode() const;
|
||||
std::shared_ptr<Environment> getEnvironment();
|
||||
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 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 cleanupUnusedFunctions();
|
||||
void cleanupUnusedThunks();
|
||||
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:
|
||||
Value evaluateWithoutTrampoline(const std::shared_ptr<Expr>& expr);
|
||||
void addStdLibFunctions();
|
||||
Value runTrampoline(Value initialResult);
|
||||
// Stored argv/executable for sys module
|
||||
std::vector<std::string> argvData;
|
||||
std::string executableFile;
|
||||
};
|
||||
|
||||
14
src/headers/runtime/ModuleDef.h
Normal file
14
src/headers/runtime/ModuleDef.h
Normal file
@ -0,0 +1,14 @@
|
||||
// ModuleDef.h
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include "Value.h"
|
||||
|
||||
struct Module {
|
||||
std::string name;
|
||||
std::shared_ptr<std::unordered_map<std::string, Value>> exports;
|
||||
};
|
||||
|
||||
|
||||
70
src/headers/runtime/ModuleRegistry.h
Normal file
70
src/headers/runtime/ModuleRegistry.h
Normal file
@ -0,0 +1,70 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include "TypeWrapper.h" // BuiltinFunction, Value
|
||||
|
||||
class Interpreter; // fwd
|
||||
|
||||
class ModuleRegistry {
|
||||
public:
|
||||
struct ModuleBuilder {
|
||||
std::string moduleName;
|
||||
Interpreter& interpreterRef;
|
||||
std::unordered_map<std::string, Value> exports;
|
||||
ModuleBuilder(const std::string& n, Interpreter& i) : moduleName(n), interpreterRef(i) {}
|
||||
void fn(const std::string& name, std::function<Value(std::vector<Value>, int, int)> func) {
|
||||
exports[name] = Value(std::make_shared<BuiltinFunction>(name, func));
|
||||
}
|
||||
void val(const std::string& name, const Value& v) { exports[name] = v; }
|
||||
};
|
||||
|
||||
using Factory = std::function<Value(Interpreter&)>;
|
||||
|
||||
void registerFactory(const std::string& name, Factory factory) {
|
||||
factories[name] = std::move(factory);
|
||||
}
|
||||
|
||||
void registerModule(const std::string& name, std::function<void(ModuleBuilder&)> init) {
|
||||
registerFactory(name, [name, init](Interpreter& I) -> Value {
|
||||
ModuleBuilder b(name, I);
|
||||
init(b);
|
||||
auto m = std::make_shared<Module>(name, b.exports);
|
||||
return Value(m);
|
||||
});
|
||||
}
|
||||
|
||||
bool has(const std::string& name) const {
|
||||
auto it = factories.find(name);
|
||||
if (it == factories.end()) return false;
|
||||
// Respect policy for presence checks to optionally cloak denied modules
|
||||
if (!allowBuiltins) return false;
|
||||
if (!allowList.empty() && allowList.find(name) == allowList.end()) return false;
|
||||
if (denyList.find(name) != denyList.end()) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
Value create(const std::string& name, Interpreter& I) const {
|
||||
auto it = factories.find(name);
|
||||
if (it == factories.end()) return NONE_VALUE;
|
||||
if (!allowBuiltins) return NONE_VALUE;
|
||||
if (!allowList.empty() && allowList.find(name) == allowList.end()) return NONE_VALUE;
|
||||
if (denyList.find(name) != denyList.end()) return NONE_VALUE;
|
||||
return it->second(I);
|
||||
}
|
||||
|
||||
void setPolicy(bool allow) { allowBuiltins = allow; }
|
||||
void setAllowList(const std::vector<std::string>& allowed) { allowList = std::unordered_set<std::string>(allowed.begin(), allowed.end()); }
|
||||
void setDenyList(const std::vector<std::string>& denied) { denyList = std::unordered_set<std::string>(denied.begin(), denied.end()); }
|
||||
|
||||
private:
|
||||
std::unordered_map<std::string, Factory> factories;
|
||||
std::unordered_set<std::string> allowList;
|
||||
std::unordered_set<std::string> denyList;
|
||||
bool allowBuiltins = true;
|
||||
};
|
||||
|
||||
|
||||
@ -28,8 +28,6 @@ public:
|
||||
void cleanupUnusedFunctions(std::vector<std::shared_ptr<BuiltinFunction>>& functions);
|
||||
void cleanupUnusedFunctions(std::vector<std::shared_ptr<Function>>& functions);
|
||||
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,
|
||||
std::vector<std::shared_ptr<Function>>& functions,
|
||||
std::vector<std::shared_ptr<Thunk>>& thunks);
|
||||
|
||||
@ -10,51 +10,22 @@
|
||||
struct Stmt;
|
||||
struct Environment;
|
||||
|
||||
struct Object
|
||||
{
|
||||
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
|
||||
struct Function
|
||||
{
|
||||
const std::string name;
|
||||
const std::vector<std::string> params;
|
||||
const std::vector<std::shared_ptr<Stmt>> body;
|
||||
const std::shared_ptr<Environment> closure;
|
||||
const std::string ownerClass; // empty for non-methods
|
||||
|
||||
Function(std::string name, std::vector<std::string> params,
|
||||
std::vector<std::shared_ptr<Stmt>> body,
|
||||
std::shared_ptr<Environment> closure)
|
||||
: name(name), params(params), body(body), closure(closure) {}
|
||||
std::shared_ptr<Environment> 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::function<Value(std::vector<Value>, int, int)> func;
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include <cmath>
|
||||
#include <stdexcept>
|
||||
@ -13,6 +14,7 @@ struct Environment;
|
||||
struct Function;
|
||||
struct BuiltinFunction;
|
||||
struct Thunk;
|
||||
struct Module;
|
||||
|
||||
// Type tags for the Value union
|
||||
enum ValueType {
|
||||
@ -24,9 +26,12 @@ enum ValueType {
|
||||
VAL_BUILTIN_FUNCTION,
|
||||
VAL_THUNK,
|
||||
VAL_ARRAY,
|
||||
VAL_DICT
|
||||
VAL_DICT,
|
||||
VAL_MODULE
|
||||
};
|
||||
|
||||
// (moved below Value)
|
||||
|
||||
// Tagged value system (like Lua) - no heap allocation for simple values
|
||||
struct Value {
|
||||
union {
|
||||
@ -37,6 +42,7 @@ struct Value {
|
||||
std::string string_value; // Store strings outside the union for safety
|
||||
std::shared_ptr<std::vector<Value> > array_value; // Store arrays as shared_ptr for mutability
|
||||
std::shared_ptr<std::unordered_map<std::string, Value> > dict_value; // Store dictionaries as shared_ptr for mutability
|
||||
std::shared_ptr<Module> module_value; // Module object
|
||||
|
||||
// Store functions as shared_ptr for proper reference counting
|
||||
std::shared_ptr<Function> function;
|
||||
@ -58,6 +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(const std::unordered_map<std::string, Value>& dict) : type(ValueType::VAL_DICT), dict_value(std::make_shared<std::unordered_map<std::string, Value> >(dict)) {}
|
||||
Value(std::unordered_map<std::string, Value>&& dict) : type(ValueType::VAL_DICT), dict_value(std::make_shared<std::unordered_map<std::string, Value> >(std::move(dict))) {}
|
||||
Value(std::shared_ptr<Module> m) : type(ValueType::VAL_MODULE), module_value(std::move(m)) {}
|
||||
|
||||
// Destructor to clean up functions and thunks
|
||||
~Value() {
|
||||
@ -70,7 +77,7 @@ struct Value {
|
||||
// Move constructor
|
||||
Value(Value&& other) noexcept
|
||||
: type(other.type), string_value(std::move(other.string_value)), array_value(std::move(other.array_value)), dict_value(std::move(other.dict_value)),
|
||||
function(std::move(other.function)), builtin_function(std::move(other.builtin_function)), thunk(std::move(other.thunk)) {
|
||||
function(std::move(other.function)), builtin_function(std::move(other.builtin_function)), thunk(std::move(other.thunk)), module_value(std::move(other.module_value)) {
|
||||
if (type != ValueType::VAL_STRING && type != ValueType::VAL_ARRAY && type != ValueType::VAL_DICT &&
|
||||
type != ValueType::VAL_FUNCTION && type != ValueType::VAL_BUILTIN_FUNCTION && type != ValueType::VAL_THUNK) {
|
||||
number = other.number; // Copy the union
|
||||
@ -94,6 +101,8 @@ struct Value {
|
||||
builtin_function = std::move(other.builtin_function);
|
||||
} else if (type == ValueType::VAL_THUNK) {
|
||||
thunk = std::move(other.thunk);
|
||||
} else if (type == ValueType::VAL_MODULE) {
|
||||
module_value = std::move(other.module_value);
|
||||
} else {
|
||||
number = other.number;
|
||||
}
|
||||
@ -117,6 +126,8 @@ struct Value {
|
||||
builtin_function = other.builtin_function; // shared_ptr automatically handles sharing
|
||||
} else if (type == ValueType::VAL_THUNK) {
|
||||
thunk = other.thunk; // shared_ptr automatically handles sharing
|
||||
} else if (type == ValueType::VAL_MODULE) {
|
||||
module_value = other.module_value; // shared module
|
||||
} else {
|
||||
number = other.number;
|
||||
}
|
||||
@ -146,6 +157,8 @@ struct Value {
|
||||
builtin_function = other.builtin_function; // shared_ptr automatically handles sharing
|
||||
} else if (type == ValueType::VAL_THUNK) {
|
||||
thunk = other.thunk; // shared_ptr automatically handles sharing
|
||||
} else if (type == ValueType::VAL_MODULE) {
|
||||
module_value = other.module_value;
|
||||
} else {
|
||||
number = other.number;
|
||||
}
|
||||
@ -161,6 +174,7 @@ struct Value {
|
||||
inline bool isBuiltinFunction() const { return type == ValueType::VAL_BUILTIN_FUNCTION; }
|
||||
inline bool isArray() const { return type == ValueType::VAL_ARRAY; }
|
||||
inline bool isDict() const { return type == ValueType::VAL_DICT; }
|
||||
inline bool isModule() const { return type == ValueType::VAL_MODULE; }
|
||||
inline bool isThunk() const { return type == ValueType::VAL_THUNK; }
|
||||
inline bool isNone() const { return type == ValueType::VAL_NONE; }
|
||||
|
||||
@ -176,6 +190,7 @@ struct Value {
|
||||
case ValueType::VAL_THUNK: return "thunk";
|
||||
case ValueType::VAL_ARRAY: return "array";
|
||||
case ValueType::VAL_DICT: return "dict";
|
||||
case ValueType::VAL_MODULE: return "module";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
@ -198,6 +213,7 @@ struct Value {
|
||||
inline std::unordered_map<std::string, Value>& asDict() {
|
||||
return *dict_value;
|
||||
}
|
||||
inline Module* asModule() const { return isModule() ? module_value.get() : nullptr; }
|
||||
inline Function* asFunction() const { return isFunction() ? function.get() : nullptr; }
|
||||
inline BuiltinFunction* asBuiltinFunction() const { return isBuiltinFunction() ? builtin_function.get() : nullptr; }
|
||||
inline Thunk* asThunk() const { return isThunk() ? thunk.get() : nullptr; }
|
||||
@ -214,6 +230,7 @@ struct Value {
|
||||
case ValueType::VAL_THUNK: return thunk != nullptr;
|
||||
case ValueType::VAL_ARRAY: return !array_value->empty();
|
||||
case ValueType::VAL_DICT: return !dict_value->empty();
|
||||
case ValueType::VAL_MODULE: return module_value != nullptr;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
@ -296,6 +313,12 @@ struct Value {
|
||||
result += "}";
|
||||
return result;
|
||||
}
|
||||
case ValueType::VAL_MODULE: {
|
||||
// Avoid accessing Module fields when it's still an incomplete type in some TUs.
|
||||
// Delegate formatting to a small helper defined out-of-line in Value.cpp.
|
||||
extern std::string formatModuleForToString(const std::shared_ptr<Module>&);
|
||||
return formatModuleForToString(module_value);
|
||||
}
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
@ -420,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
|
||||
extern const Value NONE_VALUE;
|
||||
extern const Value TRUE_VALUE;
|
||||
extern const Value FALSE_VALUE;
|
||||
extern const Value ZERO_VALUE;
|
||||
extern const Value ONE_VALUE;
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
41
src/sources/builtinModules/base64.cpp
Normal file
41
src/sources/builtinModules/base64.cpp
Normal 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()));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
64
src/sources/builtinModules/eval.cpp
Normal file
64
src/sources/builtinModules/eval.cpp
Normal 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();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
72
src/sources/builtinModules/io.cpp
Normal file
72
src/sources/builtinModules/io.cpp
Normal 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
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
83
src/sources/builtinModules/json.cpp
Normal file
83
src/sources/builtinModules/json.cpp
Normal 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]));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
56
src/sources/builtinModules/math.cpp
Normal file
56
src/sources/builtinModules/math.cpp
Normal 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));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
132
src/sources/builtinModules/os.cpp
Normal file
132
src/sources/builtinModules/os.cpp
Normal 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
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
63
src/sources/builtinModules/path.cpp
Normal file
63
src/sources/builtinModules/path.cpp
Normal 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());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
35
src/sources/builtinModules/rand.cpp
Normal file
35
src/sources/builtinModules/rand.cpp
Normal 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)];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
25
src/sources/builtinModules/register.cpp
Normal file
25
src/sources/builtinModules/register.cpp
Normal 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
|
||||
}
|
||||
|
||||
|
||||
99
src/sources/builtinModules/sys.cpp
Normal file
99
src/sources/builtinModules/sys.cpp
Normal 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
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
31
src/sources/builtinModules/time.cpp
Normal file
31
src/sources/builtinModules/time.cpp
Normal 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;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -3,35 +3,23 @@
|
||||
#include "bob.h"
|
||||
#include "Parser.h"
|
||||
|
||||
void Bob::ensureInterpreter(bool interactive) {
|
||||
if (!interpreter) interpreter = msptr(Interpreter)(interactive);
|
||||
applyPendingConfigs();
|
||||
}
|
||||
|
||||
void Bob::runFile(const std::string& path)
|
||||
{
|
||||
this->interpreter = msptr(Interpreter)(false);
|
||||
std::ifstream file = std::ifstream(path);
|
||||
|
||||
std::string source;
|
||||
|
||||
if(file.is_open()){
|
||||
source = std::string(std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>());
|
||||
ensureInterpreter(false);
|
||||
interpreter->addStdLibFunctions();
|
||||
if (!evalFile(path)) {
|
||||
std::cout << "Execution failed\n";
|
||||
}
|
||||
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()
|
||||
{
|
||||
this->interpreter = msptr(Interpreter)(true);
|
||||
|
||||
ensureInterpreter(true);
|
||||
std::cout << "Bob v" << VERSION << ", 2025\n";
|
||||
while(true)
|
||||
{
|
||||
@ -46,50 +34,40 @@ void Bob::runPrompt()
|
||||
|
||||
// Reset error state before each REPL command
|
||||
errorReporter.resetErrorState();
|
||||
interpreter->addStdLibFunctions();
|
||||
(void)evalString(line, "REPL");
|
||||
}
|
||||
}
|
||||
|
||||
// Load source code into error reporter for context
|
||||
errorReporter.loadSource(line, "REPL");
|
||||
|
||||
// Connect error reporter to interpreter
|
||||
bool Bob::evalFile(const std::string& path) {
|
||||
std::ifstream file(path);
|
||||
if (!file.is_open()) return false;
|
||||
std::string src((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
||||
errorReporter.loadSource(src, path);
|
||||
interpreter->setErrorReporter(&errorReporter);
|
||||
|
||||
this->run(line);
|
||||
}
|
||||
}
|
||||
|
||||
void Bob::run(std::string source)
|
||||
{
|
||||
try {
|
||||
// Connect error reporter to lexer
|
||||
lexer.setErrorReporter(&errorReporter);
|
||||
|
||||
std::vector<Token> tokens = lexer.Tokenize(std::move(source));
|
||||
auto tokens = lexer.Tokenize(src);
|
||||
Parser p(tokens);
|
||||
|
||||
// Connect error reporter to parser
|
||||
p.setErrorReporter(&errorReporter);
|
||||
|
||||
std::vector<sptr(Stmt)> statements = p.parse();
|
||||
auto statements = p.parse();
|
||||
interpreter->interpret(statements);
|
||||
}
|
||||
catch(std::exception &e)
|
||||
{
|
||||
// Only suppress errors that have already been reported by the error reporter
|
||||
if (errorReporter.hasReportedError()) {
|
||||
return;
|
||||
}
|
||||
return true;
|
||||
} catch (...) { return false; }
|
||||
}
|
||||
|
||||
// For errors that weren't reported (like parser errors, undefined variables, etc.)
|
||||
// print them normally
|
||||
std::cout << "Error: " << e.what() << '\n';
|
||||
return;
|
||||
}
|
||||
catch(const std::exception& e)
|
||||
{
|
||||
// Unknown error - report it since it wasn't handled by the interpreter
|
||||
errorReporter.reportError(0, 0, "Unknown Error", "An unknown error occurred: " + std::string(e.what()));
|
||||
return;
|
||||
}
|
||||
bool Bob::evalString(const std::string& code, const std::string& filename) {
|
||||
errorReporter.loadSource(code, filename);
|
||||
interpreter->setErrorReporter(&errorReporter);
|
||||
try {
|
||||
lexer.setErrorReporter(&errorReporter);
|
||||
auto tokens = lexer.Tokenize(code);
|
||||
Parser p(tokens);
|
||||
p.setErrorReporter(&errorReporter);
|
||||
auto statements = p.parse();
|
||||
interpreter->interpret(statements);
|
||||
return true;
|
||||
} catch (...) { return false; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -2,13 +2,27 @@
|
||||
|
||||
//
|
||||
#include "bob.h"
|
||||
#include "Interpreter.h"
|
||||
|
||||
int main(int argc, char* argv[]){
|
||||
Bob bobLang;
|
||||
// Enable open preset (all builtins, file imports allowed)
|
||||
bobLang.setSafetyPreset("open");
|
||||
|
||||
|
||||
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]);
|
||||
} 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();
|
||||
}
|
||||
|
||||
|
||||
@ -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) {
|
||||
hadError = true;
|
||||
displaySourceContext(line, column, errorType, message, operator_, showArrow);
|
||||
@ -75,6 +98,8 @@ void ErrorReporter::reportErrorWithContext(const ErrorContext& context) {
|
||||
|
||||
if (!context.fileName.empty()) {
|
||||
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) +
|
||||
|
||||
@ -363,18 +363,14 @@ std::vector<Token> Lexer::Tokenize(std::string source){
|
||||
|
||||
}
|
||||
if(!isNotation) {
|
||||
if (!src.empty() && src[0] == '.') {
|
||||
advance();
|
||||
if (!src.empty() && std::isdigit(src[0])) {
|
||||
// Only treat '.' as part of the number if followed by a digit
|
||||
if (src.size() > 1 && src[0] == '.' && std::isdigit(src[1])) {
|
||||
advance(); // consume '.'
|
||||
num += '.';
|
||||
while (!src.empty() && std::isdigit(src[0])) {
|
||||
num += src[0];
|
||||
advance();
|
||||
}
|
||||
} else {
|
||||
throw std::runtime_error("LEXER: malformed number at: " + std::to_string(this->line));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
@ -252,21 +252,35 @@ sptr(Expr) Parser::postfix()
|
||||
{
|
||||
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})) {
|
||||
Token oper = previous();
|
||||
|
||||
// Ensure the expression is a variable or array indexing
|
||||
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) {
|
||||
errorReporter->reportError(oper.line, oper.column, "Parse 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.");
|
||||
}
|
||||
|
||||
return msptr(IncrementExpr)(expr, oper, false); // false = postfix
|
||||
expr = msptr(IncrementExpr)(expr, oper, false);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return expr;
|
||||
@ -281,8 +295,15 @@ sptr(Expr) Parser::primary()
|
||||
if(match({NUMBER})) return msptr(LiteralExpr)(previous().lexeme, true, false, false);
|
||||
if(match({STRING})) return msptr(LiteralExpr)(previous().lexeme, false, false, false);
|
||||
|
||||
if(match( {IDENTIFIER})) {
|
||||
return call();
|
||||
if(match( {IDENTIFIER, THIS, SUPER})) {
|
||||
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}))
|
||||
@ -360,7 +381,13 @@ sptr(Expr) Parser::dictLiteral()
|
||||
|
||||
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) {
|
||||
if (match({OPEN_PAREN})) {
|
||||
@ -396,6 +423,8 @@ sptr(Stmt) Parser::declaration()
|
||||
try{
|
||||
if(match({VAR})) return varDeclaration();
|
||||
if(match({FUNCTION})) return functionDeclaration();
|
||||
if(match({CLASS})) return classDeclaration();
|
||||
if(match({EXTENSION})) return extensionDeclaration();
|
||||
return statement();
|
||||
}
|
||||
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()
|
||||
{
|
||||
Token name = consume(IDENTIFIER, "Expected variable name.");
|
||||
@ -477,6 +586,13 @@ std::shared_ptr<Expr> Parser::functionExpression() {
|
||||
sptr(Stmt) Parser::statement()
|
||||
{
|
||||
if(match({RETURN})) return returnStatement();
|
||||
if(match({IMPORT})) return importStatement();
|
||||
// Fallback if lexer didn't classify keyword: detect by lexeme
|
||||
if (check(IDENTIFIER) && peek().lexeme == "import") { advance(); return importStatement(); }
|
||||
if(match({FROM})) return fromImportStatement();
|
||||
if (check(IDENTIFIER) && peek().lexeme == "from") { advance(); return fromImportStatement(); }
|
||||
if(match({TRY})) return tryStatement();
|
||||
if(match({THROW})) return throwStatement();
|
||||
if(match({IF})) return ifStatement();
|
||||
if(match({DO})) return doWhileStatement();
|
||||
if(match({WHILE})) return whileStatement();
|
||||
@ -486,7 +602,7 @@ sptr(Stmt) Parser::statement()
|
||||
if(match({OPEN_BRACE})) return msptr(BlockStmt)(block());
|
||||
|
||||
// Check for assignment statement - simplified approach
|
||||
if(check(IDENTIFIER)) {
|
||||
if(check(IDENTIFIER) || check(THIS)) {
|
||||
// Try to parse as assignment expression first
|
||||
int currentPos = current;
|
||||
try {
|
||||
@ -511,6 +627,43 @@ sptr(Stmt) Parser::statement()
|
||||
return expressionStatement();
|
||||
}
|
||||
|
||||
std::shared_ptr<Stmt> Parser::importStatement() {
|
||||
Token importTok = previous();
|
||||
// import Name [as Alias] | import "path"
|
||||
bool isString = check(STRING);
|
||||
Token mod = isString ? advance() : consume(IDENTIFIER, "Expected module name or path string after 'import'.");
|
||||
// Keep IDENTIFIER for name-based imports; resolver will try file and then builtin
|
||||
bool hasAlias = false; Token alias;
|
||||
if (match({AS})) {
|
||||
hasAlias = true;
|
||||
alias = consume(IDENTIFIER, "Expected alias identifier after 'as'.");
|
||||
}
|
||||
consume(SEMICOLON, "Expected ';' after import statement.");
|
||||
return msptr(ImportStmt)(importTok, mod, hasAlias, alias);
|
||||
}
|
||||
|
||||
std::shared_ptr<Stmt> Parser::fromImportStatement() {
|
||||
Token fromTok = previous();
|
||||
bool isString = check(STRING);
|
||||
Token mod = isString ? advance() : consume(IDENTIFIER, "Expected module name or path string after 'from'.");
|
||||
// Keep IDENTIFIER for name-based from-imports
|
||||
consume(IMPORT, "Expected 'import' after module name.");
|
||||
// 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()
|
||||
{
|
||||
Token name = consume(IDENTIFIER, "Expected variable name for assignment.");
|
||||
@ -676,6 +829,35 @@ std::vector<sptr(Stmt)> Parser::block()
|
||||
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) {
|
||||
std::vector<sptr(Expr)> arguments;
|
||||
|
||||
|
||||
@ -2,6 +2,15 @@
|
||||
#include "ErrorReporter.h"
|
||||
|
||||
void Environment::assign(const Token& name, const Value& value) {
|
||||
// Disallow reassignment of module bindings (immutability of module variable)
|
||||
auto itv = variables.find(name.lexeme);
|
||||
if (itv != variables.end() && itv->second.isModule()) {
|
||||
if (errorReporter) {
|
||||
errorReporter->reportError(name.line, name.column, "Import Error",
|
||||
"Cannot reassign module binding '" + name.lexeme + "'", "");
|
||||
}
|
||||
throw std::runtime_error("Cannot reassign module binding '" + name.lexeme + "'");
|
||||
}
|
||||
auto it = variables.find(name.lexeme);
|
||||
if (it != variables.end()) {
|
||||
it->second = value;
|
||||
@ -13,7 +22,9 @@ void Environment::assign(const Token& name, const Value& value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Report only if not within a try; otherwise let try/catch handle
|
||||
if (errorReporter) {
|
||||
// We cannot check tryDepth here directly; rely on Executor to suppress double-reporting
|
||||
errorReporter->reportError(name.line, name.column, "Runtime Error",
|
||||
"Undefined variable '" + name.lexeme + "'", "");
|
||||
}
|
||||
@ -37,27 +48,14 @@ Value Environment::get(const Token& name) {
|
||||
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() {
|
||||
for (auto &entry : variables) {
|
||||
Value &v = entry.second;
|
||||
if (v.isArray()) {
|
||||
// Replace with a new empty array to avoid mutating original shared storage
|
||||
entry.second = Value(std::vector<Value>{});
|
||||
} 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>{});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
#include "Evaluator.h"
|
||||
#include "Interpreter.h"
|
||||
#include "Environment.h"
|
||||
#include "AssignmentUtils.h"
|
||||
#include "helperFunctions/HelperFunctions.h"
|
||||
|
||||
Evaluator::Evaluator(Interpreter* interpreter) : interpreter(interpreter) {}
|
||||
@ -35,8 +37,6 @@ Value Evaluator::visitUnaryExpr(const std::shared_ptr<UnaryExpr>& expression)
|
||||
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());
|
||||
@ -46,8 +46,6 @@ Value Evaluator::visitUnaryExpr(const std::shared_ptr<UnaryExpr>& expression)
|
||||
|
||||
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()))));
|
||||
@ -104,8 +102,6 @@ Value Evaluator::visitBinaryExpr(const std::shared_ptr<BinaryExpr>& expression)
|
||||
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()));
|
||||
}
|
||||
|
||||
@ -115,8 +111,23 @@ Value Evaluator::visitBinaryExpr(const std::shared_ptr<BinaryExpr>& expression)
|
||||
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 SLASH: {
|
||||
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_OR: 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);
|
||||
}
|
||||
} 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;
|
||||
throw; // Propagate to statement driver (try/catch) without reporting here
|
||||
}
|
||||
}
|
||||
|
||||
@ -164,7 +172,7 @@ Value Evaluator::visitIncrementExpr(const std::shared_ptr<IncrementExpr>& expres
|
||||
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)) {
|
||||
interpreter->getEnvironment()->assign(varExpr->name, Value(newValue));
|
||||
} 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
|
||||
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 {
|
||||
interpreter->reportError(expression->oper.line, expression->oper.column,
|
||||
"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);
|
||||
|
||||
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
|
||||
interpreter->getEnvironment()->assign(expression->name, value);
|
||||
|
||||
// Now that the old values are released, perform cleanup on any reassignment
|
||||
// Perform cleanup on any reassignment
|
||||
interpreter->forceCleanup();
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Error during assignment: " << e.what() << std::endl;
|
||||
throw; // Re-throw to see the full stack trace
|
||||
return value;
|
||||
}
|
||||
} else {
|
||||
// Handle compound assignment operators
|
||||
|
||||
// Compound assignment operators
|
||||
Value currentValue = interpreter->getEnvironment()->get(expression->name);
|
||||
Value newValue;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
try {
|
||||
Value newValue = computeCompoundAssignment(currentValue, expression->op.type, value);
|
||||
interpreter->getEnvironment()->assign(expression->name, 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) {
|
||||
@ -318,6 +284,7 @@ Value Evaluator::visitArrayIndexExpr(const std::shared_ptr<ArrayIndexExpr>& expr
|
||||
if (!index.isNumber()) {
|
||||
interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error",
|
||||
"Array index must be a number", "");
|
||||
interpreter->markInlineErrorReported();
|
||||
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())) {
|
||||
interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error",
|
||||
"Array index out of bounds", "");
|
||||
interpreter->markInlineErrorReported();
|
||||
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()) {
|
||||
interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error",
|
||||
"Dictionary key must be a string", "");
|
||||
interpreter->markInlineErrorReported();
|
||||
throw std::runtime_error("Dictionary key must be a string");
|
||||
}
|
||||
|
||||
@ -353,6 +322,7 @@ Value Evaluator::visitArrayIndexExpr(const std::shared_ptr<ArrayIndexExpr>& expr
|
||||
} else {
|
||||
interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error",
|
||||
"Can only index arrays and dictionaries", "");
|
||||
interpreter->markInlineErrorReported();
|
||||
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);
|
||||
std::string propertyName = expr->name.lexeme;
|
||||
|
||||
if (object.isDict()) {
|
||||
return getDictProperty(object, propertyName);
|
||||
if (object.isModule()) {
|
||||
// Forward to module exports
|
||||
auto* mod = object.asModule();
|
||||
if (mod && mod->exports) {
|
||||
auto it = mod->exports->find(propertyName);
|
||||
if (it != mod->exports->end()) return it->second;
|
||||
}
|
||||
return NONE_VALUE;
|
||||
} else if (object.isDict()) {
|
||||
Value v = getDictProperty(object, propertyName);
|
||||
if (!v.isNone()) {
|
||||
// If this is an inherited inline method, prefer a current-class extension override
|
||||
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()) {
|
||||
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 {
|
||||
// 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",
|
||||
"Cannot access property '" + propertyName + "' on this type", "");
|
||||
interpreter->markInlineErrorReported();
|
||||
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()) {
|
||||
// Handle array assignment
|
||||
if (!index.isNumber()) {
|
||||
if (!interpreter->isInTry()) {
|
||||
interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error",
|
||||
"Array index must be a number", "");
|
||||
interpreter->markInlineErrorReported();
|
||||
}
|
||||
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();
|
||||
|
||||
if (idx < 0 || idx >= static_cast<int>(arr.size())) {
|
||||
if (!interpreter->isInTry()) {
|
||||
interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error",
|
||||
"Array index out of bounds", "");
|
||||
interpreter->markInlineErrorReported();
|
||||
}
|
||||
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()) {
|
||||
// Handle dictionary assignment
|
||||
if (!index.isString()) {
|
||||
if (!interpreter->isInTry()) {
|
||||
interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error",
|
||||
"Dictionary key must be a string", "");
|
||||
interpreter->markInlineErrorReported();
|
||||
}
|
||||
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;
|
||||
|
||||
} else {
|
||||
if (!interpreter->isInTry()) {
|
||||
interpreter->reportError(expr->bracket.line, expr->bracket.column, "Runtime Error",
|
||||
"Can only assign to array or dictionary elements", "");
|
||||
interpreter->markInlineErrorReported();
|
||||
}
|
||||
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);
|
||||
std::string propertyName = expr->name.lexeme;
|
||||
|
||||
if (object.isDict()) {
|
||||
if (object.isModule()) {
|
||||
// Modules are immutable: disallow setting properties
|
||||
if (!interpreter->isInTry()) {
|
||||
interpreter->reportError(expr->name.line, expr->name.column, "Import Error",
|
||||
"Cannot assign property '" + propertyName + "' on module (immutable)", "");
|
||||
interpreter->markInlineErrorReported();
|
||||
}
|
||||
throw std::runtime_error("Cannot assign property on module (immutable)");
|
||||
} else if (object.isDict()) {
|
||||
// Modify the dictionary in place
|
||||
std::unordered_map<std::string, Value>& dict = object.asDict();
|
||||
dict[propertyName] = value;
|
||||
return value; // Return the assigned value
|
||||
} else {
|
||||
if (!interpreter->isInTry()) {
|
||||
interpreter->reportError(expr->name.line, expr->name.column, "Runtime Error",
|
||||
"Cannot assign property '" + propertyName + "' on non-object", "");
|
||||
interpreter->markInlineErrorReported();
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
// Capture a snapshot of the current environment so loop vars like 'i' are frozen per iteration
|
||||
auto closureEnv = std::make_shared<Environment>(*interpreter->getEnvironment());
|
||||
closureEnv->pruneForClosureCapture();
|
||||
|
||||
auto function = std::make_shared<Function>("", paramNames, expression->body, closureEnv);
|
||||
return Value(function);
|
||||
}
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
#include "Executor.h"
|
||||
#include "Evaluator.h"
|
||||
#include "Interpreter.h"
|
||||
#include "Environment.h"
|
||||
#include "Parser.h"
|
||||
#include "AssignmentUtils.h"
|
||||
#include <iostream>
|
||||
|
||||
Executor::Executor(Interpreter* interpreter, Evaluator* evaluator)
|
||||
@ -9,13 +12,47 @@ Executor::Executor(Interpreter* interpreter, Evaluator* evaluator)
|
||||
Executor::~Executor() {}
|
||||
|
||||
void Executor::interpret(const std::vector<std::shared_ptr<Stmt>>& statements) {
|
||||
ExecutionContext top;
|
||||
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) {
|
||||
try {
|
||||
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) {
|
||||
@ -24,7 +61,14 @@ void Executor::executeBlock(const std::vector<std::shared_ptr<Stmt>>& statements
|
||||
|
||||
for (const auto& statement : statements) {
|
||||
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);
|
||||
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) {
|
||||
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())
|
||||
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;
|
||||
if (statement->initializer != nullptr) {
|
||||
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);
|
||||
}
|
||||
@ -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) {
|
||||
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);
|
||||
} else if (statement->elseBranch != nullptr) {
|
||||
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))) {
|
||||
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 (context) {
|
||||
context->hasReturn = true;
|
||||
@ -120,26 +174,17 @@ void Executor::visitDoWhileStmt(const std::shared_ptr<DoWhileStmt>& statement, E
|
||||
loopContext.isFunctionBody = context->isFunctionBody;
|
||||
}
|
||||
|
||||
do {
|
||||
while (true) {
|
||||
execute(statement->body, &loopContext);
|
||||
|
||||
if (loopContext.hasReturn) {
|
||||
if (context) {
|
||||
context->hasReturn = true;
|
||||
context->returnValue = loopContext.returnValue;
|
||||
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 (context) { context->hasReturn = true; context->returnValue = loopContext.returnValue; } break; }
|
||||
if (loopContext.shouldBreak) { break; }
|
||||
if (loopContext.shouldContinue) { loopContext.shouldContinue = false; }
|
||||
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) {
|
||||
@ -152,9 +197,15 @@ void Executor::visitForStmt(const std::shared_ptr<ForStmt>& statement, Execution
|
||||
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);
|
||||
|
||||
if (loopContext.hasThrow) { if (context) { context->hasThrow = true; context->thrownValue = loopContext.thrownValue; } break; }
|
||||
if (loopContext.hasReturn) {
|
||||
if (context) {
|
||||
context->hasReturn = true;
|
||||
@ -171,12 +222,16 @@ void Executor::visitForStmt(const std::shared_ptr<ForStmt>& statement, Execution
|
||||
loopContext.shouldContinue = false;
|
||||
if (statement->increment != nullptr) {
|
||||
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;
|
||||
}
|
||||
|
||||
if (statement->increment != nullptr) {
|
||||
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) {
|
||||
try {
|
||||
Value value = statement->value->accept(evaluator);
|
||||
|
||||
if (statement->op.type == EQUAL) {
|
||||
try {
|
||||
// Assign first to release references held by the old value
|
||||
interpreter->getEnvironment()->assign(statement->name, value);
|
||||
|
||||
// Clean up on any reassignment, regardless of old/new type
|
||||
// Clean up on any reassignment
|
||||
interpreter->forceCleanup();
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Error during assignment: " << e.what() << std::endl;
|
||||
throw; // Re-throw to see the full stack trace
|
||||
return;
|
||||
}
|
||||
} 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:
|
||||
// Compound assignment operators
|
||||
Value currentValue = interpreter->getEnvironment()->get(statement->name);
|
||||
try {
|
||||
Value newValue = computeCompoundAssignment(currentValue, statement->op.type, value);
|
||||
interpreter->getEnvironment()->assign(statement->name, newValue);
|
||||
} catch (const std::runtime_error&) {
|
||||
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);
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Error in visitAssignStmt: " << e.what() << std::endl;
|
||||
throw; // Re-throw to see the full stack trace
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
void Executor::visitClassStmt(const std::shared_ptr<ClassStmt>& statement, ExecutionContext* context) {
|
||||
std::unordered_map<std::string, Value> classDict;
|
||||
// If parent exists, copy parent's methods as defaults (single inheritance prototype copy)
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,19 @@
|
||||
#include "Interpreter.h"
|
||||
#include "register.h"
|
||||
#include "Evaluator.h"
|
||||
#include "Executor.h"
|
||||
#include "BobStdLib.h"
|
||||
#include "ErrorReporter.h"
|
||||
#include "Environment.h"
|
||||
#include "Expression.h"
|
||||
#include "Parser.h"
|
||||
#include <filesystem>
|
||||
#if defined(_WIN32)
|
||||
#include <direct.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
|
||||
Interpreter::Interpreter(bool isInteractive)
|
||||
@ -9,6 +21,11 @@ Interpreter::Interpreter(bool isInteractive)
|
||||
evaluator = std::make_unique<Evaluator>(this);
|
||||
executor = std::make_unique<Executor>(this, evaluator.get());
|
||||
environment = std::make_shared<Environment>();
|
||||
// Default module search paths: current dir and tests
|
||||
moduleSearchPaths = { ".", "tests" };
|
||||
|
||||
// Register all builtin modules via aggregator
|
||||
registerAllBuiltinModules(*this);
|
||||
}
|
||||
|
||||
Interpreter::~Interpreter() = default;
|
||||
@ -32,6 +49,25 @@ Value Interpreter::evaluate(const std::shared_ptr<Expr>& expr) {
|
||||
}
|
||||
return runTrampoline(result);
|
||||
}
|
||||
bool Interpreter::defineGlobalVar(const std::string& name, const Value& value) {
|
||||
if (!environment) return false;
|
||||
try {
|
||||
environment->define(name, value);
|
||||
return true;
|
||||
} catch (...) { return false; }
|
||||
}
|
||||
|
||||
bool Interpreter::tryGetGlobalVar(const std::string& name, Value& out) const {
|
||||
if (!environment) return false;
|
||||
try {
|
||||
out = environment->get(Token{IDENTIFIER, name, 0, 0});
|
||||
return true;
|
||||
} catch (...) { return false; }
|
||||
}
|
||||
|
||||
bool Interpreter::hasReportedError() const {
|
||||
return inlineErrorReported;
|
||||
}
|
||||
|
||||
Value Interpreter::runTrampoline(Value initialResult) {
|
||||
Value current = initialResult;
|
||||
@ -56,14 +92,169 @@ std::string Interpreter::stringify(Value object) {
|
||||
void Interpreter::addStdLibFunctions() {
|
||||
BobStdLib::addToEnvironment(environment, *this, errorReporter);
|
||||
}
|
||||
void Interpreter::setModulePolicy(bool allowFiles, bool preferFiles, const std::vector<std::string>& searchPaths) {
|
||||
allowFileImports = allowFiles;
|
||||
preferFileOverBuiltin = preferFiles;
|
||||
moduleSearchPaths = searchPaths;
|
||||
}
|
||||
|
||||
static std::string joinPath(const std::string& baseDir, const std::string& rel) {
|
||||
namespace fs = std::filesystem;
|
||||
fs::path p = fs::path(baseDir) / fs::path(rel);
|
||||
return fs::path(p).lexically_normal().string();
|
||||
}
|
||||
|
||||
static std::string locateModuleFile(const std::string& baseDir, const std::vector<std::string>& searchPaths, const std::string& nameDotBob) {
|
||||
namespace fs = std::filesystem;
|
||||
// Only search relative to the importing file's directory
|
||||
// 1) baseDir/name.bob
|
||||
if (!baseDir.empty()) {
|
||||
std::string p = joinPath(baseDir, nameDotBob);
|
||||
if (fs::exists(fs::path(p))) return p;
|
||||
}
|
||||
// 2) baseDir/searchPath/name.bob (search paths are relative to baseDir)
|
||||
for (const auto& sp : searchPaths) {
|
||||
if (!baseDir.empty()) {
|
||||
std::string pb = joinPath(baseDir, joinPath(sp, nameDotBob));
|
||||
if (fs::exists(fs::path(pb))) return pb;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
Value Interpreter::importModule(const std::string& spec, int line, int column) {
|
||||
// Determine if spec is a path string (heuristic: contains '/' or ends with .bob)
|
||||
bool looksPath = spec.find('/') != std::string::npos || (spec.size() >= 4 && spec.rfind(".bob") == spec.size() - 4) || spec.find("..") != std::string::npos;
|
||||
// Cache key resolution
|
||||
std::string key = spec;
|
||||
std::string baseDir = "";
|
||||
if (errorReporter && !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) {
|
||||
builtinFunctions.push_back(func);
|
||||
}
|
||||
|
||||
void Interpreter::addThunk(std::shared_ptr<Thunk> thunk) {
|
||||
thunks.push_back(thunk);
|
||||
}
|
||||
|
||||
|
||||
void Interpreter::addFunction(std::shared_ptr<Function> function) {
|
||||
functions.push_back(function);
|
||||
@ -74,7 +265,6 @@ void Interpreter::setErrorReporter(ErrorReporter* reporter) {
|
||||
if (environment) {
|
||||
environment->setErrorReporter(reporter);
|
||||
}
|
||||
addStdLibFunctions();
|
||||
}
|
||||
|
||||
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) {
|
||||
// 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) {
|
||||
errorReporter->reportError(line, column, errorType, message, lexeme);
|
||||
}
|
||||
@ -107,22 +303,277 @@ void Interpreter::forceCleanup() {
|
||||
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 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()) {
|
||||
// Handle builtin functions with direct evaluation
|
||||
std::vector<Value> arguments;
|
||||
for (const auto& argument : expression->arguments) {
|
||||
arguments.push_back(evaluate(argument)); // Direct call
|
||||
arguments.push_back(evaluate(argument));
|
||||
}
|
||||
BuiltinFunction* builtinFunction = callee.asBuiltinFunction();
|
||||
return builtinFunction->func(arguments, expression->paren.line, expression->paren.column);
|
||||
}
|
||||
|
||||
if (!callee.isFunction()) {
|
||||
// Provide better error message with type information (like original)
|
||||
std::string errorMsg = "Can only call functions, got " + callee.getType();
|
||||
std::string errorMsg = isSuperCall ? ("Undefined super method '" + methodName + "'")
|
||||
: ("Can only call functions, got " + callee.getType());
|
||||
if (errorReporter) {
|
||||
errorReporter->reportError(expression->paren.line, expression->paren.column, "Runtime Error",
|
||||
errorMsg, "");
|
||||
@ -134,7 +585,7 @@ Value Interpreter::evaluateCallExprInline(const std::shared_ptr<CallExpr>& expre
|
||||
|
||||
std::vector<Value> 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)
|
||||
@ -150,12 +601,18 @@ Value Interpreter::evaluateCallExprInline(const std::shared_ptr<CallExpr>& expre
|
||||
|
||||
// Check if this is a tail call for inline TCO
|
||||
if (expression->isTailCall) {
|
||||
// Create a thunk for tail call optimization - original inline version
|
||||
auto thunk = std::make_shared<Thunk>([this, function, arguments]() -> Value {
|
||||
// Use RAII to manage environment (exactly like original)
|
||||
|
||||
auto thunk = std::make_shared<Thunk>([this, function, arguments, isMethodCall, receiver, isSuperCall]() -> Value {
|
||||
ScopedEnv _env(environment);
|
||||
environment = std::make_shared<Environment>(function->closure);
|
||||
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++) {
|
||||
environment->define(function->params[i], arguments[i]);
|
||||
@ -164,12 +621,11 @@ Value Interpreter::evaluateCallExprInline(const std::shared_ptr<CallExpr>& expre
|
||||
ExecutionContext context;
|
||||
context.isFunctionBody = true;
|
||||
|
||||
// Use RAII to manage thunk execution flag
|
||||
ScopedThunkFlag _inThunk(inThunkExecution);
|
||||
|
||||
// Execute function body (inline like original - direct accept for performance)
|
||||
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) {
|
||||
return context.returnValue;
|
||||
}
|
||||
@ -178,10 +634,8 @@ Value Interpreter::evaluateCallExprInline(const std::shared_ptr<CallExpr>& expre
|
||||
return context.returnValue;
|
||||
});
|
||||
|
||||
// Store the thunk to keep it alive and return as Value (exactly like original)
|
||||
thunks.push_back(thunk);
|
||||
|
||||
// Automatic cleanup check
|
||||
thunkCreationCount++;
|
||||
if (thunkCreationCount >= CLEANUP_THRESHOLD) {
|
||||
cleanupUnusedThunks();
|
||||
@ -190,10 +644,16 @@ Value Interpreter::evaluateCallExprInline(const std::shared_ptr<CallExpr>& expre
|
||||
|
||||
return Value(thunk);
|
||||
} else {
|
||||
// Normal function call - create new environment (exactly like original)
|
||||
ScopedEnv _env(environment);
|
||||
environment = std::make_shared<Environment>(function->closure);
|
||||
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++) {
|
||||
environment->define(function->params[i], arguments[i]);
|
||||
@ -202,31 +662,13 @@ Value Interpreter::evaluateCallExprInline(const std::shared_ptr<CallExpr>& expre
|
||||
ExecutionContext context;
|
||||
context.isFunctionBody = true;
|
||||
|
||||
// Execute function body (exactly like original - direct accept for performance)
|
||||
for (const auto& stmt : function->body) {
|
||||
stmt->accept(executor.get(), &context); // Direct call like original
|
||||
if (context.hasReturn) {
|
||||
return context.returnValue;
|
||||
}
|
||||
stmt->accept(executor.get(), &context);
|
||||
if (context.hasThrow) { setPendingThrow(context.thrownValue, context.throwLine, context.throwColumn); return NONE_VALUE; }
|
||||
if (context.hasReturn) { 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
|
||||
}
|
||||
|
||||
@ -13,122 +13,19 @@
|
||||
#include <algorithm>
|
||||
|
||||
|
||||
bool RuntimeDiagnostics::isTruthy(Value object) {
|
||||
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::isTruthy(Value object) { return object.isTruthy(); }
|
||||
|
||||
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()) {
|
||||
// Numbers and booleans: 0 and false are equal, non-zero and true are equal
|
||||
if (b.asBoolean()) {
|
||||
return a.asNumber() != 0.0;
|
||||
} else {
|
||||
return a.asNumber() == 0.0;
|
||||
return b.asBoolean() ? (a.asNumber() != 0.0) : (a.asNumber() == 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
if (a.isBoolean() && b.isNumber()) {
|
||||
// Same as above, but reversed
|
||||
if (a.asBoolean()) {
|
||||
return b.asNumber() != 0.0;
|
||||
} else {
|
||||
return b.asNumber() == 0.0;
|
||||
return a.asBoolean() ? (b.asNumber() != 0.0) : (b.asNumber() == 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
// For all other type combinations, return false
|
||||
return false;
|
||||
return a.equals(b);
|
||||
}
|
||||
|
||||
std::string RuntimeDiagnostics::stringify(Value object) {
|
||||
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::stringify(Value object) { return object.toString(); }
|
||||
|
||||
std::string RuntimeDiagnostics::formatNumber(double 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 result = "[";
|
||||
std::string RuntimeDiagnostics::formatArray(const std::vector<Value>& arr) { return Value(arr).toString(); }
|
||||
|
||||
for (size_t i = 0; i < arr.size(); i++) {
|
||||
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;
|
||||
}
|
||||
std::string RuntimeDiagnostics::formatDict(const std::unordered_map<std::string, Value>& dict) { return Value(dict).toString(); }
|
||||
|
||||
void RuntimeDiagnostics::cleanupUnusedFunctions(std::vector<std::shared_ptr<BuiltinFunction>>& functions) {
|
||||
// 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,
|
||||
std::vector<std::shared_ptr<Function>>& functions,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
|
||||
#include "TypeWrapper.h"
|
||||
#include "TypeWrapper.h"
|
||||
#include <iostream>
|
||||
|
||||
|
||||
@ -4,5 +4,12 @@
|
||||
const Value NONE_VALUE = Value();
|
||||
const Value TRUE_VALUE = Value(true);
|
||||
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>");
|
||||
}
|
||||
|
||||
@ -76,104 +76,7 @@ void BobStdLib::addToEnvironment(std::shared_ptr<Environment> env, Interpreter&
|
||||
// Store the shared_ptr in the interpreter to keep it alive
|
||||
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
|
||||
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
|
||||
interpreter.addBuiltinFunction(assertFunc);
|
||||
|
||||
// Create a built-in time function (returns microseconds since Unix epoch)
|
||||
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);
|
||||
// time-related globals moved into builtin time module
|
||||
|
||||
// Create a built-in input function
|
||||
auto inputFunc = std::make_shared<BuiltinFunction>("input",
|
||||
@ -293,6 +176,8 @@ void BobStdLib::addToEnvironment(std::shared_ptr<Environment> env, Interpreter&
|
||||
typeName = "array";
|
||||
} else if (args[0].isDict()) {
|
||||
typeName = "dict";
|
||||
} else if (args[0].isModule()) {
|
||||
typeName = "module";
|
||||
} else {
|
||||
typeName = "unknown";
|
||||
}
|
||||
@ -405,444 +290,60 @@ void BobStdLib::addToEnvironment(std::shared_ptr<Environment> env, Interpreter&
|
||||
// Store the shared_ptr in the interpreter to keep it alive
|
||||
interpreter.addBuiltinFunction(toBooleanFunc);
|
||||
|
||||
// Create a built-in exit function to terminate the program
|
||||
auto exitFunc = std::make_shared<BuiltinFunction>("exit",
|
||||
[](std::vector<Value> args, int line, int column) -> Value {
|
||||
int exitCode = 0; // Default exit code
|
||||
// exit moved to sys module
|
||||
|
||||
if (args.size() > 0) {
|
||||
if (args[0].isNumber()) {
|
||||
exitCode = static_cast<int>(args[0].asNumber());
|
||||
}
|
||||
// If not a number, just use default exit code 0
|
||||
}
|
||||
// sleep moved into builtin time module
|
||||
|
||||
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
|
||||
interpreter.addBuiltinFunction(exitFunc);
|
||||
|
||||
// Create a built-in sleep function for animations and timing
|
||||
auto sleepFunc = std::make_shared<BuiltinFunction>("sleep",
|
||||
[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);
|
||||
auto functionsFunc = std::make_shared<BuiltinFunction>("functions",
|
||||
[](std::vector<Value> args, int, int) -> Value {
|
||||
if (args.size() != 1) return Value(std::vector<Value>{});
|
||||
Value obj = args[0];
|
||||
std::vector<Value> out;
|
||||
auto pushIfFn = [&out](const std::pair<const std::string, Value>& kv){
|
||||
if (kv.second.isFunction() || kv.second.isBuiltinFunction()) out.push_back(Value(kv.first));
|
||||
};
|
||||
if (obj.isModule()) {
|
||||
auto* mod = obj.asModule();
|
||||
if (mod && mod->exports) {
|
||||
for (const auto& kv : *mod->exports) pushIfFn(kv);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
return Value(out);
|
||||
});
|
||||
env->define("sleep", Value(sleepFunc));
|
||||
env->define("functions", Value(functionsFunc));
|
||||
interpreter.addBuiltinFunction(functionsFunc);
|
||||
|
||||
// Store the shared_ptr in the interpreter to keep it alive
|
||||
interpreter.addBuiltinFunction(sleepFunc);
|
||||
// random moved to rand module
|
||||
|
||||
// Create a built-in random function
|
||||
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()) + ".");
|
||||
}
|
||||
// (eval and evalFile moved to eval module)
|
||||
|
||||
// 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
|
||||
interpreter.addBuiltinFunction(randomFunc);
|
||||
// (file I/O moved to io module)
|
||||
|
||||
// Create a built-in eval function (like Python's eval)
|
||||
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);
|
||||
// memoryUsage moved to sys module
|
||||
|
||||
}
|
||||
@ -786,9 +786,9 @@ print("Comprehensive number tests: PASS");
|
||||
// TEST 29: TIME FUNCTION
|
||||
// ========================================
|
||||
print("\n--- Test 29: Time Function ---");
|
||||
|
||||
var start_time = time();
|
||||
var end_time = time();
|
||||
import time;
|
||||
var start_time = time.now();
|
||||
var end_time = time.now();
|
||||
var duration = end_time - start_time;
|
||||
assert(start_time > 0, "Start time should be positive");
|
||||
assert(end_time >= start_time, "End time should be >= start time");
|
||||
@ -2310,15 +2310,15 @@ print("\n--- Test 50: Arrays ---");
|
||||
|
||||
// Array creation
|
||||
var emptyArray = [];
|
||||
assert(len(emptyArray) == 0, "Empty array length");
|
||||
assert(emptyArray.len() == 0, "Empty array length");
|
||||
|
||||
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[4] == 5, "Array indexing - last element");
|
||||
|
||||
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[1] == "hello", "Mixed array - string");
|
||||
assert(mixedArray[2] == true, "Mixed array - boolean");
|
||||
@ -2333,35 +2333,35 @@ assert(mixedArray[1] == "world", "Array string assignment");
|
||||
|
||||
// Array operations
|
||||
var testArray = [1, 2, 3];
|
||||
push(testArray, 4);
|
||||
assert(len(testArray) == 4, "Array push - length");
|
||||
testArray.push(4);
|
||||
assert(testArray.len() == 4, "Array push - length");
|
||||
assert(testArray[3] == 4, "Array push - value");
|
||||
|
||||
var poppedValue = pop(testArray);
|
||||
var poppedValue = testArray.pop();
|
||||
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
|
||||
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");
|
||||
|
||||
// Array with function calls
|
||||
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[1]() == "hello", "Function array - second function");
|
||||
|
||||
// Array edge cases
|
||||
var singleElement = [42];
|
||||
assert(len(singleElement) == 1, "Single element array");
|
||||
assert(singleElement.len() == 1, "Single element array");
|
||||
assert(singleElement[0] == 42, "Single element access");
|
||||
|
||||
var largeArray = [];
|
||||
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");
|
||||
|
||||
print("Arrays: PASS");
|
||||
@ -2373,40 +2373,40 @@ print("\n--- Test 51: Array Built-in Functions ---");
|
||||
|
||||
// len() function
|
||||
var testLenArray = [1, 2, 3, 4, 5];
|
||||
assert(len(testLenArray) == 5, "len() with array");
|
||||
assert(testLenArray.len() == 5, "len() with array");
|
||||
|
||||
var emptyLenArray = [];
|
||||
assert(len(emptyLenArray) == 0, "len() with empty array");
|
||||
assert(emptyLenArray.len() == 0, "len() with empty array");
|
||||
|
||||
// len() with strings
|
||||
assert(len("hello") == 5, "len() with string");
|
||||
assert(len("") == 0, "len() with empty string");
|
||||
assert("hello".len() == 5, "len() with string");
|
||||
assert("".len() == 0, "len() with empty string");
|
||||
|
||||
// push() function
|
||||
var pushArray = [1, 2, 3];
|
||||
push(pushArray, 4);
|
||||
assert(len(pushArray) == 4, "push() - length check");
|
||||
pushArray.push(4);
|
||||
assert(pushArray.len() == 4, "push() - length check");
|
||||
assert(pushArray[3] == 4, "push() - value check");
|
||||
|
||||
push(pushArray, "hello");
|
||||
assert(len(pushArray) == 5, "push() - mixed types");
|
||||
pushArray.push("hello");
|
||||
assert(pushArray.len() == 5, "push() - mixed types");
|
||||
assert(pushArray[4] == "hello", "push() - string value");
|
||||
|
||||
// pop() function
|
||||
var popArray = [1, 2, 3, 4];
|
||||
var popped1 = pop(popArray);
|
||||
var popped1 = popArray.pop();
|
||||
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(len(popArray) == 2, "pop() - length after second pop");
|
||||
assert(popArray.len() == 2, "pop() - length after second pop");
|
||||
|
||||
// pop() edge cases
|
||||
var singlePopArray = [42];
|
||||
var singlePopped = pop(singlePopArray);
|
||||
var singlePopped = singlePopArray.pop();
|
||||
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");
|
||||
|
||||
@ -2416,15 +2416,17 @@ print("Array built-in functions: PASS");
|
||||
print("\n--- Test 52: New Built-in Functions ---");
|
||||
|
||||
// sleep() function
|
||||
var startTime = time();
|
||||
sleep(0.001); // Sleep for 1ms (much shorter for testing)
|
||||
var endTime = time();
|
||||
import time as T;
|
||||
var startTime = T.now();
|
||||
T.sleep(0.01); // Sleep briefly to ensure elapsed time
|
||||
var endTime = T.now();
|
||||
assert(endTime > startTime, "sleep() - time elapsed");
|
||||
|
||||
// random() function - test proper seeding
|
||||
var random1 = random();
|
||||
var random2 = random();
|
||||
var random3 = random();
|
||||
import rand as R;
|
||||
var random1 = R.random();
|
||||
var random2 = R.random();
|
||||
var random3 = R.random();
|
||||
assert(random1 >= 0 && random1 <= 1, "random() - range check 1");
|
||||
assert(random2 >= 0 && random2 <= 1, "random() - range check 2");
|
||||
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");
|
||||
|
||||
// Test random number generation in different ranges
|
||||
var randomRange1 = random() * 10;
|
||||
var randomRange2 = random() * 100;
|
||||
var randomRange1 = R.random() * 10;
|
||||
var randomRange2 = R.random() * 100;
|
||||
assert(randomRange1 >= 0 && randomRange1 <= 10, "random() - range 0-10");
|
||||
assert(randomRange2 >= 0 && randomRange2 <= 100, "random() - range 0-100");
|
||||
|
||||
// eval() function
|
||||
eval("var evalVar = 42;");
|
||||
// eval() function (now via eval module)
|
||||
import eval as E;
|
||||
E.eval("var evalVar = 42;");
|
||||
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
|
||||
|
||||
// Test eval with complex expressions
|
||||
eval("var complexVar = [1, 2, 3];");
|
||||
assert(len(complexVar) == 3, "eval() - complex expression");
|
||||
E.eval("var complexVar = [1, 2, 3];");
|
||||
assert(complexVar.len() == 3, "eval() - complex expression");
|
||||
|
||||
// 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");
|
||||
|
||||
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
|
||||
// ========================================
|
||||
@ -2479,9 +2495,9 @@ print("\n--- Test 52.6: Enhanced Dictionary Tests ---");
|
||||
|
||||
// Test dictionary with none values
|
||||
var dictWithNone = {"name": "Bob", "age": none, "city": "SF"};
|
||||
assert(has(dictWithNone, "name"), "Dictionary has() - existing key");
|
||||
assert(has(dictWithNone, "age"), "Dictionary has() - none value");
|
||||
assert(!has(dictWithNone, "phone"), "Dictionary has() - missing key");
|
||||
assert(dictWithNone.has("name"), "Dictionary has() - existing key");
|
||||
assert(dictWithNone.has("age"), "Dictionary has() - none value");
|
||||
assert(!dictWithNone.has("phone"), "Dictionary has() - missing key");
|
||||
|
||||
// Test setting keys to none
|
||||
dictWithNone["age"] = none;
|
||||
@ -2489,18 +2505,18 @@ assert(dictWithNone["age"] == none, "Dictionary - setting key to none");
|
||||
|
||||
// Test dictionary with all none values
|
||||
var allNoneDict = {"a": none, "b": none, "c": none};
|
||||
var allKeys = keys(allNoneDict);
|
||||
var allValues = values(allNoneDict);
|
||||
assert(len(allKeys) == 3, "Dictionary - all none keys count");
|
||||
assert(len(allValues) == 3, "Dictionary - all none values count");
|
||||
var allKeys = ({"a": none, "b": none, "c": none}).keys();
|
||||
var allValues = ({"a": none, "b": none, "c": none}).values();
|
||||
assert(allKeys.len() == 3, "Dictionary - all none keys count");
|
||||
assert(allValues.len() == 3, "Dictionary - all none values count");
|
||||
|
||||
// Test dictionary stress test
|
||||
var stressDict = {};
|
||||
for (var i = 0; i < 100; i = i + 1) {
|
||||
stressDict["key" + toString(i)] = i;
|
||||
}
|
||||
assert(len(keys(stressDict)) == 100, "Dictionary stress test - keys");
|
||||
assert(len(values(stressDict)) == 100, "Dictionary stress test - values");
|
||||
assert(stressDict.keys().len() == 100, "Dictionary stress test - keys");
|
||||
assert(stressDict.values().len() == 100, "Dictionary stress test - values");
|
||||
|
||||
print("Enhanced dictionary tests: PASS");
|
||||
|
||||
@ -2551,15 +2567,15 @@ assert(emptyDict.length == 0, "Empty dict length");
|
||||
assert(emptyDict.empty == true, "Empty dict empty");
|
||||
|
||||
// Test dict.keys and dict.values properties
|
||||
assert(len(dictWithProps.keys) == 3, "Dict keys property length");
|
||||
assert(len(dictWithProps.values) == 3, "Dict values property length");
|
||||
assert(len(emptyDict.keys) == 0, "Empty dict keys length");
|
||||
assert(len(emptyDict.values) == 0, "Empty dict values length");
|
||||
assert(dictWithProps.keys.len() == 3, "Dict keys property length");
|
||||
assert(dictWithProps.values.len() == 3, "Dict values property length");
|
||||
assert(emptyDict.keys.len() == 0, "Empty dict keys length");
|
||||
assert(emptyDict.values.len() == 0, "Empty dict values length");
|
||||
|
||||
// Test equivalence with old functions
|
||||
var testDict = {"x": 10, "y": 20};
|
||||
assert(len(testDict.keys) == len(keys(testDict)), "Dict keys equivalent to keys() function");
|
||||
assert(len(testDict.values) == len(values(testDict)), "Dict values equivalent to values() function");
|
||||
assert(testDict.keys.len() == testDict.keys.len(), "Dict keys property works");
|
||||
assert(testDict.values.len() == testDict.values.len(), "Dict values property works");
|
||||
print(" Dict builtin properties: PASS");
|
||||
|
||||
// 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) {
|
||||
var largeArray = [];
|
||||
for (var j = 0; j < 100; j = j + 1) {
|
||||
push(largeArray, "string" + toString(j));
|
||||
largeArray.push("string" + toString(j));
|
||||
}
|
||||
|
||||
var largeDict = {};
|
||||
@ -2703,7 +2719,7 @@ var errorArray = [1, 2, 3];
|
||||
// Test array bounds checking
|
||||
// Note: These would cause runtime errors in the actual implementation
|
||||
// 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
|
||||
assert(errorArray[0] == 1, "Array bounds - valid index 0");
|
||||
@ -2718,15 +2734,15 @@ print("\n--- Test 54: Array Performance ---");
|
||||
|
||||
// Test large array operations
|
||||
var perfArray = [];
|
||||
var startTime = time();
|
||||
import time as T; var startTime = T.now();
|
||||
|
||||
// Create large array
|
||||
for (var i = 0; i < 1000; i = i + 1) {
|
||||
push(perfArray, i);
|
||||
perfArray.push(i);
|
||||
}
|
||||
|
||||
var midTime = time();
|
||||
assert(len(perfArray) == 1000, "Array performance - creation");
|
||||
var midTime = T.now();
|
||||
assert(perfArray.len() == 1000, "Array performance - creation");
|
||||
|
||||
// Access elements
|
||||
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");
|
||||
}
|
||||
|
||||
var endTime = time();
|
||||
var endTime = T.now();
|
||||
assert(endTime > startTime, "Array performance - time check");
|
||||
|
||||
print("Array performance: PASS");
|
||||
@ -2751,7 +2767,7 @@ var funcArray2 = [
|
||||
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[1]() == 2, "Function array - call 2");
|
||||
assert(funcArray2[2]() == 3, "Function array - call 3");
|
||||
@ -2762,18 +2778,18 @@ func 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");
|
||||
|
||||
// Function that modifies array
|
||||
func modifyArray(arr) {
|
||||
push(arr, 999);
|
||||
arr.push(999);
|
||||
return arr;
|
||||
}
|
||||
|
||||
var modArray = [1, 2, 3];
|
||||
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");
|
||||
|
||||
print("Array with functions: PASS");
|
||||
@ -2785,11 +2801,11 @@ print("\n--- Test 56: Array Edge Cases and Advanced Features ---");
|
||||
|
||||
// Array with none values
|
||||
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 ==
|
||||
// We can test that the array contains the expected number of elements
|
||||
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") {
|
||||
noneCount = noneCount + 1;
|
||||
}
|
||||
@ -2798,7 +2814,7 @@ assert(noneCount == 2, "Array with none values - none count");
|
||||
|
||||
// Array with complex expressions
|
||||
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[1] == 6, "Complex array - multiplication");
|
||||
assert(complexArray[2] == 5, "Complex array - division");
|
||||
@ -2806,7 +2822,7 @@ assert(complexArray[3] == 3, "Complex array - subtraction");
|
||||
|
||||
// Array with string operations
|
||||
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[1] == "testtest", "String array - multiplication");
|
||||
assert(stringArray[2] == "abc", "String array - multiple concatenation");
|
||||
@ -2816,14 +2832,14 @@ func getValue() { return 42; }
|
||||
func getString() { return "hello"; }
|
||||
|
||||
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[1] == "hello", "Function result array - string function");
|
||||
assert(funcResultArray[2] == 3, "Function result array - expression");
|
||||
|
||||
// Array with ternary operators
|
||||
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[1] == "no", "Ternary array - false 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
|
||||
var exprArray = [1, 2, 3];
|
||||
var pushResult = push(exprArray, 4);
|
||||
assert(len(exprArray) == 4, "Array push in expression");
|
||||
var pushResult = exprArray.push(4);
|
||||
assert(exprArray.len() == 4, "Array push in expression");
|
||||
assert(exprArray[3] == 4, "Array push result");
|
||||
|
||||
var popResult = pop(exprArray);
|
||||
var popResult = exprArray.pop();
|
||||
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");
|
||||
|
||||
@ -2852,31 +2868,31 @@ print("\n--- Test 57: Array Stress Testing ---");
|
||||
|
||||
// Large array creation and manipulation
|
||||
var stressArray = [];
|
||||
var startTime = time();
|
||||
import time as T2; var startTime = T2.now();
|
||||
|
||||
// Create large array with mixed types
|
||||
for (var i = 0; i < 500; i = i + 1) {
|
||||
if (i % 3 == 0) {
|
||||
push(stressArray, i);
|
||||
stressArray.push(i);
|
||||
} else if (i % 3 == 1) {
|
||||
push(stressArray, "string" + toString(i));
|
||||
stressArray.push("string" + toString(i));
|
||||
} else {
|
||||
push(stressArray, i > 250);
|
||||
stressArray.push(i > 250);
|
||||
}
|
||||
}
|
||||
|
||||
var midTime = time();
|
||||
assert(len(stressArray) == 500, "Stress test - array creation");
|
||||
var midTime = T2.now();
|
||||
assert(stressArray.len() == 500, "Stress test - array creation");
|
||||
|
||||
// Access and modify elements
|
||||
for (var j = 0; j < 100; j = j + 1) {
|
||||
var index = j * 5;
|
||||
if (index < len(stressArray)) {
|
||||
if (index < stressArray.len()) {
|
||||
stressArray[index] = "modified" + toString(j);
|
||||
}
|
||||
}
|
||||
|
||||
var endTime = time();
|
||||
var endTime = T2.now();
|
||||
assert(endTime > startTime, "Stress test - time validation");
|
||||
|
||||
// Verify modifications
|
||||
@ -2992,7 +3008,8 @@ assert(toInt(-0.0) == 0, "toInt(-0.0) should be 0");
|
||||
|
||||
// Test with random numbers
|
||||
for(var i = 0; i < 5; i++) {
|
||||
var randomFloat = random() * 10;
|
||||
import rand as Rn;
|
||||
var randomFloat = Rn.random() * 10;
|
||||
var randomInt = toInt(randomFloat);
|
||||
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(" * 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("Test suite complete.");
|
||||
139
tests.bob
139
tests.bob
@ -1,53 +1,96 @@
|
||||
var a = [];
|
||||
// // var a = [];
|
||||
|
||||
for(var i = 0; i < 1000000; i++){
|
||||
print(i);
|
||||
// // for(var i = 0; i < 1000000; i++){
|
||||
// // print(i);
|
||||
|
||||
// Create nested structures with functions at different levels
|
||||
if (i % 4 == 0) {
|
||||
// Nested array with function
|
||||
push(a, [
|
||||
func(){print("Array nested func i=" + i); return i;},
|
||||
[func(){return "Deep array func " + i;}],
|
||||
i
|
||||
]);
|
||||
} else if (i % 4 == 1) {
|
||||
// Nested dict with function
|
||||
push(a, {
|
||||
"func": func(){print("Dict func i=" + i); return i;},
|
||||
"nested": {"deepFunc": func(){return "Deep dict func " + i;}},
|
||||
"value": i
|
||||
});
|
||||
} else if (i % 4 == 2) {
|
||||
// Mixed nested array/dict with functions
|
||||
push(a, [
|
||||
{"arrayInDict": func(){return "Mixed " + i;}},
|
||||
[func(){return "Array in array " + i;}, {"more": func(){return i;}}],
|
||||
func(){print("Top level in mixed i=" + i); return i;}
|
||||
]);
|
||||
} else {
|
||||
// Simple function (original test case)
|
||||
push(a, func(){print("Simple func i=" + i); return toString(i);});
|
||||
}
|
||||
}
|
||||
// // // Create nested structures with functions at different levels
|
||||
// // if (i % 4 == 0) {
|
||||
// // // Nested array with function
|
||||
// // push(a, [
|
||||
// // func(){print("Array nested func i=" + i); return i;},
|
||||
// // [func(){return "Deep array func " + i;}],
|
||||
// // i
|
||||
// // ]);
|
||||
// // } else if (i % 4 == 1) {
|
||||
// // // Nested dict with function
|
||||
// // push(a, {
|
||||
// // "func": func(){print("Dict func i=" + i); return i;},
|
||||
// // "nested": {"deepFunc": func(){return "Deep dict func " + i;}},
|
||||
// // "value": i
|
||||
// // });
|
||||
// // } else if (i % 4 == 2) {
|
||||
// // // Mixed nested array/dict with functions
|
||||
// // push(a, [
|
||||
// // {"arrayInDict": func(){return "Mixed " + i;}},
|
||||
// // [func(){return "Array in array " + i;}, {"more": func(){return i;}}],
|
||||
// // func(){print("Top level in mixed i=" + i); return i;}
|
||||
// // ]);
|
||||
// // } else {
|
||||
// // // Simple function (original test case)
|
||||
// // push(a, func(){print("Simple func i=" + i); return toString(i);});
|
||||
// // }
|
||||
// // }
|
||||
|
||||
print("Before: " + len(a));
|
||||
print("Memory usage: " + memoryUsage() + " MB");
|
||||
// // print("Before: " + len(a));
|
||||
// // print("Memory usage: " + memoryUsage() + " MB");
|
||||
|
||||
// Test different types of nested function calls
|
||||
a[3691](); // Simple function
|
||||
if (len(a[3692]) > 0) {
|
||||
a[3692][0](); // Nested array function
|
||||
}
|
||||
if (a[3693]["func"]) {
|
||||
a[3693]["func"](); // Nested dict function
|
||||
}
|
||||
print(a);
|
||||
//writeFile("array_contents.txt", toString(a));
|
||||
print("Array contents written to array_contents.txt");
|
||||
print("Memory before cleanup: " + memoryUsage() + " MB");
|
||||
input("Press any key to free memory");
|
||||
// // // Test different types of nested function calls
|
||||
// // a[3691](); // Simple function
|
||||
// // if (len(a[3692]) > 0) {
|
||||
// // a[3692][0](); // Nested array function
|
||||
// // }
|
||||
// // if (a[3693]["func"]) {
|
||||
// // a[3693]["func"](); // Nested dict function
|
||||
// // }
|
||||
// // //print(a);
|
||||
// // //writeFile("array_contents.txt", toString(a));
|
||||
// // print("Array contents written to array_contents.txt");
|
||||
// // print("Memory before cleanup: " + memoryUsage() + " MB");
|
||||
// // input("Press any key to free memory");
|
||||
|
||||
a = none;
|
||||
print("Memory after cleanup: " + memoryUsage() + " MB");
|
||||
input("waiting...");
|
||||
// // a = none;
|
||||
// // print("Memory after cleanup: " + memoryUsage() + " MB");
|
||||
// // 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
5
tests/debug_any_tag.bob
Normal file
@ -0,0 +1,5 @@
|
||||
extension any { func tag() { return "<" + toString(this) + ">"; } }
|
||||
var plain = { "x": 1 };
|
||||
print(plain.tag());
|
||||
|
||||
|
||||
7
tests/debug_inheritance.bob
Normal file
7
tests/debug_inheritance.bob
Normal 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());
|
||||
|
||||
|
||||
3
tests/debug_init_params.bob
Normal file
3
tests/debug_init_params.bob
Normal file
@ -0,0 +1,3 @@
|
||||
class P { func init(f,l) { print(f + ":" + l); } }
|
||||
var p = P("A","B");
|
||||
|
||||
8
tests/debug_init_person.bob
Normal file
8
tests/debug_init_person.bob
Normal 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);
|
||||
|
||||
|
||||
6
tests/debug_mixed_super.bob
Normal file
6
tests/debug_mixed_super.bob
Normal 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());
|
||||
|
||||
|
||||
12
tests/debug_super_chain.bob
Normal file
12
tests/debug_super_chain.bob
Normal 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
10
tests/debug_super_min.bob
Normal 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());
|
||||
|
||||
|
||||
5
tests/import_user_of_mod_hello.bob
Normal file
5
tests/import_user_of_mod_hello.bob
Normal file
@ -0,0 +1,5 @@
|
||||
// uses mod_hello without importing it here; relies on prior import in same interpreter
|
||||
var ok1 = (mod_hello.greet("Z") == "hi Z");
|
||||
assert(ok1, "imported module binding visible across eval files in same interpreter");
|
||||
print("import user: PASS");
|
||||
|
||||
3
tests/mod_hello.bob
Normal file
3
tests/mod_hello.bob
Normal file
@ -0,0 +1,3 @@
|
||||
var X = 5;
|
||||
func greet(name) { return "hi " + name; }
|
||||
|
||||
11
tests/test_base64.bob
Normal file
11
tests/test_base64.bob
Normal 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");
|
||||
|
||||
|
||||
24
tests/test_builtin_methods_style.bob
Normal file
24
tests/test_builtin_methods_style.bob
Normal 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()");
|
||||
77
tests/test_class_and_extension.bob
Normal file
77
tests/test_class_and_extension.bob
Normal 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).");
|
||||
|
||||
|
||||
20
tests/test_class_basic.bob
Normal file
20
tests/test_class_basic.bob
Normal 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");
|
||||
|
||||
|
||||
52
tests/test_class_edge_cases.bob
Normal file
52
tests/test_class_edge_cases.bob
Normal 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");
|
||||
|
||||
|
||||
21
tests/test_class_extension_user.bob
Normal file
21
tests/test_class_extension_user.bob
Normal 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");
|
||||
|
||||
|
||||
40
tests/test_class_inheritance.bob
Normal file
40
tests/test_class_inheritance.bob
Normal 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
15
tests/test_class_init.bob
Normal 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");
|
||||
|
||||
|
||||
85
tests/test_class_stress.bob
Normal file
85
tests/test_class_stress.bob
Normal 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");
|
||||
|
||||
|
||||
21
tests/test_class_super.bob
Normal file
21
tests/test_class_super.bob
Normal 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");
|
||||
|
||||
|
||||
15
tests/test_class_with_this.bob
Normal file
15
tests/test_class_with_this.bob
Normal 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");
|
||||
|
||||
|
||||
94
tests/test_classes_comprehensive.bob
Normal file
94
tests/test_classes_comprehensive.bob
Normal 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");
|
||||
|
||||
|
||||
92
tests/test_classes_extensive.bob
Normal file
92
tests/test_classes_extensive.bob
Normal 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");
|
||||
|
||||
|
||||
35
tests/test_extension_methods.bob
Normal file
35
tests/test_extension_methods.bob
Normal 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");
|
||||
|
||||
|
||||
10
tests/test_extension_syntax.bob
Normal file
10
tests/test_extension_syntax.bob
Normal 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
5
tests/test_hash.bob
Normal file
@ -0,0 +1,5 @@
|
||||
print("\n--- Test: hash module ---");
|
||||
// Placeholder: implement when hash module is added
|
||||
print("hash: SKIPPED");
|
||||
|
||||
|
||||
48
tests/test_imports_basic.bob
Normal file
48
tests/test_imports_basic.bob
Normal 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");
|
||||
|
||||
21
tests/test_imports_builtin.bob
Normal file
21
tests/test_imports_builtin.bob
Normal 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
Loading…
Reference in New Issue
Block a user