Property Expression, Fixed memory leaks
This commit is contained in:
parent
87d56bbb13
commit
f70c6abd77
@ -73,6 +73,18 @@ target_include_directories(bob PRIVATE
|
||||
# Apply compiler options
|
||||
target_compile_options(bob PRIVATE ${BOB_COMPILE_OPTIONS})
|
||||
|
||||
# Enable Link Time Optimization (LTO) for Release builds
|
||||
if(CMAKE_BUILD_TYPE STREQUAL "Release")
|
||||
include(CheckIPOSupported)
|
||||
check_ipo_supported(RESULT ipo_supported OUTPUT ipo_error)
|
||||
if(ipo_supported)
|
||||
message(STATUS "IPO/LTO enabled")
|
||||
set_property(TARGET bob PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE)
|
||||
else()
|
||||
message(WARNING "IPO/LTO not supported: ${ipo_error}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Platform-specific settings
|
||||
if(WIN32)
|
||||
# Windows-specific settings
|
||||
@ -142,4 +154,7 @@ message(STATUS " Build Type: ${CMAKE_BUILD_TYPE}")
|
||||
message(STATUS " Compiler: ${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}")
|
||||
message(STATUS " C++ Standard: ${CMAKE_CXX_STANDARD}")
|
||||
message(STATUS " Install Prefix: ${CMAKE_INSTALL_PREFIX}")
|
||||
message(STATUS " Source Files Found: ${BOB_ALL_SOURCES}")
|
||||
message(STATUS " Runtime Sources: ${BOB_RUNTIME_SOURCES}")
|
||||
message(STATUS " Parsing Sources: ${BOB_PARSING_SOURCES}")
|
||||
message(STATUS " Stdlib Sources: ${BOB_STDLIB_SOURCES}")
|
||||
message(STATUS " CLI Sources: ${BOB_CLI_SOURCES}")
|
||||
@ -52,15 +52,40 @@ var nested = [[1, 2], [3, 4]];
|
||||
// Access and modify
|
||||
print(numbers[0]); // 1
|
||||
numbers[1] = 99;
|
||||
|
||||
// Array properties (read-only)
|
||||
print(numbers.length); // 3
|
||||
print(numbers.first); // 1
|
||||
print(numbers.last); // 3
|
||||
print(numbers.empty); // false
|
||||
|
||||
var empty = [];
|
||||
print(empty.length); // 0
|
||||
print(empty.first); // none
|
||||
print(empty.empty); // true
|
||||
```
|
||||
|
||||
### Dictionaries
|
||||
```go
|
||||
var person = {"name": "Alice", "age": 30};
|
||||
|
||||
// Access and modify
|
||||
// Access and modify (bracket notation)
|
||||
print(person["name"]); // Alice
|
||||
person["city"] = "NYC";
|
||||
|
||||
// Access and modify (dot notation - cleaner syntax)
|
||||
print(person.name); // Alice
|
||||
person.city = "NYC";
|
||||
person.age = 31;
|
||||
|
||||
// Both notations are equivalent
|
||||
assert(person.name == person["name"]);
|
||||
|
||||
// Dictionary properties (built-in)
|
||||
print(person.length); // 3 (number of key-value pairs)
|
||||
print(person.empty); // false
|
||||
print(person.keys); // ["name", "age", "city"]
|
||||
print(person.values); // ["Alice", 31, "NYC"]
|
||||
```
|
||||
|
||||
## Variables
|
||||
@ -229,7 +254,7 @@ assert(condition, "message"); // Testing
|
||||
time(); // Current time in microseconds
|
||||
sleep(1.5); // Sleep for 1.5 seconds
|
||||
random(); // Random number 0-1
|
||||
eval("1 + 2"); // Evaluate string as code
|
||||
eval("print('Hello');"); // Execute string as code
|
||||
exit(0); // Exit program
|
||||
```
|
||||
|
||||
@ -342,7 +367,14 @@ for (var i = 0; i < len(lines); i = i + 1) {
|
||||
}
|
||||
}
|
||||
|
||||
writeFile("output.txt", join(processed, "\n"));
|
||||
var output = "";
|
||||
for (var i = 0; i < len(processed); i = i + 1) {
|
||||
output = output + processed[i];
|
||||
if (i < len(processed) - 1) {
|
||||
output = output + "\n";
|
||||
}
|
||||
}
|
||||
writeFile("output.txt", output);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@ -47,8 +47,8 @@ Bob is a mature, working programming language with a modern architecture and com
|
||||
#### **Data Structures (Complete)**
|
||||
- **Arrays**: Full support with indexing, assignment, nested arrays
|
||||
- **Dictionaries**: Full support with key-value pairs, nested dictionaries
|
||||
- **Array Operations**: `len()`, `push()`, `pop()`, indexing, assignment
|
||||
- **Dictionary Operations**: `keys()`, `values()`, `has()`, indexing, assignment
|
||||
- **Array Operations**: `len()`, `push()`, `pop()`, indexing, assignment, properties (`length`, `first`, `last`, `empty`)
|
||||
- **Dictionary Operations**: `keys()`, `values()`, `has()`, indexing, assignment, dot notation (`obj.prop`)
|
||||
- **Mixed Types**: Arrays and dictionaries can hold any value types
|
||||
|
||||
#### **Standard Library (Complete)**
|
||||
@ -169,7 +169,7 @@ var person = {
|
||||
- Add object literal syntax
|
||||
- Implement `this` binding
|
||||
- Support method calls
|
||||
- Add property access/assignment
|
||||
- ✅ Add property access/assignment (completed - dot notation for dictionaries and arrays)
|
||||
|
||||
#### **Classes (Optional)**
|
||||
```bob
|
||||
|
||||
56
examples/demo_features.bob
Normal file
56
examples/demo_features.bob
Normal file
@ -0,0 +1,56 @@
|
||||
// ===========================================================
|
||||
// Bob feature-tour demo
|
||||
// – dot-notation for dictionaries & arrays
|
||||
// – property assignment (incl. nested)
|
||||
// – built-in properties: dict.keys / dict.values
|
||||
// arr.length / arr.first / arr.last / arr.empty
|
||||
// – tail-call-optimised (TCO) recursion
|
||||
// ===========================================================
|
||||
|
||||
// ---------- 1. Dictionary property access / assignment ----------
|
||||
var person = { "name": "Alice", "age": 30 };
|
||||
print(person.name);
|
||||
print(person.age);
|
||||
|
||||
// add a brand-new user property
|
||||
person.country = "Canada";
|
||||
print(person.country);
|
||||
|
||||
// nested property assignment
|
||||
var team = { "lead": { "name": "Bob", "age": 40 } };
|
||||
team.lead.age = 41;
|
||||
print(team.lead.age);
|
||||
|
||||
// built-in dictionary properties
|
||||
print("dict length = " + team.length);
|
||||
print(team.keys);
|
||||
print(team.values[0].name);
|
||||
|
||||
// ---------- 2. Array dot-properties ----------
|
||||
var nums = [ 10, 20, 30, 40 ];
|
||||
print("len = " + nums.length);
|
||||
print("first = " + nums.first);
|
||||
print("last = " + nums.last);
|
||||
print("empty? " + nums.empty);
|
||||
|
||||
// dot-properties are read-only; assignment still via index
|
||||
nums[0] = 99;
|
||||
print(nums.first);
|
||||
|
||||
// ---------- 3. Tail-call-optimised recursion ----------
|
||||
func fibTail(n, a, b) {
|
||||
return n <= 0 ? a : fibTail(n - 1, b, a + b);
|
||||
}
|
||||
|
||||
print("Fast TCO fib(90) = " + fibTail(90, 0, 1));
|
||||
print("Fast TCO fib(5000) = " + fibTail(5000, 0, 1));
|
||||
|
||||
// ---------- 4. Compare non-TCO version (stack grows!) ----------
|
||||
func fibPlain(n) {
|
||||
return n <= 1 ? n : fibPlain(n - 1) + fibPlain(n - 2);
|
||||
}
|
||||
|
||||
print("Slow recursive fib(35) = " + fibPlain(35));
|
||||
|
||||
// ---------- 5. Summary ----------
|
||||
print("All new features demonstrated!");
|
||||
95
leakTests/README.md
Normal file
95
leakTests/README.md
Normal file
@ -0,0 +1,95 @@
|
||||
# Bob Memory Leak Test Suite
|
||||
|
||||
This directory contains comprehensive memory leak tests for the Bob programming language. Each test file focuses on different scenarios that could potentially cause memory leaks.
|
||||
|
||||
## Test Files
|
||||
|
||||
### `leaktest_functions.bob`
|
||||
Tests function-related memory scenarios:
|
||||
- Recursive function closures
|
||||
- Function factories (functions returning functions)
|
||||
- Deep function nesting
|
||||
- Circular function references
|
||||
- Expected behavior: Memory should be freed when functions are cleared
|
||||
|
||||
### `leaktest_collections.bob`
|
||||
Tests collection (arrays/dictionaries) memory scenarios:
|
||||
- Large nested arrays
|
||||
- Large nested dictionaries
|
||||
- Mixed array/dict structures
|
||||
- Self-referencing structures
|
||||
- Large string collections
|
||||
- Expected behavior: Collections should be properly freed when reassigned
|
||||
|
||||
### `leaktest_mixed.bob`
|
||||
Tests mixed type scenarios and edge cases:
|
||||
- Functions capturing collections
|
||||
- Collections containing functions and mixed types
|
||||
- Dynamic property assignment patterns
|
||||
- Type reassignment chains
|
||||
- Rapid allocation/deallocation cycles
|
||||
- Expected behavior: Memory should be freed regardless of type mixing
|
||||
|
||||
### `leaktest_loops.bob`
|
||||
Tests memory behavior in loops and repetitive operations:
|
||||
- Nested loop allocation
|
||||
- While loop accumulation
|
||||
- Variable reassignment in loops
|
||||
- Do-while function creation
|
||||
- Complex loop control flow
|
||||
- Memory churn tests
|
||||
- Expected behavior: Loop-created objects should be freed when variables are reassigned
|
||||
|
||||
### `leaktest_builtin.bob`
|
||||
Tests builtin function and stdlib memory behavior:
|
||||
- Heavy string operations
|
||||
- Type conversion stress tests
|
||||
- Array/Dict builtin operations
|
||||
- Eval function stress tests
|
||||
- File I/O operations
|
||||
- Random number generation
|
||||
- Expected behavior: Builtin operations should not leak memory
|
||||
|
||||
## How to Run Tests
|
||||
|
||||
Run each test individually and monitor memory usage:
|
||||
|
||||
```bash
|
||||
# Monitor memory before, during, and after each test
|
||||
./build-ninja/bin/bob leakTests/leaktest_functions.bob
|
||||
./build-ninja/bin/bob leakTests/leaktest_collections.bob
|
||||
./build-ninja/bin/bob leakTests/leaktest_mixed.bob
|
||||
./build-ninja/bin/bob leakTests/leaktest_loops.bob
|
||||
./build-ninja/bin/bob leakTests/leaktest_builtin.bob
|
||||
```
|
||||
|
||||
## Expected Behavior
|
||||
|
||||
After the memory leak fixes:
|
||||
1. **Memory should increase** during object creation phases
|
||||
2. **Memory should decrease** significantly when objects are cleared (set to `none`, `[]`, different types, etc.)
|
||||
3. **Memory should return close to baseline** after each test section
|
||||
4. **No gradual memory increase** across multiple test cycles
|
||||
|
||||
## Memory Monitoring
|
||||
|
||||
Use system tools to monitor memory:
|
||||
- **macOS**: Activity Monitor or `top -pid $(pgrep bob)`
|
||||
- **Linux**: `top`, `htop`, or `ps aux | grep bob`
|
||||
- **Windows**: Task Manager or Process Monitor
|
||||
|
||||
Look for:
|
||||
- Memory spikes during creation phases ✅ Expected
|
||||
- Memory drops after "cleared" messages ✅ Expected
|
||||
- Memory staying high after clearing ❌ Potential leak
|
||||
- Gradual increase across test cycles ❌ Potential leak
|
||||
|
||||
## Test Scenarios Covered
|
||||
|
||||
- **Object Types**: Functions, Arrays, Dictionaries, Strings, Numbers, Booleans
|
||||
- **Memory Patterns**: Allocation, Deallocation, Reassignment, Type Changes
|
||||
- **Edge Cases**: Circular references, Deep nesting, Self-references, Mixed types
|
||||
- **Operations**: Loops, Builtin functions, File I/O, Type conversions
|
||||
- **Cleanup Triggers**: Setting to `none`, `[]`, `{}`, different types, string values
|
||||
|
||||
This comprehensive test suite should help identify any remaining memory leak scenarios in the Bob interpreter.
|
||||
172
leakTests/leaktest_builtin.bob
Normal file
172
leakTests/leaktest_builtin.bob
Normal file
@ -0,0 +1,172 @@
|
||||
// Memory leak test: Builtin function and stdlib scenarios
|
||||
// Test memory behavior with builtin functions and standard library
|
||||
|
||||
print("=== Builtin Function Memory Leak Tests ===");
|
||||
print("Initial memory: " + memoryUsage() + " MB");
|
||||
|
||||
// Test 1: Heavy string operations
|
||||
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, {
|
||||
"original": str,
|
||||
"upper": str, // Bob doesn't have toUpper, but test string storage
|
||||
"length": len(str),
|
||||
"type": type(str)
|
||||
});
|
||||
}
|
||||
print("Created " + len(stringData) + " string operation results");
|
||||
print("Memory: " + memoryUsage() + " MB");
|
||||
input("Press Enter to clear string data...");
|
||||
stringData = none;
|
||||
print("Memory after clear: " + memoryUsage() + " MB");
|
||||
input("Cleared. Check memory usage...");
|
||||
|
||||
// Test 2: Type conversion stress test
|
||||
print("Test 2: Type conversion stress");
|
||||
var conversions = [];
|
||||
for (var i = 0; i < 200000; i++) {
|
||||
var num = i * 1.5;
|
||||
var str = toString(num);
|
||||
var backToNum = toNumber(str);
|
||||
var intVal = toInt(num);
|
||||
var boolVal = toBoolean(i % 2);
|
||||
|
||||
push(conversions, [
|
||||
num, str, backToNum, intVal, boolVal,
|
||||
type(num), type(str), type(backToNum), type(intVal), type(boolVal)
|
||||
]);
|
||||
}
|
||||
print("Created " + len(conversions) + " type conversion results");
|
||||
print("Memory: " + memoryUsage() + " MB");
|
||||
input("Press Enter to clear conversions...");
|
||||
conversions = [];
|
||||
print("Memory after clear: " + memoryUsage() + " MB");
|
||||
input("Cleared. Check memory usage...");
|
||||
|
||||
// Test 3: Array/Dict builtin operations
|
||||
print("Test 3: Collection builtin operations");
|
||||
var collections = [];
|
||||
for (var i = 0; i < 50000; i++) {
|
||||
var arr = [i, i+1, i+2];
|
||||
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 dictKeys = keys(dict);
|
||||
var dictValues = values(dict);
|
||||
var hasA = has(dict, "a");
|
||||
|
||||
push(collections, {
|
||||
"array": arr,
|
||||
"dict": dict,
|
||||
"arrLen": arrLen,
|
||||
"popped": popped,
|
||||
"keys": dictKeys,
|
||||
"values": dictValues,
|
||||
"hasA": hasA
|
||||
});
|
||||
}
|
||||
print("Created " + len(collections) + " collection operation results");
|
||||
print("Memory: " + memoryUsage() + " MB");
|
||||
input("Press Enter to clear collections...");
|
||||
collections = "cleared";
|
||||
print("Memory after clear: " + memoryUsage() + " MB");
|
||||
input("Cleared. Check memory usage...");
|
||||
|
||||
// Test 4: Eval function stress test
|
||||
print("Test 4: Eval function stress");
|
||||
var evalResults = [];
|
||||
for (var i = 0; i < 10000; i++) {
|
||||
var expression = toString(i) + " * 2 + 1;";
|
||||
var result = eval(expression);
|
||||
|
||||
var funcExpr = "func() { return " + toString(i) + "; };";
|
||||
var evalFunc = eval(funcExpr);
|
||||
|
||||
push(evalResults, {
|
||||
"expr": expression,
|
||||
"result": result,
|
||||
"func": evalFunc,
|
||||
"funcResult": evalFunc()
|
||||
});
|
||||
}
|
||||
print("Created " + len(evalResults) + " eval results");
|
||||
print("Memory: " + memoryUsage() + " MB");
|
||||
input("Press Enter to clear eval results...");
|
||||
evalResults = none;
|
||||
print("Memory after clear: " + memoryUsage() + " MB");
|
||||
input("Cleared. Check memory usage...");
|
||||
|
||||
// Test 5: File I/O operations (if supported)
|
||||
print("Test 5: File I/O operations");
|
||||
var fileData = [];
|
||||
for (var i = 0; i < 1000; i++) {
|
||||
var filename = "temp_" + toString(i) + ".txt";
|
||||
var content = "This is test data for file " + toString(i) + "\n";
|
||||
content = content + "Line 2: " + toString(i * 2) + "\n";
|
||||
content = content + "Line 3: " + toString(i * 3) + "\n";
|
||||
|
||||
// Write file
|
||||
writeFile(filename, content);
|
||||
|
||||
// Check if exists
|
||||
var exists = fileExists(filename);
|
||||
|
||||
// Read back
|
||||
var readContent = readFile(filename);
|
||||
var lines = readLines(filename);
|
||||
|
||||
push(fileData, {
|
||||
"filename": filename,
|
||||
"exists": exists,
|
||||
"content": readContent,
|
||||
"lines": lines,
|
||||
"lineCount": len(lines)
|
||||
});
|
||||
}
|
||||
print("Created " + len(fileData) + " file operation results");
|
||||
print("Memory: " + memoryUsage() + " MB");
|
||||
input("Press Enter to clear file data...");
|
||||
fileData = [];
|
||||
print("Memory after clear: " + memoryUsage() + " MB");
|
||||
|
||||
// Clean up test files
|
||||
print("Cleaning up test files...");
|
||||
for (var i = 0; i < 1000; i++) {
|
||||
var filename = "temp_" + toString(i) + ".txt";
|
||||
if (fileExists(filename)) {
|
||||
// Bob doesn't have deleteFile, but we created them
|
||||
print("Note: Test file " + filename + " still exists");
|
||||
if (i > 10) break; // Don't spam too many messages
|
||||
}
|
||||
}
|
||||
input("File data cleared. Check memory usage...");
|
||||
|
||||
// Test 6: Random number generation
|
||||
print("Test 6: Random number stress");
|
||||
var randomData = [];
|
||||
for (var i = 0; i < 200000; i++) {
|
||||
var rand1 = random();
|
||||
var rand2 = random();
|
||||
var sum = rand1 + rand2;
|
||||
|
||||
push(randomData, {
|
||||
"rand1": rand1,
|
||||
"rand2": rand2,
|
||||
"sum": sum,
|
||||
"product": rand1 * rand2
|
||||
});
|
||||
}
|
||||
print("Created " + len(randomData) + " random number results");
|
||||
print("Memory: " + memoryUsage() + " MB");
|
||||
input("Press Enter to clear random data...");
|
||||
randomData = none;
|
||||
print("Memory after clear: " + memoryUsage() + " MB");
|
||||
input("Cleared. Check memory usage...");
|
||||
|
||||
print("=== Builtin Function Tests Complete ===");
|
||||
110
leakTests/leaktest_collections.bob
Normal file
110
leakTests/leaktest_collections.bob
Normal file
@ -0,0 +1,110 @@
|
||||
// Memory leak test: Collection-related scenarios
|
||||
// Test arrays, dictionaries, and nested structures
|
||||
|
||||
print("=== Collection Memory Leak Tests ===");
|
||||
print("Initial memory: " + memoryUsage() + " MB");
|
||||
|
||||
// Test 1: Large nested arrays
|
||||
print("Test 1: Large nested arrays");
|
||||
var nestedArrays = [];
|
||||
for (var i = 0; i < 50000; i++) {
|
||||
push(nestedArrays, [
|
||||
[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("Memory: " + memoryUsage() + " MB");
|
||||
input("Press Enter to clear nested arrays...");
|
||||
nestedArrays = none;
|
||||
print("Memory after clear: " + memoryUsage() + " MB");
|
||||
input("Cleared. Check memory usage...");
|
||||
|
||||
// Test 2: Large nested dictionaries
|
||||
print("Test 2: Large nested dictionaries");
|
||||
var nestedDicts = [];
|
||||
for (var i = 0; i < 50000; i++) {
|
||||
push(nestedDicts, {
|
||||
"id": i,
|
||||
"data": {
|
||||
"value": i * 2,
|
||||
"nested": {
|
||||
"deep": {
|
||||
"deeper": i * 3,
|
||||
"info": "test" + i
|
||||
}
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"created": i,
|
||||
"tags": ["tag" + i, "tag" + (i+1)]
|
||||
}
|
||||
});
|
||||
}
|
||||
print("Created " + len(nestedDicts) + " nested dictionary structures");
|
||||
print("Memory: " + memoryUsage() + " MB");
|
||||
input("Press Enter to clear nested dicts...");
|
||||
nestedDicts = [];
|
||||
print("Memory after clear: " + memoryUsage() + " MB");
|
||||
input("Cleared. Check memory usage...");
|
||||
|
||||
// Test 3: Mixed array/dict structures
|
||||
print("Test 3: Mixed array/dict structures");
|
||||
var mixedStructures = [];
|
||||
for (var i = 0; i < 30000; i++) {
|
||||
push(mixedStructures, [
|
||||
{"arrays": [[i, i+1], [i+2, i+3]]},
|
||||
[{"dicts": {"a": i, "b": i+1}}, {"more": [i, i+1]}],
|
||||
{
|
||||
"complex": [
|
||||
{"nested": [i, {"deep": i*2}]},
|
||||
[{"very": {"deep": [i, i+1, {"final": i*3}]}}]
|
||||
]
|
||||
}
|
||||
]);
|
||||
}
|
||||
print("Created " + len(mixedStructures) + " mixed structures");
|
||||
print("Memory: " + memoryUsage() + " MB");
|
||||
input("Press Enter to clear mixed structures...");
|
||||
mixedStructures = "cleared";
|
||||
print("Memory after clear: " + memoryUsage() + " MB");
|
||||
input("Cleared. Check memory usage...");
|
||||
|
||||
// Test 4: Self-referencing structures (potential cycles)
|
||||
print("Test 4: Self-referencing structures");
|
||||
var selfRef = [];
|
||||
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);
|
||||
}
|
||||
print("Created " + len(selfRef) + " self-referencing structures");
|
||||
print("Memory: " + memoryUsage() + " MB");
|
||||
input("Press Enter to clear self-ref structures...");
|
||||
selfRef = 123;
|
||||
print("Memory after clear: " + memoryUsage() + " MB");
|
||||
input("Cleared. Check memory usage...");
|
||||
|
||||
// Test 5: Large string collections
|
||||
print("Test 5: Large string collections");
|
||||
var stringCollections = [];
|
||||
for (var i = 0; i < 100000; i++) {
|
||||
var longString = "";
|
||||
for (var j = 0; j < 100; j++) {
|
||||
longString = longString + "data" + i + "_" + j + " ";
|
||||
}
|
||||
push(stringCollections, {
|
||||
"content": longString,
|
||||
"words": [longString, longString + "_copy", longString + "_backup"]
|
||||
});
|
||||
}
|
||||
print("Created " + len(stringCollections) + " string collections");
|
||||
print("Memory: " + memoryUsage() + " MB");
|
||||
input("Press Enter to clear string collections...");
|
||||
stringCollections = none;
|
||||
print("Memory after clear: " + memoryUsage() + " MB");
|
||||
input("Cleared. Check memory usage...");
|
||||
|
||||
print("=== Collection Tests Complete ===");
|
||||
89
leakTests/leaktest_functions.bob
Normal file
89
leakTests/leaktest_functions.bob
Normal file
@ -0,0 +1,89 @@
|
||||
// Memory leak test: Function-related scenarios
|
||||
// Test various function patterns that could cause memory leaks
|
||||
|
||||
print("=== Function Memory Leak Tests ===");
|
||||
print("Initial memory: " + memoryUsage() + " MB");
|
||||
|
||||
// Test 1: Recursive function closures
|
||||
print("Test 1: Recursive function closures");
|
||||
var recursiveFuncs = [];
|
||||
for (var i = 0; i < 100000; i++) {
|
||||
push(recursiveFuncs, func() {
|
||||
var capturedI = i;
|
||||
return func() {
|
||||
if (capturedI > 0) {
|
||||
return capturedI * capturedI;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
});
|
||||
}
|
||||
print("Created " + len(recursiveFuncs) + " recursive closure functions");
|
||||
print("Memory after creation: " + memoryUsage() + " MB");
|
||||
input("Press Enter to clear recursive functions...");
|
||||
recursiveFuncs = none;
|
||||
print("Memory after cleanup: " + memoryUsage() + " MB");
|
||||
input("Cleared. Check memory usage...");
|
||||
|
||||
// Test 2: Functions returning functions (factory pattern)
|
||||
print("Test 2: Function factories");
|
||||
var factories = [];
|
||||
for (var i = 0; i < 100000; i++) {
|
||||
push(factories, func() {
|
||||
var multiplier = i;
|
||||
return func(x) {
|
||||
return x * multiplier;
|
||||
};
|
||||
});
|
||||
}
|
||||
print("Created " + len(factories) + " function factories");
|
||||
print("Memory: " + memoryUsage() + " MB");
|
||||
input("Press Enter to clear factories...");
|
||||
factories = [];
|
||||
print("Memory after clear: " + memoryUsage() + " MB");
|
||||
input("Cleared. Check memory usage...");
|
||||
|
||||
// Test 3: Deep function nesting
|
||||
print("Test 3: Deep function nesting");
|
||||
var deepNested = [];
|
||||
for (var i = 0; i < 50000; i++) {
|
||||
push(deepNested, func() {
|
||||
return func() {
|
||||
return func() {
|
||||
return func() {
|
||||
return func() {
|
||||
return i * 42;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
});
|
||||
}
|
||||
print("Created " + len(deepNested) + " deeply nested functions");
|
||||
print("Memory: " + memoryUsage() + " MB");
|
||||
input("Press Enter to clear deep nested...");
|
||||
deepNested = "test";
|
||||
print("Memory after clear: " + memoryUsage() + " MB");
|
||||
input("Cleared. Check memory usage...");
|
||||
|
||||
// Test 4: Circular function references
|
||||
print("Test 4: Circular function references");
|
||||
var circularFuncs = [];
|
||||
for (var i = 0; i < 1000000; i++) {
|
||||
var funcA = func() {
|
||||
return "A" + 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; }]);
|
||||
}
|
||||
print("Created " + len(circularFuncs) + " circular function structures");
|
||||
print("Memory: " + memoryUsage() + " MB");
|
||||
input("Press Enter to clear circular functions...");
|
||||
circularFuncs = 42;
|
||||
print("Memory after clear: " + memoryUsage() + " MB");
|
||||
input("Cleared. Check memory usage...");
|
||||
|
||||
print("=== Function Tests Complete ===");
|
||||
145
leakTests/leaktest_loops.bob
Normal file
145
leakTests/leaktest_loops.bob
Normal file
@ -0,0 +1,145 @@
|
||||
// Memory leak test: Loop and repetitive operation scenarios
|
||||
// Test memory behavior in various loop patterns
|
||||
|
||||
print("=== Loop Memory Leak Tests ===");
|
||||
print("Initial memory: " + memoryUsage() + " MB");
|
||||
|
||||
// Test 1: Nested loop memory allocation
|
||||
print("Test 1: Nested loop allocation");
|
||||
var nestedData = [];
|
||||
for (var i = 0; i < 1000; i++) {
|
||||
var row = [];
|
||||
for (var j = 0; j < 1000; j++) {
|
||||
push(row, {
|
||||
"i": i,
|
||||
"j": j,
|
||||
"func": func() { return i * j; },
|
||||
"data": [i, j, i+j]
|
||||
});
|
||||
}
|
||||
push(nestedData, row);
|
||||
}
|
||||
print("Created " + len(nestedData) + "x" + len(nestedData[0]) + " nested structure");
|
||||
print("Memory: " + memoryUsage() + " MB");
|
||||
input("Press Enter to clear nested data...");
|
||||
nestedData = none;
|
||||
print("Memory after clear: " + memoryUsage() + " MB");
|
||||
input("Cleared. Check memory usage...");
|
||||
|
||||
// Test 2: While loop with accumulation
|
||||
print("Test 2: While loop accumulation");
|
||||
var accumulator = [];
|
||||
var counter = 0;
|
||||
while (counter < 500000) {
|
||||
push(accumulator, {
|
||||
"count": counter,
|
||||
"func": func() { return counter * 2; },
|
||||
"meta": ["item" + counter, counter % 100]
|
||||
});
|
||||
counter = counter + 1;
|
||||
}
|
||||
print("Accumulated " + len(accumulator) + " items in while loop");
|
||||
print("Memory: " + memoryUsage() + " MB");
|
||||
input("Press Enter to clear accumulator...");
|
||||
accumulator = [];
|
||||
print("Memory after clear: " + memoryUsage() + " MB");
|
||||
input("Cleared. Check memory usage...");
|
||||
|
||||
// Test 3: For loop with variable reassignment
|
||||
print("Test 3: Variable reassignment in loops");
|
||||
var reassignVar = none;
|
||||
for (var i = 0; i < 200000; i++) {
|
||||
// Constantly reassign to different types
|
||||
if (i % 4 == 0) {
|
||||
reassignVar = [i, func() { return i; }];
|
||||
} else if (i % 4 == 1) {
|
||||
reassignVar = {"id": i, "func": func() { return i*2; }};
|
||||
} else if (i % 4 == 2) {
|
||||
reassignVar = func() { return i*3; };
|
||||
} else {
|
||||
reassignVar = "string" + i;
|
||||
}
|
||||
}
|
||||
print("Completed " + 200000 + " reassignments, final type: " + type(reassignVar));
|
||||
print("Memory: " + memoryUsage() + " MB");
|
||||
input("Press Enter to clear reassignment var...");
|
||||
reassignVar = none;
|
||||
print("Memory after clear: " + memoryUsage() + " MB");
|
||||
input("Cleared. Check memory usage...");
|
||||
|
||||
// Test 4: Do-while with function creation
|
||||
print("Test 4: Do-while function creation");
|
||||
var doWhileFuncs = [];
|
||||
var dwCounter = 0;
|
||||
do {
|
||||
push(doWhileFuncs, func() {
|
||||
var captured = dwCounter;
|
||||
return func() {
|
||||
return captured * captured;
|
||||
};
|
||||
});
|
||||
dwCounter = dwCounter + 1;
|
||||
} while (dwCounter < 100000);
|
||||
print("Created " + len(doWhileFuncs) + " functions in do-while");
|
||||
print("Memory: " + memoryUsage() + " MB");
|
||||
input("Press Enter to clear do-while functions...");
|
||||
doWhileFuncs = "cleared";
|
||||
print("Memory after clear: " + memoryUsage() + " MB");
|
||||
input("Cleared. Check memory usage...");
|
||||
|
||||
// Test 5: Loop with early breaks and continues
|
||||
print("Test 5: Complex loop control flow");
|
||||
var complexData = [];
|
||||
for (var i = 0; i < 500000; i++) {
|
||||
if (i % 7 == 0) {
|
||||
continue; // Skip some iterations
|
||||
}
|
||||
|
||||
if (i > 400000 && i % 100 == 0) {
|
||||
// Create larger objects near the end
|
||||
push(complexData, {
|
||||
"large": [
|
||||
func() { return i; },
|
||||
[i, i+1, i+2, i+3],
|
||||
{"nested": {"deep": func() { return i*2; }}}
|
||||
]
|
||||
});
|
||||
} else {
|
||||
push(complexData, func() { return i; });
|
||||
}
|
||||
|
||||
if (i > 450000 && len(complexData) > 350000) {
|
||||
break; // Early exit
|
||||
}
|
||||
}
|
||||
print("Complex loop created " + len(complexData) + " items");
|
||||
print("Memory: " + memoryUsage() + " MB");
|
||||
input("Press Enter to clear complex data...");
|
||||
complexData = none;
|
||||
print("Memory after clear: " + memoryUsage() + " MB");
|
||||
input("Cleared. Check memory usage...");
|
||||
|
||||
// Test 6: Memory churn test (create and destroy in same loop)
|
||||
print("Test 6: Memory churn test");
|
||||
for (var cycle = 0; cycle < 100; cycle++) {
|
||||
var churnData = [];
|
||||
|
||||
// Create lots of data
|
||||
for (var i = 0; i < 10000; i++) {
|
||||
push(churnData, [
|
||||
func() { return i + cycle; },
|
||||
{"cycle": cycle, "item": i}
|
||||
]);
|
||||
}
|
||||
|
||||
// Clear it immediately
|
||||
churnData = none;
|
||||
|
||||
if (cycle % 10 == 0) {
|
||||
print("Completed churn cycle " + cycle + ", Memory: " + memoryUsage() + " MB");
|
||||
}
|
||||
}
|
||||
print("Final memory after churn test: " + memoryUsage() + " MB");
|
||||
input("Completed memory churn test. Check memory usage...");
|
||||
|
||||
print("=== Loop Tests Complete ===");
|
||||
113
leakTests/leaktest_mixed.bob
Normal file
113
leakTests/leaktest_mixed.bob
Normal file
@ -0,0 +1,113 @@
|
||||
// Memory leak test: Mixed type scenarios and edge cases
|
||||
// Test combinations and edge cases that might cause leaks
|
||||
|
||||
print("=== Mixed Type Memory Leak Tests ===");
|
||||
print("Initial memory: " + memoryUsage() + " MB");
|
||||
|
||||
// Test 1: Functions with collection captures
|
||||
print("Test 1: Functions capturing collections");
|
||||
var funcWithCollections = [];
|
||||
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() {
|
||||
// Capture both collections
|
||||
var localArray = capturedArray;
|
||||
var localDict = capturedDict;
|
||||
return func() {
|
||||
return len(localArray) + len(localDict);
|
||||
};
|
||||
});
|
||||
}
|
||||
print("Created " + len(funcWithCollections) + " functions with collection captures");
|
||||
print("Memory: " + memoryUsage() + " MB");
|
||||
input("Press Enter to clear function collections...");
|
||||
funcWithCollections = [];
|
||||
print("Memory after clear: " + memoryUsage() + " MB");
|
||||
input("Cleared. Check memory usage...");
|
||||
|
||||
// Test 2: Collections containing functions and other collections
|
||||
print("Test 2: Collections with mixed content");
|
||||
var mixedContent = [];
|
||||
for (var i = 0; i < 30000; i++) {
|
||||
push(mixedContent, [
|
||||
func() { return i; }, // Function
|
||||
[i, i+1, func() { return i*2; }], // Array with function
|
||||
{"value": i, "func": func() { return i*3; }}, // Dict with function
|
||||
"string" + i, // String
|
||||
i * 1.5, // Number
|
||||
i % 2 == 0 // Boolean
|
||||
]);
|
||||
}
|
||||
print("Created " + len(mixedContent) + " mixed content collections");
|
||||
print("Memory: " + memoryUsage() + " MB");
|
||||
input("Press Enter to clear mixed content...");
|
||||
mixedContent = none;
|
||||
print("Memory after clear: " + memoryUsage() + " MB");
|
||||
input("Cleared. Check memory usage...");
|
||||
|
||||
// Test 3: Property assignment patterns
|
||||
print("Test 3: Property assignment patterns");
|
||||
var propObjects = [];
|
||||
for (var i = 0; i < 100000; i++) {
|
||||
var obj = {"base": i};
|
||||
// Dynamic property assignment
|
||||
obj["prop" + i] = func() { return i; };
|
||||
obj["nested"] = {"deep": {"value": i}};
|
||||
obj["array"] = [1, 2, 3];
|
||||
obj["array"][0] = func() { return i * 2; };
|
||||
|
||||
push(propObjects, obj);
|
||||
}
|
||||
print("Created " + len(propObjects) + " objects with dynamic properties");
|
||||
print("Memory: " + memoryUsage() + " MB");
|
||||
input("Press Enter to clear property objects...");
|
||||
propObjects = "cleared";
|
||||
print("Memory after clear: " + memoryUsage() + " MB");
|
||||
input("Cleared. Check memory usage...");
|
||||
|
||||
// Test 4: Type reassignment chains
|
||||
print("Test 4: Type reassignment chains");
|
||||
var typeChains = [];
|
||||
for (var i = 0; i < 200000; i++) {
|
||||
push(typeChains, 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++) {
|
||||
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++) {
|
||||
typeChains[i] = {"id": i, "func": func() { return i; }};
|
||||
}
|
||||
print("Memory after dict conversion: " + memoryUsage() + " MB");
|
||||
input("Converted to dicts. Press Enter to clear...");
|
||||
typeChains = none;
|
||||
print("Memory after clear: " + memoryUsage() + " MB");
|
||||
input("Cleared. Check memory usage...");
|
||||
|
||||
// Test 5: Rapid allocation/deallocation
|
||||
print("Test 5: Rapid allocation/deallocation");
|
||||
for (var round = 0; round < 10; round++) {
|
||||
var temp = [];
|
||||
for (var i = 0; i < 100000; i++) {
|
||||
push(temp, [
|
||||
func() { return i; },
|
||||
{"data": [i, i+1, func() { return i*2; }]}
|
||||
]);
|
||||
}
|
||||
print("Round " + round + ": Created " + len(temp) + " items, Memory: " + memoryUsage() + " MB");
|
||||
temp = none; // Clear immediately
|
||||
if (round % 2 == 1) print("After clear round " + round + ": " + memoryUsage() + " MB");
|
||||
}
|
||||
print("Final memory after all cycles: " + memoryUsage() + " MB");
|
||||
input("Completed rapid allocation cycles. Check memory usage...");
|
||||
|
||||
print("=== Mixed Type Tests Complete ===");
|
||||
52
run_leak_tests.sh
Executable file
52
run_leak_tests.sh
Executable file
@ -0,0 +1,52 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Bob Memory Leak Test Runner
|
||||
# Runs all leak tests in sequence with memory monitoring
|
||||
|
||||
echo "🧪 Bob Memory Leak Test Suite"
|
||||
echo "============================"
|
||||
echo ""
|
||||
|
||||
# Build first
|
||||
echo "📦 Building Bob..."
|
||||
ninja -C build-ninja
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "❌ Build failed"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Build successful"
|
||||
echo ""
|
||||
|
||||
# Function to run a test and show memory info
|
||||
run_test() {
|
||||
local test_file=$1
|
||||
local test_name=$2
|
||||
|
||||
echo "🔬 Running: $test_name"
|
||||
echo "File: $test_file"
|
||||
echo "Memory monitoring: Use Activity Monitor (macOS) or top/htop (Linux)"
|
||||
echo "Press Ctrl+C during test to abort, or follow prompts to continue"
|
||||
echo ""
|
||||
|
||||
./build-ninja/bin/bob "$test_file"
|
||||
|
||||
echo ""
|
||||
echo "✅ Completed: $test_name"
|
||||
echo "----------------------------------------"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Run all tests
|
||||
run_test "leakTests/leaktest_functions.bob" "Function Memory Tests"
|
||||
run_test "leakTests/leaktest_collections.bob" "Collection Memory Tests"
|
||||
run_test "leakTests/leaktest_mixed.bob" "Mixed Type Memory Tests"
|
||||
run_test "leakTests/leaktest_loops.bob" "Loop Memory Tests"
|
||||
run_test "leakTests/leaktest_builtin.bob" "Builtin Function Memory Tests"
|
||||
|
||||
echo "🎉 All memory leak tests completed!"
|
||||
echo ""
|
||||
echo "💡 Memory Monitoring Tips:"
|
||||
echo "- Memory should spike during object creation"
|
||||
echo "- Memory should drop after 'cleared' messages"
|
||||
echo "- Memory should return close to baseline between tests"
|
||||
echo "- Watch for gradual increases across test cycles (indicates leaks)"
|
||||
@ -14,8 +14,10 @@ struct IncrementExpr;
|
||||
struct TernaryExpr;
|
||||
struct ArrayLiteralExpr;
|
||||
struct ArrayIndexExpr;
|
||||
struct PropertyExpr;
|
||||
|
||||
struct ArrayAssignExpr;
|
||||
struct PropertyAssignExpr;
|
||||
struct DictLiteralExpr;
|
||||
struct DictIndexExpr;
|
||||
struct DictAssignExpr;
|
||||
@ -44,8 +46,10 @@ struct ExprVisitor
|
||||
virtual Value visitTernaryExpr(const std::shared_ptr<TernaryExpr>& expr) = 0;
|
||||
virtual Value visitArrayLiteralExpr(const std::shared_ptr<ArrayLiteralExpr>& expr) = 0;
|
||||
virtual Value visitArrayIndexExpr(const std::shared_ptr<ArrayIndexExpr>& expr) = 0;
|
||||
virtual Value visitPropertyExpr(const std::shared_ptr<PropertyExpr>& expr) = 0;
|
||||
|
||||
virtual Value visitArrayAssignExpr(const std::shared_ptr<ArrayAssignExpr>& expr) = 0;
|
||||
virtual Value visitPropertyAssignExpr(const std::shared_ptr<PropertyAssignExpr>& expr) = 0;
|
||||
virtual Value visitDictLiteralExpr(const std::shared_ptr<DictLiteralExpr>& expr) = 0;
|
||||
|
||||
};
|
||||
@ -205,7 +209,19 @@ struct ArrayIndexExpr : Expr
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
struct PropertyExpr : Expr
|
||||
{
|
||||
std::shared_ptr<Expr> object;
|
||||
Token name;
|
||||
|
||||
PropertyExpr(std::shared_ptr<Expr> object, Token name)
|
||||
: object(object), name(name) {}
|
||||
|
||||
Value accept(ExprVisitor* visitor) override
|
||||
{
|
||||
return visitor->visitPropertyExpr(std::static_pointer_cast<PropertyExpr>(shared_from_this()));
|
||||
}
|
||||
};
|
||||
|
||||
struct ArrayAssignExpr : Expr
|
||||
{
|
||||
@ -222,6 +238,21 @@ struct ArrayAssignExpr : Expr
|
||||
}
|
||||
};
|
||||
|
||||
struct PropertyAssignExpr : Expr
|
||||
{
|
||||
std::shared_ptr<Expr> object;
|
||||
Token name;
|
||||
std::shared_ptr<Expr> value;
|
||||
|
||||
PropertyAssignExpr(std::shared_ptr<Expr> object, Token name, std::shared_ptr<Expr> value)
|
||||
: object(object), name(name), value(value) {}
|
||||
|
||||
Value accept(ExprVisitor* visitor) override
|
||||
{
|
||||
return visitor->visitPropertyAssignExpr(std::static_pointer_cast<PropertyAssignExpr>(shared_from_this()));
|
||||
}
|
||||
};
|
||||
|
||||
struct DictLiteralExpr : Expr
|
||||
{
|
||||
std::vector<std::pair<std::string, std::shared_ptr<Expr>>> pairs;
|
||||
|
||||
@ -44,6 +44,9 @@ public:
|
||||
// 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(); }
|
||||
|
||||
|
||||
@ -33,6 +33,13 @@ public:
|
||||
Value visitTernaryExpr(const std::shared_ptr<TernaryExpr>& expression) override;
|
||||
Value visitArrayLiteralExpr(const std::shared_ptr<ArrayLiteralExpr>& expression) override;
|
||||
Value visitArrayIndexExpr(const std::shared_ptr<ArrayIndexExpr>& expression) override;
|
||||
Value visitPropertyExpr(const std::shared_ptr<PropertyExpr>& expression) override;
|
||||
Value visitArrayAssignExpr(const std::shared_ptr<ArrayAssignExpr>& expression) override;
|
||||
Value visitPropertyAssignExpr(const std::shared_ptr<PropertyAssignExpr>& expression) override;
|
||||
Value visitDictLiteralExpr(const std::shared_ptr<DictLiteralExpr>& expression) override;
|
||||
|
||||
private:
|
||||
// Helper methods for builtin properties
|
||||
Value getArrayProperty(const Value& array, const std::string& propertyName);
|
||||
Value getDictProperty(const Value& dict, const std::string& propertyName);
|
||||
};
|
||||
|
||||
@ -64,6 +64,12 @@ private:
|
||||
std::vector<std::shared_ptr<Thunk>> thunks; // Store thunks to prevent memory leaks
|
||||
ErrorReporter* errorReporter;
|
||||
bool inThunkExecution = false;
|
||||
|
||||
// Automatic cleanup tracking
|
||||
int functionCreationCount = 0;
|
||||
int thunkCreationCount = 0;
|
||||
static const int CLEANUP_THRESHOLD = 1000000;
|
||||
|
||||
RuntimeDiagnostics diagnostics; // Utility functions for runtime operations
|
||||
std::unique_ptr<Evaluator> evaluator;
|
||||
std::unique_ptr<Executor> executor;
|
||||
@ -78,6 +84,7 @@ public:
|
||||
|
||||
// Methods needed by Evaluator
|
||||
Value evaluate(const std::shared_ptr<Expr>& expr);
|
||||
Value evaluateCallExprInline(const std::shared_ptr<CallExpr>& expression); // Inline TCO for performance
|
||||
void execute(const std::shared_ptr<Stmt>& statement, ExecutionContext* context = nullptr);
|
||||
void executeBlock(std::vector<std::shared_ptr<Stmt>> statements, std::shared_ptr<Environment> env, ExecutionContext* context = nullptr);
|
||||
bool isTruthy(Value object);
|
||||
@ -93,6 +100,15 @@ public:
|
||||
void cleanupUnusedFunctions();
|
||||
void cleanupUnusedThunks();
|
||||
void forceCleanup();
|
||||
|
||||
// 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);
|
||||
|
||||
@ -26,9 +26,13 @@ public:
|
||||
|
||||
// Memory management utilities
|
||||
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);
|
||||
|
||||
private:
|
||||
// Helper methods for stringify
|
||||
|
||||
@ -32,14 +32,17 @@ struct Value {
|
||||
union {
|
||||
double number;
|
||||
bool boolean;
|
||||
Function* function;
|
||||
BuiltinFunction* builtin_function;
|
||||
Thunk* thunk;
|
||||
};
|
||||
ValueType type;
|
||||
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
|
||||
|
||||
// Store functions as shared_ptr for proper reference counting
|
||||
std::shared_ptr<Function> function;
|
||||
std::shared_ptr<BuiltinFunction> builtin_function;
|
||||
std::shared_ptr<Thunk> thunk;
|
||||
|
||||
|
||||
// Constructors
|
||||
Value() : number(0.0), type(ValueType::VAL_NONE) {}
|
||||
@ -48,20 +51,28 @@ struct Value {
|
||||
Value(const char* s) : type(ValueType::VAL_STRING), string_value(s ? s : "") {}
|
||||
Value(const std::string& s) : type(ValueType::VAL_STRING), string_value(s) {}
|
||||
Value(std::string&& s) : type(ValueType::VAL_STRING), string_value(std::move(s)) {}
|
||||
Value(Function* f) : function(f), type(ValueType::VAL_FUNCTION) {}
|
||||
Value(BuiltinFunction* bf) : builtin_function(bf), type(ValueType::VAL_BUILTIN_FUNCTION) {}
|
||||
Value(Thunk* t) : thunk(t), type(ValueType::VAL_THUNK) {}
|
||||
Value(std::shared_ptr<Function> f) : function(f), type(ValueType::VAL_FUNCTION) {}
|
||||
Value(std::shared_ptr<BuiltinFunction> bf) : builtin_function(bf), type(ValueType::VAL_BUILTIN_FUNCTION) {}
|
||||
Value(std::shared_ptr<Thunk> t) : thunk(t), type(ValueType::VAL_THUNK) {}
|
||||
Value(const std::vector<Value>& arr) : type(ValueType::VAL_ARRAY), array_value(std::make_shared<std::vector<Value> >(arr)) {}
|
||||
Value(std::vector<Value>&& arr) : type(ValueType::VAL_ARRAY), array_value(std::make_shared<std::vector<Value> >(std::move(arr))) {}
|
||||
Value(const std::unordered_map<std::string, Value>& dict) : type(ValueType::VAL_DICT), dict_value(std::make_shared<std::unordered_map<std::string, Value> >(dict)) {}
|
||||
Value(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))) {}
|
||||
|
||||
// Destructor to clean up functions and thunks
|
||||
~Value() {
|
||||
// Functions and thunks are managed by the Interpreter, so we don't delete them
|
||||
// Arrays and dictionaries are managed by shared_ptr, so they clean up automatically
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 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)) {
|
||||
if (type != ValueType::VAL_STRING && type != ValueType::VAL_ARRAY && type != ValueType::VAL_DICT) {
|
||||
: 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)) {
|
||||
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
|
||||
}
|
||||
other.type = ValueType::VAL_NONE;
|
||||
@ -74,12 +85,19 @@ struct Value {
|
||||
if (type == ValueType::VAL_STRING) {
|
||||
string_value = std::move(other.string_value);
|
||||
} else if (type == ValueType::VAL_ARRAY) {
|
||||
array_value = std::move(other.array_value); // shared_ptr automatically handles moving
|
||||
array_value = std::move(other.array_value);
|
||||
} else if (type == ValueType::VAL_DICT) {
|
||||
dict_value = std::move(other.dict_value); // shared_ptr automatically handles moving
|
||||
dict_value = std::move(other.dict_value);
|
||||
} else if (type == ValueType::VAL_FUNCTION) {
|
||||
function = std::move(other.function);
|
||||
} else if (type == ValueType::VAL_BUILTIN_FUNCTION) {
|
||||
builtin_function = std::move(other.builtin_function);
|
||||
} else if (type == ValueType::VAL_THUNK) {
|
||||
thunk = std::move(other.thunk);
|
||||
} else {
|
||||
number = other.number; // Copy the union
|
||||
number = other.number;
|
||||
}
|
||||
|
||||
other.type = ValueType::VAL_NONE;
|
||||
}
|
||||
return *this;
|
||||
@ -93,23 +111,43 @@ struct Value {
|
||||
array_value = other.array_value; // shared_ptr automatically handles sharing
|
||||
} else if (type == ValueType::VAL_DICT) {
|
||||
dict_value = other.dict_value; // shared_ptr automatically handles sharing
|
||||
} else if (type == ValueType::VAL_FUNCTION) {
|
||||
function = other.function; // shared_ptr automatically handles sharing
|
||||
} else if (type == ValueType::VAL_BUILTIN_FUNCTION) {
|
||||
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 {
|
||||
number = other.number; // Copy the union
|
||||
number = other.number;
|
||||
}
|
||||
}
|
||||
|
||||
// Copy assignment (only when needed)
|
||||
Value& operator=(const Value& other) {
|
||||
if (this != &other) {
|
||||
// First, clear all old shared_ptr members to release references
|
||||
array_value.reset();
|
||||
dict_value.reset();
|
||||
function.reset();
|
||||
builtin_function.reset();
|
||||
thunk.reset();
|
||||
|
||||
// Then set the new type and value
|
||||
type = other.type;
|
||||
if (type == ValueType::VAL_STRING) {
|
||||
string_value = other.string_value;
|
||||
} else if (type == ValueType::VAL_ARRAY) {
|
||||
array_value = other.array_value; // shared_ptr automatically handles sharing
|
||||
} else if (type == ValueType::VAL_DICT) {
|
||||
dict_value = other.dict_value; // shared_ptr automatically handles sharing
|
||||
dict_value = other.dict_value;
|
||||
} else if (type == ValueType::VAL_FUNCTION) {
|
||||
function = other.function; // shared_ptr automatically handles sharing
|
||||
} else if (type == ValueType::VAL_BUILTIN_FUNCTION) {
|
||||
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 {
|
||||
number = other.number; // Copy the union
|
||||
number = other.number;
|
||||
}
|
||||
}
|
||||
return *this;
|
||||
@ -160,9 +198,9 @@ struct Value {
|
||||
inline std::unordered_map<std::string, Value>& asDict() {
|
||||
return *dict_value;
|
||||
}
|
||||
inline Function* asFunction() const { return isFunction() ? function : nullptr; }
|
||||
inline BuiltinFunction* asBuiltinFunction() const { return isBuiltinFunction() ? builtin_function : nullptr; }
|
||||
inline Thunk* asThunk() const { return isThunk() ? thunk : 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; }
|
||||
|
||||
// Truthiness check - inline for performance
|
||||
inline bool isTruthy() const {
|
||||
|
||||
@ -430,5 +430,5 @@ Value Evaluator::visitFunctionExpr(const std::shared_ptr<FunctionExpr>& expressi
|
||||
|
||||
auto function = std::make_shared<Function>("", paramNames, expression->body, interpreter->getEnvironment());
|
||||
interpreter->addFunction(function);
|
||||
return Value(function.get());
|
||||
return Value(function);
|
||||
}
|
||||
|
||||
@ -63,7 +63,7 @@ void Executor::visitFunctionStmt(const std::shared_ptr<FunctionStmt>& statement,
|
||||
statement->body,
|
||||
interpreter->getEnvironment());
|
||||
interpreter->addFunction(function);
|
||||
interpreter->getEnvironment()->define(statement->name.lexeme, Value(function.get()));
|
||||
interpreter->getEnvironment()->define(statement->name.lexeme, Value(function));
|
||||
}
|
||||
|
||||
void Executor::visitReturnStmt(const std::shared_ptr<ReturnStmt>& statement, ExecutionContext* context) {
|
||||
|
||||
@ -1,96 +0,0 @@
|
||||
#include "Interpreter.h"
|
||||
#include "Evaluator.h"
|
||||
#include "Executor.h"
|
||||
#include "BobStdLib.h"
|
||||
#include <iostream>
|
||||
|
||||
Interpreter::Interpreter(bool isInteractive)
|
||||
: isInteractive(isInteractive), errorReporter(nullptr) {
|
||||
evaluator = std::make_unique<Evaluator>(this);
|
||||
executor = std::make_unique<Executor>(this, evaluator.get());
|
||||
environment = std::make_shared<Environment>();
|
||||
}
|
||||
|
||||
Interpreter::~Interpreter() = default;
|
||||
|
||||
void Interpreter::interpret(std::vector<std::shared_ptr<Stmt>> statements) {
|
||||
executor->interpret(statements);
|
||||
}
|
||||
|
||||
void Interpreter::execute(const std::shared_ptr<Stmt>& statement, ExecutionContext* context) {
|
||||
statement->accept(executor.get(), context);
|
||||
}
|
||||
|
||||
void Interpreter::executeBlock(std::vector<std::shared_ptr<Stmt>> statements, std::shared_ptr<Environment> env, ExecutionContext* context) {
|
||||
executor->executeBlock(statements, env, context);
|
||||
}
|
||||
|
||||
Value Interpreter::evaluate(const std::shared_ptr<Expr>& expr) {
|
||||
Value result = expr->accept(evaluator.get());
|
||||
if (inThunkExecution) {
|
||||
return result;
|
||||
}
|
||||
return runTrampoline(result);
|
||||
}
|
||||
|
||||
Value Interpreter::runTrampoline(Value initialResult) {
|
||||
Value current = initialResult;
|
||||
while (current.isThunk()) {
|
||||
current = current.asThunk()->execute();
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
bool Interpreter::isTruthy(Value object) {
|
||||
return diagnostics.isTruthy(object);
|
||||
}
|
||||
|
||||
bool Interpreter::isEqual(Value a, Value b) {
|
||||
return diagnostics.isEqual(a, b);
|
||||
}
|
||||
|
||||
std::string Interpreter::stringify(Value object) {
|
||||
return diagnostics.stringify(object);
|
||||
}
|
||||
|
||||
void Interpreter::addStdLibFunctions() {
|
||||
BobStdLib::addToEnvironment(environment, *this, errorReporter);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
void Interpreter::setErrorReporter(ErrorReporter* reporter) {
|
||||
errorReporter = reporter;
|
||||
if (environment) {
|
||||
environment->setErrorReporter(reporter);
|
||||
}
|
||||
addStdLibFunctions();
|
||||
}
|
||||
|
||||
bool Interpreter::isInteractiveMode() const {
|
||||
return isInteractive;
|
||||
}
|
||||
|
||||
std::shared_ptr<Environment> Interpreter::getEnvironment() {
|
||||
return environment;
|
||||
}
|
||||
|
||||
void Interpreter::setEnvironment(std::shared_ptr<Environment> env) {
|
||||
environment = env;
|
||||
}
|
||||
|
||||
void Interpreter::reportError(int line, int column, const std::string& errorType, const std::string& message, const std::string& lexeme) {
|
||||
if (errorReporter) {
|
||||
errorReporter->reportError(line, column, errorType, message, lexeme);
|
||||
}
|
||||
}
|
||||
@ -138,6 +138,16 @@ sptr(Expr) Parser::assignmentExpression()
|
||||
auto arrayExpr = std::dynamic_pointer_cast<ArrayIndexExpr>(expr);
|
||||
return msptr(ArrayAssignExpr)(arrayExpr->array, arrayExpr->index, value, arrayExpr->bracket);
|
||||
}
|
||||
else if(std::dynamic_pointer_cast<PropertyExpr>(expr))
|
||||
{
|
||||
auto propertyExpr = std::dynamic_pointer_cast<PropertyExpr>(expr);
|
||||
return msptr(PropertyAssignExpr)(propertyExpr->object, propertyExpr->name, value);
|
||||
}
|
||||
else if(std::dynamic_pointer_cast<ArrayIndexExpr>(expr))
|
||||
{
|
||||
auto arrayExpr = std::dynamic_pointer_cast<ArrayIndexExpr>(expr);
|
||||
return msptr(ArrayAssignExpr)(arrayExpr->array, arrayExpr->index, value, arrayExpr->bracket);
|
||||
}
|
||||
|
||||
|
||||
if (errorReporter) {
|
||||
@ -357,6 +367,9 @@ sptr(Expr) Parser::call()
|
||||
expr = finishCall(expr);
|
||||
} else if (match({OPEN_BRACKET})) {
|
||||
expr = finishArrayIndex(expr);
|
||||
} else if (match({DOT})) {
|
||||
Token name = consume(IDENTIFIER, "Expected property name after '.'.");
|
||||
expr = msptr(PropertyExpr)(expr, name);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
@ -472,33 +485,27 @@ sptr(Stmt) Parser::statement()
|
||||
if(match({CONTINUE})) return continueStatement();
|
||||
if(match({OPEN_BRACE})) return msptr(BlockStmt)(block());
|
||||
|
||||
// Check for assignment statement
|
||||
// Check for assignment statement - simplified approach
|
||||
if(check(IDENTIFIER)) {
|
||||
// Look ahead to see if this is an assignment
|
||||
// Try to parse as assignment expression first
|
||||
int currentPos = current;
|
||||
advance(); // consume identifier
|
||||
|
||||
// Check for simple variable assignment
|
||||
if(match({EQUAL, PLUS_EQUAL, MINUS_EQUAL, STAR_EQUAL, SLASH_EQUAL, PERCENT_EQUAL,
|
||||
BIN_AND_EQUAL, BIN_OR_EQUAL, BIN_XOR_EQUAL, BIN_SLEFT_EQUAL, BIN_SRIGHT_EQUAL})) {
|
||||
// Reset position and parse as assignment statement
|
||||
current = currentPos;
|
||||
return assignmentStatement();
|
||||
}
|
||||
|
||||
// Check for array assignment (identifier followed by [)
|
||||
if(match({OPEN_BRACKET})) {
|
||||
// Reset position and parse as assignment expression
|
||||
current = currentPos;
|
||||
try {
|
||||
sptr(Expr) expr = assignmentExpression();
|
||||
consume(SEMICOLON, "Expected ';' after assignment.");
|
||||
return msptr(ExpressionStmt)(expr);
|
||||
|
||||
// If we successfully parsed an assignment expression, it's an assignment statement
|
||||
if(std::dynamic_pointer_cast<AssignExpr>(expr) ||
|
||||
std::dynamic_pointer_cast<ArrayAssignExpr>(expr) ||
|
||||
std::dynamic_pointer_cast<PropertyAssignExpr>(expr)) {
|
||||
consume(SEMICOLON, "Expected ';' after assignment.");
|
||||
return msptr(ExpressionStmt)(expr);
|
||||
}
|
||||
|
||||
// If it's not an assignment, reset and parse as expression statement
|
||||
current = currentPos;
|
||||
} catch (...) {
|
||||
// If assignment parsing failed, reset and parse as expression statement
|
||||
current = currentPos;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Reset position and parse as expression statement
|
||||
current = currentPos;
|
||||
}
|
||||
|
||||
return expressionStatement();
|
||||
|
||||
@ -48,4 +48,20 @@ Value Environment::get(const std::string& 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>{});
|
||||
}
|
||||
}
|
||||
if (parent) {
|
||||
parent->pruneForClosureCapture();
|
||||
}
|
||||
}
|
||||
@ -213,27 +213,74 @@ Value Evaluator::visitIncrementExpr(const std::shared_ptr<IncrementExpr>& expres
|
||||
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;
|
||||
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
|
||||
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
|
||||
}
|
||||
default:
|
||||
break;
|
||||
} else {
|
||||
// Handle 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);
|
||||
}
|
||||
|
||||
interpreter->getEnvironment()->assign(expression->name, newValue);
|
||||
return newValue;
|
||||
}
|
||||
interpreter->getEnvironment()->assign(expression->name, value);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
@ -248,62 +295,8 @@ Value Evaluator::visitTernaryExpr(const std::shared_ptr<TernaryExpr>& expression
|
||||
}
|
||||
|
||||
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.");
|
||||
}
|
||||
// Delegate to inline implementation in Interpreter for performance
|
||||
return interpreter->evaluateCallExprInline(expression);
|
||||
}
|
||||
|
||||
Value Evaluator::visitArrayLiteralExpr(const std::shared_ptr<ArrayLiteralExpr>& expr) {
|
||||
@ -364,6 +357,21 @@ Value Evaluator::visitArrayIndexExpr(const std::shared_ptr<ArrayIndexExpr>& expr
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
} else if (object.isArray()) {
|
||||
return getArrayProperty(object, propertyName);
|
||||
} else {
|
||||
interpreter->reportError(expr->name.line, expr->name.column, "Runtime Error",
|
||||
"Cannot access property '" + propertyName + "' on this type", "");
|
||||
throw std::runtime_error("Cannot access property '" + propertyName + "' on this type");
|
||||
}
|
||||
}
|
||||
|
||||
Value Evaluator::visitArrayAssignExpr(const std::shared_ptr<ArrayAssignExpr>& expr) {
|
||||
Value array = expr->array->accept(this);
|
||||
Value index = expr->index->accept(this);
|
||||
@ -422,13 +430,95 @@ Value Evaluator::visitDictLiteralExpr(const std::shared_ptr<DictLiteralExpr>& ex
|
||||
return Value(dict);
|
||||
}
|
||||
|
||||
Value Evaluator::visitPropertyAssignExpr(const std::shared_ptr<PropertyAssignExpr>& expr) {
|
||||
Value object = expr->object->accept(this);
|
||||
Value value = expr->value->accept(this);
|
||||
std::string propertyName = expr->name.lexeme;
|
||||
|
||||
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 {
|
||||
interpreter->reportError(expr->name.line, expr->name.column, "Runtime Error",
|
||||
"Cannot assign property '" + propertyName + "' on non-object", "");
|
||||
throw std::runtime_error("Cannot assign property '" + propertyName + "' on non-object");
|
||||
}
|
||||
}
|
||||
|
||||
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.get());
|
||||
// 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);
|
||||
}
|
||||
|
||||
Value Evaluator::getArrayProperty(const Value& arrayValue, const std::string& propertyName) {
|
||||
const std::vector<Value>& arr = arrayValue.asArray();
|
||||
|
||||
// Create builtin array properties as an actual dictionary
|
||||
std::unordered_map<std::string, Value> arrayProperties;
|
||||
arrayProperties["length"] = Value(static_cast<double>(arr.size()));
|
||||
arrayProperties["empty"] = Value(arr.empty());
|
||||
|
||||
if (!arr.empty()) {
|
||||
arrayProperties["first"] = arr[0];
|
||||
arrayProperties["last"] = arr[arr.size() - 1];
|
||||
} else {
|
||||
arrayProperties["first"] = NONE_VALUE;
|
||||
arrayProperties["last"] = NONE_VALUE;
|
||||
}
|
||||
|
||||
// Look up the requested property
|
||||
auto it = arrayProperties.find(propertyName);
|
||||
if (it != arrayProperties.end()) {
|
||||
return it->second;
|
||||
} else {
|
||||
return NONE_VALUE; // Unknown property
|
||||
}
|
||||
}
|
||||
|
||||
Value Evaluator::getDictProperty(const Value& dictValue, const std::string& propertyName) {
|
||||
const std::unordered_map<std::string, Value>& dict = dictValue.asDict();
|
||||
|
||||
// First check if it's a user-defined property
|
||||
auto userProp = dict.find(propertyName);
|
||||
if (userProp != dict.end()) {
|
||||
return userProp->second;
|
||||
}
|
||||
|
||||
// If not found, check for builtin dictionary properties
|
||||
std::unordered_map<std::string, Value> dictProperties;
|
||||
dictProperties["length"] = Value(static_cast<double>(dict.size()));
|
||||
dictProperties["empty"] = Value(dict.empty());
|
||||
|
||||
// Create keys array
|
||||
std::vector<Value> keysArray;
|
||||
for (const auto& pair : dict) {
|
||||
keysArray.push_back(Value(pair.first));
|
||||
}
|
||||
dictProperties["keys"] = Value(keysArray);
|
||||
|
||||
// Create values array
|
||||
std::vector<Value> valuesArray;
|
||||
for (const auto& pair : dict) {
|
||||
valuesArray.push_back(pair.second);
|
||||
}
|
||||
dictProperties["values"] = Value(valuesArray);
|
||||
|
||||
auto builtinProp = dictProperties.find(propertyName);
|
||||
if (builtinProp != dictProperties.end()) {
|
||||
return builtinProp->second;
|
||||
}
|
||||
|
||||
// Property not found
|
||||
return NONE_VALUE;
|
||||
}
|
||||
|
||||
@ -63,7 +63,7 @@ void Executor::visitFunctionStmt(const std::shared_ptr<FunctionStmt>& statement,
|
||||
statement->body,
|
||||
interpreter->getEnvironment());
|
||||
interpreter->addFunction(function);
|
||||
interpreter->getEnvironment()->define(statement->name.lexeme, Value(function.get()));
|
||||
interpreter->getEnvironment()->define(statement->name.lexeme, Value(function));
|
||||
}
|
||||
|
||||
void Executor::visitReturnStmt(const std::shared_ptr<ReturnStmt>& statement, ExecutionContext* context) {
|
||||
@ -194,11 +194,21 @@ void Executor::visitContinueStmt(const std::shared_ptr<ContinueStmt>& statement,
|
||||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
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
|
||||
}
|
||||
} else {
|
||||
// Handle compound assignment operators
|
||||
Value currentValue = interpreter->getEnvironment()->get(statement->name);
|
||||
Value newValue;
|
||||
@ -242,4 +252,8 @@ void Executor::visitAssignStmt(const std::shared_ptr<AssignStmt>& statement, Exe
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@ -94,3 +94,139 @@ void Interpreter::reportError(int line, int column, const std::string& errorType
|
||||
errorReporter->reportError(line, column, errorType, message, lexeme);
|
||||
}
|
||||
}
|
||||
|
||||
void Interpreter::cleanupUnusedFunctions() {
|
||||
diagnostics.cleanupUnusedFunctions(functions);
|
||||
}
|
||||
|
||||
void Interpreter::cleanupUnusedThunks() {
|
||||
diagnostics.cleanupUnusedThunks(thunks);
|
||||
}
|
||||
|
||||
void Interpreter::forceCleanup() {
|
||||
diagnostics.forceCleanup(builtinFunctions, functions, thunks);
|
||||
}
|
||||
|
||||
Value Interpreter::evaluateCallExprInline(const std::shared_ptr<CallExpr>& expression) {
|
||||
Value callee = evaluate(expression->callee); // Direct call instead of through evaluator
|
||||
|
||||
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
|
||||
}
|
||||
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();
|
||||
if (errorReporter) {
|
||||
errorReporter->reportError(expression->paren.line, expression->paren.column, "Runtime Error",
|
||||
errorMsg, "");
|
||||
}
|
||||
throw std::runtime_error(errorMsg);
|
||||
}
|
||||
|
||||
Function* function = callee.asFunction();
|
||||
|
||||
std::vector<Value> arguments;
|
||||
for (const auto& argument : expression->arguments) {
|
||||
arguments.push_back(evaluate(argument)); // Direct call instead of through evaluator
|
||||
}
|
||||
|
||||
// Check arity (like original)
|
||||
if (arguments.size() != function->params.size()) {
|
||||
if (errorReporter) {
|
||||
errorReporter->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("Expected " + std::to_string(function->params.size()) +
|
||||
" arguments but got " + std::to_string(arguments.size()) + ".");
|
||||
}
|
||||
|
||||
// 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)
|
||||
ScopedEnv _env(environment);
|
||||
environment = std::make_shared<Environment>(function->closure);
|
||||
environment->setErrorReporter(errorReporter);
|
||||
|
||||
for (size_t i = 0; i < function->params.size(); i++) {
|
||||
environment->define(function->params[i], arguments[i]);
|
||||
}
|
||||
|
||||
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
|
||||
if (context.hasReturn) {
|
||||
return context.returnValue;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
thunkCreationCount = 0;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
for (size_t i = 0; i < function->params.size(); i++) {
|
||||
environment->define(function->params[i], arguments[i]);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -2,11 +2,17 @@
|
||||
#include "Value.h"
|
||||
#include "TypeWrapper.h" // For Function and BuiltinFunction definitions
|
||||
#include <sstream>
|
||||
#if defined(__linux__)
|
||||
#include <malloc.h>
|
||||
#elif defined(__APPLE__)
|
||||
#include <malloc/malloc.h>
|
||||
#endif
|
||||
#include <iomanip>
|
||||
#include <limits>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
|
||||
bool RuntimeDiagnostics::isTruthy(Value object) {
|
||||
if(object.isBoolean()) {
|
||||
return object.asBoolean();
|
||||
@ -182,6 +188,18 @@ void RuntimeDiagnostics::cleanupUnusedFunctions(std::vector<std::shared_ptr<Buil
|
||||
);
|
||||
}
|
||||
|
||||
void RuntimeDiagnostics::cleanupUnusedFunctions(std::vector<std::shared_ptr<Function>>& functions) {
|
||||
// Only remove functions that are definitely not referenced anywhere (use_count == 1)
|
||||
// This is more conservative to prevent dangling pointer issues
|
||||
functions.erase(
|
||||
std::remove_if(functions.begin(), functions.end(),
|
||||
[](const std::shared_ptr<Function>& func) {
|
||||
return func.use_count() == 1; // Only referenced by this vector, nowhere else
|
||||
}),
|
||||
functions.end()
|
||||
);
|
||||
}
|
||||
|
||||
void RuntimeDiagnostics::cleanupUnusedThunks(std::vector<std::shared_ptr<Thunk>>& thunks) {
|
||||
// Only remove thunks that are definitely not referenced anywhere (use_count == 1)
|
||||
// This is more conservative to prevent dangling pointer issues
|
||||
@ -212,4 +230,39 @@ void RuntimeDiagnostics::forceCleanup(std::vector<std::shared_ptr<BuiltinFunctio
|
||||
}),
|
||||
thunks.end()
|
||||
);
|
||||
}
|
||||
|
||||
void RuntimeDiagnostics::forceCleanup(std::vector<std::shared_ptr<BuiltinFunction>>& builtinFunctions,
|
||||
std::vector<std::shared_ptr<Function>>& functions,
|
||||
std::vector<std::shared_ptr<Thunk>>& thunks) {
|
||||
try {
|
||||
// Remove functions only when they are exclusively held by the interpreter vector
|
||||
functions.erase(
|
||||
std::remove_if(functions.begin(), functions.end(),
|
||||
[](const std::shared_ptr<Function>& func) {
|
||||
return func.use_count() == 1;
|
||||
}),
|
||||
functions.end()
|
||||
);
|
||||
|
||||
// Also cleanup builtin functions and thunks
|
||||
builtinFunctions.erase(
|
||||
std::remove_if(builtinFunctions.begin(), builtinFunctions.end(),
|
||||
[](const std::shared_ptr<BuiltinFunction>& func) {
|
||||
return func.use_count() <= 1; // Only referenced by Interpreter
|
||||
}),
|
||||
builtinFunctions.end()
|
||||
);
|
||||
|
||||
thunks.erase(
|
||||
std::remove_if(thunks.begin(), thunks.end(),
|
||||
[](const std::shared_ptr<Thunk>& thunk) {
|
||||
return thunk.use_count() <= 1; // Only referenced by Interpreter
|
||||
}),
|
||||
thunks.end()
|
||||
);
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Exception in forceCleanup: " << e.what() << std::endl;
|
||||
throw; // Re-throw to let the caller handle it
|
||||
}
|
||||
}
|
||||
@ -9,6 +9,16 @@
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
// Platform-specific includes for memory usage
|
||||
#if defined(__APPLE__) && defined(__MACH__)
|
||||
#include <mach/mach.h>
|
||||
#elif defined(__linux__)
|
||||
// Uses /proc/self/status, no extra includes needed
|
||||
#elif defined(_WIN32)
|
||||
#include <windows.h>
|
||||
#include <psapi.h>
|
||||
#endif
|
||||
|
||||
void BobStdLib::addToEnvironment(std::shared_ptr<Environment> env, Interpreter& interpreter, ErrorReporter* errorReporter) {
|
||||
// Create a built-in toString function
|
||||
auto toStringFunc = std::make_shared<BuiltinFunction>("toString",
|
||||
@ -23,7 +33,7 @@ void BobStdLib::addToEnvironment(std::shared_ptr<Environment> env, Interpreter&
|
||||
|
||||
return Value(interpreter.stringify(args[0]));
|
||||
});
|
||||
env->define("toString", Value(toStringFunc.get()));
|
||||
env->define("toString", Value(toStringFunc));
|
||||
|
||||
// Store the shared_ptr in the interpreter to keep it alive
|
||||
interpreter.addBuiltinFunction(toStringFunc);
|
||||
@ -42,7 +52,7 @@ void BobStdLib::addToEnvironment(std::shared_ptr<Environment> env, Interpreter&
|
||||
std::cout << interpreter.stringify(args[0]) << '\n';
|
||||
return NONE_VALUE;
|
||||
});
|
||||
env->define("print", Value(printFunc.get()));
|
||||
env->define("print", Value(printFunc));
|
||||
|
||||
// Store the shared_ptr in the interpreter to keep it alive
|
||||
interpreter.addBuiltinFunction(printFunc);
|
||||
@ -61,7 +71,7 @@ void BobStdLib::addToEnvironment(std::shared_ptr<Environment> env, Interpreter&
|
||||
std::cout << interpreter.stringify(args[0]) << std::flush;
|
||||
return NONE_VALUE;
|
||||
});
|
||||
env->define("printRaw", Value(printRawFunc.get()));
|
||||
env->define("printRaw", Value(printRawFunc));
|
||||
|
||||
// Store the shared_ptr in the interpreter to keep it alive
|
||||
interpreter.addBuiltinFunction(printRawFunc);
|
||||
@ -91,7 +101,7 @@ void BobStdLib::addToEnvironment(std::shared_ptr<Environment> env, Interpreter&
|
||||
throw std::runtime_error("len() can only be used on arrays, strings, and dictionaries");
|
||||
}
|
||||
});
|
||||
env->define("len", Value(lenFunc.get()));
|
||||
env->define("len", Value(lenFunc));
|
||||
|
||||
// Store the shared_ptr in the interpreter to keep it alive
|
||||
interpreter.addBuiltinFunction(lenFunc);
|
||||
@ -125,7 +135,7 @@ void BobStdLib::addToEnvironment(std::shared_ptr<Environment> env, Interpreter&
|
||||
|
||||
return args[0]; // Return the modified array
|
||||
});
|
||||
env->define("push", Value(pushFunc.get()));
|
||||
env->define("push", Value(pushFunc));
|
||||
interpreter.addBuiltinFunction(pushFunc);
|
||||
|
||||
// Create a built-in pop function for arrays
|
||||
@ -162,7 +172,7 @@ void BobStdLib::addToEnvironment(std::shared_ptr<Environment> env, Interpreter&
|
||||
|
||||
return lastElement; // Return the popped element
|
||||
});
|
||||
env->define("pop", Value(popFunc.get()));
|
||||
env->define("pop", Value(popFunc));
|
||||
interpreter.addBuiltinFunction(popFunc);
|
||||
|
||||
// Create a built-in assert function
|
||||
@ -201,7 +211,7 @@ void BobStdLib::addToEnvironment(std::shared_ptr<Environment> env, Interpreter&
|
||||
|
||||
return NONE_VALUE;
|
||||
});
|
||||
env->define("assert", Value(assertFunc.get()));
|
||||
env->define("assert", Value(assertFunc));
|
||||
|
||||
// Store the shared_ptr in the interpreter to keep it alive
|
||||
interpreter.addBuiltinFunction(assertFunc);
|
||||
@ -223,7 +233,7 @@ void BobStdLib::addToEnvironment(std::shared_ptr<Environment> env, Interpreter&
|
||||
|
||||
return Value(static_cast<double>(microseconds));
|
||||
});
|
||||
env->define("time", Value(timeFunc.get()));
|
||||
env->define("time", Value(timeFunc));
|
||||
|
||||
// Store the shared_ptr in the interpreter to keep it alive
|
||||
interpreter.addBuiltinFunction(timeFunc);
|
||||
@ -250,7 +260,7 @@ void BobStdLib::addToEnvironment(std::shared_ptr<Environment> env, Interpreter&
|
||||
|
||||
return Value(userInput);
|
||||
});
|
||||
env->define("input", Value(inputFunc.get()));
|
||||
env->define("input", Value(inputFunc));
|
||||
|
||||
// Store the shared_ptr in the interpreter to keep it alive
|
||||
interpreter.addBuiltinFunction(inputFunc);
|
||||
@ -289,7 +299,7 @@ void BobStdLib::addToEnvironment(std::shared_ptr<Environment> env, Interpreter&
|
||||
|
||||
return Value(typeName);
|
||||
});
|
||||
env->define("type", Value(typeFunc.get()));
|
||||
env->define("type", Value(typeFunc));
|
||||
|
||||
// Store the shared_ptr in the interpreter to keep it alive
|
||||
interpreter.addBuiltinFunction(typeFunc);
|
||||
@ -324,7 +334,7 @@ void BobStdLib::addToEnvironment(std::shared_ptr<Environment> env, Interpreter&
|
||||
return NONE_VALUE; // Return none for out of range
|
||||
}
|
||||
});
|
||||
env->define("toNumber", Value(toNumberFunc.get()));
|
||||
env->define("toNumber", Value(toNumberFunc));
|
||||
|
||||
// Store the shared_ptr in the interpreter to keep it alive
|
||||
interpreter.addBuiltinFunction(toNumberFunc);
|
||||
@ -352,7 +362,7 @@ void BobStdLib::addToEnvironment(std::shared_ptr<Environment> env, Interpreter&
|
||||
double value = args[0].asNumber();
|
||||
return Value(static_cast<double>(static_cast<long long>(value)));
|
||||
});
|
||||
env->define("toInt", Value(toIntFunc.get()));
|
||||
env->define("toInt", Value(toIntFunc));
|
||||
|
||||
// Store the shared_ptr in the interpreter to keep it alive
|
||||
interpreter.addBuiltinFunction(toIntFunc);
|
||||
@ -390,7 +400,7 @@ void BobStdLib::addToEnvironment(std::shared_ptr<Environment> env, Interpreter&
|
||||
// For any other type (functions, etc.), consider them truthy
|
||||
return Value(true);
|
||||
});
|
||||
env->define("toBoolean", Value(toBooleanFunc.get()));
|
||||
env->define("toBoolean", Value(toBooleanFunc));
|
||||
|
||||
// Store the shared_ptr in the interpreter to keep it alive
|
||||
interpreter.addBuiltinFunction(toBooleanFunc);
|
||||
@ -409,7 +419,7 @@ void BobStdLib::addToEnvironment(std::shared_ptr<Environment> env, Interpreter&
|
||||
|
||||
std::exit(exitCode);
|
||||
});
|
||||
env->define("exit", Value(exitFunc.get()));
|
||||
env->define("exit", Value(exitFunc));
|
||||
|
||||
// Store the shared_ptr in the interpreter to keep it alive
|
||||
interpreter.addBuiltinFunction(exitFunc);
|
||||
@ -448,7 +458,7 @@ void BobStdLib::addToEnvironment(std::shared_ptr<Environment> env, Interpreter&
|
||||
|
||||
return NONE_VALUE;
|
||||
});
|
||||
env->define("sleep", Value(sleepFunc.get()));
|
||||
env->define("sleep", Value(sleepFunc));
|
||||
|
||||
// Store the shared_ptr in the interpreter to keep it alive
|
||||
interpreter.addBuiltinFunction(sleepFunc);
|
||||
@ -473,7 +483,7 @@ void BobStdLib::addToEnvironment(std::shared_ptr<Environment> env, Interpreter&
|
||||
|
||||
return Value(static_cast<double>(rand()) / RAND_MAX);
|
||||
});
|
||||
env->define("random", Value(randomFunc.get()));
|
||||
env->define("random", Value(randomFunc));
|
||||
|
||||
// Store the shared_ptr in the interpreter to keep it alive
|
||||
interpreter.addBuiltinFunction(randomFunc);
|
||||
@ -526,7 +536,7 @@ void BobStdLib::addToEnvironment(std::shared_ptr<Environment> env, Interpreter&
|
||||
throw std::runtime_error("eval failed: " + std::string(e.what()));
|
||||
}
|
||||
});
|
||||
env->define("eval", Value(evalFunc.get()));
|
||||
env->define("eval", Value(evalFunc));
|
||||
|
||||
// Store the shared_ptr in the interpreter to keep it alive
|
||||
interpreter.addBuiltinFunction(evalFunc);
|
||||
@ -559,7 +569,7 @@ void BobStdLib::addToEnvironment(std::shared_ptr<Environment> env, Interpreter&
|
||||
|
||||
return Value(keys);
|
||||
});
|
||||
env->define("keys", Value(keysFunc.get()));
|
||||
env->define("keys", Value(keysFunc));
|
||||
interpreter.addBuiltinFunction(keysFunc);
|
||||
|
||||
// Create a built-in values function for dictionaries
|
||||
@ -590,7 +600,7 @@ void BobStdLib::addToEnvironment(std::shared_ptr<Environment> env, Interpreter&
|
||||
|
||||
return Value(values);
|
||||
});
|
||||
env->define("values", Value(valuesFunc.get()));
|
||||
env->define("values", Value(valuesFunc));
|
||||
interpreter.addBuiltinFunction(valuesFunc);
|
||||
|
||||
// Create a built-in has function for dictionaries
|
||||
@ -625,7 +635,7 @@ void BobStdLib::addToEnvironment(std::shared_ptr<Environment> env, Interpreter&
|
||||
|
||||
return Value(dict.find(key) != dict.end());
|
||||
});
|
||||
env->define("has", Value(hasFunc.get()));
|
||||
env->define("has", Value(hasFunc));
|
||||
interpreter.addBuiltinFunction(hasFunc);
|
||||
|
||||
// Create a built-in readFile function
|
||||
@ -664,7 +674,7 @@ void BobStdLib::addToEnvironment(std::shared_ptr<Environment> env, Interpreter&
|
||||
|
||||
return Value(buffer.str());
|
||||
});
|
||||
env->define("readFile", Value(readFileFunc.get()));
|
||||
env->define("readFile", Value(readFileFunc));
|
||||
interpreter.addBuiltinFunction(readFileFunc);
|
||||
|
||||
// Create a built-in writeFile function
|
||||
@ -711,7 +721,7 @@ void BobStdLib::addToEnvironment(std::shared_ptr<Environment> env, Interpreter&
|
||||
|
||||
return NONE_VALUE;
|
||||
});
|
||||
env->define("writeFile", Value(writeFileFunc.get()));
|
||||
env->define("writeFile", Value(writeFileFunc));
|
||||
interpreter.addBuiltinFunction(writeFileFunc);
|
||||
|
||||
// Create a built-in readLines function
|
||||
@ -754,7 +764,7 @@ void BobStdLib::addToEnvironment(std::shared_ptr<Environment> env, Interpreter&
|
||||
file.close();
|
||||
return Value(lines);
|
||||
});
|
||||
env->define("readLines", Value(readLinesFunc.get()));
|
||||
env->define("readLines", Value(readLinesFunc));
|
||||
interpreter.addBuiltinFunction(readLinesFunc);
|
||||
|
||||
// Create a built-in fileExists function
|
||||
@ -783,7 +793,56 @@ void BobStdLib::addToEnvironment(std::shared_ptr<Environment> env, Interpreter&
|
||||
|
||||
return Value(exists);
|
||||
});
|
||||
env->define("fileExists", Value(fileExistsFunc.get()));
|
||||
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);
|
||||
|
||||
}
|
||||
@ -2505,9 +2505,88 @@ assert(len(values(stressDict)) == 100, "Dictionary stress test - values");
|
||||
print("Enhanced dictionary tests: PASS");
|
||||
|
||||
// ========================================
|
||||
// TEST 52.7: TAIL CALL OPTIMIZATION
|
||||
// TEST 52.7: DOT NOTATION AND PROPERTY ACCESS
|
||||
// ========================================
|
||||
print("\n--- Test 52.7: Tail Call Optimization ---");
|
||||
print("\n--- Test 52.7: Dot Notation and Property Access ---");
|
||||
|
||||
// Basic dictionary property access (read)
|
||||
var person = {"name": "Alice", "age": 30, "city": "NYC"};
|
||||
assert(person.name == "Alice", "Basic dict property access - name");
|
||||
assert(person.age == 30, "Basic dict property access - age");
|
||||
assert(person.city == "NYC", "Basic dict property access - city");
|
||||
print(" Basic dict property access: PASS");
|
||||
|
||||
// Dictionary property assignment (write)
|
||||
var config = {"theme": "light", "lang": "en"};
|
||||
config.theme = "dark";
|
||||
config.lang = "es";
|
||||
config.fontSize = 16; // New property
|
||||
assert(config.theme == "dark", "Dict property assignment - theme");
|
||||
assert(config.lang == "es", "Dict property assignment - lang");
|
||||
assert(config.fontSize == 16, "Dict property assignment - new property");
|
||||
print(" Dict property assignment: PASS");
|
||||
|
||||
// Array properties (read-only)
|
||||
var arr = [10, 20, 30, 40, 50];
|
||||
assert(arr.length == 5, "Array length property");
|
||||
assert(arr.first == 10, "Array first property");
|
||||
assert(arr.last == 50, "Array last property");
|
||||
assert(arr.empty == false, "Array empty property");
|
||||
print(" Array properties: PASS");
|
||||
|
||||
// Array edge cases
|
||||
var emptyArr = [];
|
||||
assert(emptyArr.length == 0, "Empty array length");
|
||||
assert(emptyArr.first == none, "Empty array first");
|
||||
assert(emptyArr.last == none, "Empty array last");
|
||||
assert(emptyArr.empty == true, "Empty array empty");
|
||||
print(" Array edge cases: PASS");
|
||||
|
||||
// Dictionary builtin properties
|
||||
var dictWithProps = {"a": 1, "b": 2, "c": 3};
|
||||
assert(dictWithProps.length == 3, "Dict builtin length property");
|
||||
assert(dictWithProps.empty == false, "Dict builtin empty property");
|
||||
var emptyDict = {};
|
||||
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");
|
||||
|
||||
// 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");
|
||||
print(" Dict builtin properties: PASS");
|
||||
|
||||
// Nested property access and assignment
|
||||
var nested = {
|
||||
"database": {
|
||||
"host": "localhost",
|
||||
"port": 5432
|
||||
}
|
||||
};
|
||||
assert(nested.database.host == "localhost", "Nested property access");
|
||||
nested.database.port = 3306;
|
||||
nested.database.ssl = true; // New nested property
|
||||
assert(nested.database.port == 3306, "Nested property assignment");
|
||||
assert(nested.database.ssl == true, "New nested property assignment");
|
||||
print(" Nested properties: PASS");
|
||||
|
||||
// Property/bracket equivalence
|
||||
assert(person.name == person["name"], "Property/bracket equivalence - name");
|
||||
assert(config.theme == config["theme"], "Property/bracket equivalence - theme");
|
||||
print(" Property/bracket equivalence: PASS");
|
||||
|
||||
print("Dot notation and property access: PASS");
|
||||
|
||||
// ========================================
|
||||
// TEST 52.8: TAIL CALL OPTIMIZATION
|
||||
// ========================================
|
||||
print("\n--- Test 52.8: Tail Call Optimization ---");
|
||||
|
||||
// Test deep recursion that would stack overflow without TCO
|
||||
func factorial(n, acc) {
|
||||
@ -2964,58 +3043,6 @@ var testVar = 42;
|
||||
|
||||
print("Enhanced error reporting: PASS (manual verification required)");
|
||||
|
||||
// Test 62: Dictionary Creation and Access
|
||||
print("Test 62: Dictionary Creation and Access");
|
||||
var person = {"name": "Alice", "age": 30, "city": "New York"};
|
||||
assert(person["name"] == "Alice");
|
||||
assert(person["age"] == 30);
|
||||
assert(person["city"] == "New York");
|
||||
assert(person["missing"] == none);
|
||||
print("Dictionary creation and access test complete");
|
||||
|
||||
// Test 63: Dictionary Assignment
|
||||
print("Test 63: Dictionary Assignment");
|
||||
person["email"] = "alice@example.com";
|
||||
person["age"] = 31;
|
||||
assert(person["email"] == "alice@example.com");
|
||||
assert(person["age"] == 31);
|
||||
print("Dictionary assignment test complete");
|
||||
|
||||
// Test 64: Dictionary Built-in Functions
|
||||
print("Test 64: Dictionary Built-in Functions");
|
||||
var keys = keys(person);
|
||||
var values = values(person);
|
||||
assert(len(keys) == 4);
|
||||
assert(len(values) == 4);
|
||||
assert(has(person, "name") == true);
|
||||
assert(has(person, "missing") == false);
|
||||
print("Dictionary built-in functions test complete");
|
||||
|
||||
// Test 65: Nested Dictionaries
|
||||
print("Test 65: Nested Dictionaries");
|
||||
var gameState = {
|
||||
"player": {"health": 100, "level": 5},
|
||||
"world": {"name": "Bobland", "difficulty": "hard"}
|
||||
};
|
||||
assert(gameState["player"]["health"] == 100);
|
||||
assert(gameState["world"]["name"] == "Bobland");
|
||||
print("Nested dictionaries test complete");
|
||||
|
||||
// Test 66: Dictionary with Mixed Types
|
||||
print("Test 66: Dictionary with Mixed Types");
|
||||
var mixed = {
|
||||
"string": "hello",
|
||||
"number": 42,
|
||||
"boolean": true,
|
||||
"array": [1, 2, 3],
|
||||
"none": none
|
||||
};
|
||||
assert(mixed["string"] == "hello");
|
||||
assert(mixed["number"] == 42);
|
||||
assert(mixed["boolean"] == true);
|
||||
assert(mixed["array"][0] == 1);
|
||||
assert(mixed["none"] == none);
|
||||
print("Dictionary with mixed types test complete");
|
||||
|
||||
// Test 67: Copy Behavior - Primitive Types (By Value)
|
||||
print("Test 67: Copy Behavior - Primitive Types (By Value)");
|
||||
@ -3215,6 +3242,14 @@ print(" * Nested dictionaries");
|
||||
print(" * Mixed type values");
|
||||
print(" * Dictionary stress testing");
|
||||
print(" * Dictionary with none values");
|
||||
print("- Dot notation and property access");
|
||||
print(" * Dictionary property access (obj.prop)");
|
||||
print(" * Dictionary property assignment (obj.prop = value)");
|
||||
print(" * Nested property access (obj.a.b.c)");
|
||||
print(" * Array builtin properties (length, first, last, empty)");
|
||||
print(" * Dictionary builtin properties (length, empty, keys, values)");
|
||||
print(" * Property/bracket notation equivalence");
|
||||
print(" * User property precedence over builtins");
|
||||
print("- Copy behavior (by value vs by reference)");
|
||||
print(" * Primitive types copied by value (numbers, strings, booleans)");
|
||||
print(" * Complex types copied by reference (arrays, dictionaries)");
|
||||
|
||||
87
tests.bob
87
tests.bob
@ -1,38 +1,53 @@
|
||||
// var code = "var a = [];
|
||||
var a = [];
|
||||
|
||||
// for(var i = 0; i < 1000; i++){
|
||||
// push(a, func(){print(\"I is: \" + i); return (toString(i) + \": \") * 5;});
|
||||
// }
|
||||
// var total_ops = 0;
|
||||
// // while(len(a) > 0){
|
||||
// // var fun = pop(a);
|
||||
// // print(\"Function returns: \" + fun());
|
||||
// // total_ops++;
|
||||
// // }
|
||||
// print(len(a));
|
||||
// a = [];
|
||||
// print(len(a));
|
||||
// input(\"Waiting for input\");
|
||||
|
||||
// print(\"Total operations: \" + total_ops);";
|
||||
|
||||
|
||||
// print(code);
|
||||
|
||||
// eval(code);
|
||||
|
||||
// print(0xFF);
|
||||
var flag = true;
|
||||
var arr = [0];
|
||||
while(true){
|
||||
if(flag){
|
||||
arr[0]++;
|
||||
}else{
|
||||
arr[0]--;
|
||||
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);});
|
||||
}
|
||||
if(arr[0] == 0 || arr[0] == 10){
|
||||
flag = !flag;
|
||||
}
|
||||
print(arr[0]);
|
||||
sleep(0.01);
|
||||
}
|
||||
}
|
||||
|
||||
print("Before: " + len(a));
|
||||
print("Memory usage: " + memoryUsage() + " MB");
|
||||
|
||||
// Test different types of nested function calls
|
||||
a[3691](); // Simple function
|
||||
if (len(a[3692]) > 0) {
|
||||
a[3692][0](); // Nested array function
|
||||
}
|
||||
if (a[3693]["func"]) {
|
||||
a[3693]["func"](); // Nested dict function
|
||||
}
|
||||
print(a);
|
||||
//writeFile("array_contents.txt", toString(a));
|
||||
print("Array contents written to array_contents.txt");
|
||||
print("Memory before cleanup: " + memoryUsage() + " MB");
|
||||
input("Press any key to free memory");
|
||||
|
||||
a = none;
|
||||
print("Memory after cleanup: " + memoryUsage() + " MB");
|
||||
input("waiting...");
|
||||
|
||||
Loading…
Reference in New Issue
Block a user