diff --git a/CMakeLists.txt b/CMakeLists.txt index c27739f..f18a131 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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}") \ No newline at end of file +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}") \ No newline at end of file diff --git a/Reference/BOB_LANGUAGE_REFERENCE.md b/Reference/BOB_LANGUAGE_REFERENCE.md index 83125e1..d6a5696 100644 --- a/Reference/BOB_LANGUAGE_REFERENCE.md +++ b/Reference/BOB_LANGUAGE_REFERENCE.md @@ -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); ``` --- diff --git a/Reference/ROADMAP.md b/Reference/ROADMAP.md index 7de73b3..b598a3a 100644 --- a/Reference/ROADMAP.md +++ b/Reference/ROADMAP.md @@ -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 diff --git a/examples/demo_features.bob b/examples/demo_features.bob new file mode 100644 index 0000000..cf0d304 --- /dev/null +++ b/examples/demo_features.bob @@ -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!"); diff --git a/leakTests/README.md b/leakTests/README.md new file mode 100644 index 0000000..8c798d1 --- /dev/null +++ b/leakTests/README.md @@ -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. \ No newline at end of file diff --git a/leakTests/leaktest_builtin.bob b/leakTests/leaktest_builtin.bob new file mode 100644 index 0000000..f368227 --- /dev/null +++ b/leakTests/leaktest_builtin.bob @@ -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 ==="); \ No newline at end of file diff --git a/leakTests/leaktest_collections.bob b/leakTests/leaktest_collections.bob new file mode 100644 index 0000000..e39faaa --- /dev/null +++ b/leakTests/leaktest_collections.bob @@ -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 ==="); \ No newline at end of file diff --git a/leakTests/leaktest_functions.bob b/leakTests/leaktest_functions.bob new file mode 100644 index 0000000..93cafc6 --- /dev/null +++ b/leakTests/leaktest_functions.bob @@ -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 ==="); \ No newline at end of file diff --git a/leakTests/leaktest_loops.bob b/leakTests/leaktest_loops.bob new file mode 100644 index 0000000..d5b9a21 --- /dev/null +++ b/leakTests/leaktest_loops.bob @@ -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 ==="); \ No newline at end of file diff --git a/leakTests/leaktest_mixed.bob b/leakTests/leaktest_mixed.bob new file mode 100644 index 0000000..5a258b4 --- /dev/null +++ b/leakTests/leaktest_mixed.bob @@ -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 ==="); \ No newline at end of file diff --git a/run_leak_tests.sh b/run_leak_tests.sh new file mode 100755 index 0000000..f0d4210 --- /dev/null +++ b/run_leak_tests.sh @@ -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)" \ No newline at end of file diff --git a/src/headers/parsing/Expression.h b/src/headers/parsing/Expression.h index 34ebdca..021dfe7 100644 --- a/src/headers/parsing/Expression.h +++ b/src/headers/parsing/Expression.h @@ -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& expr) = 0; virtual Value visitArrayLiteralExpr(const std::shared_ptr& expr) = 0; virtual Value visitArrayIndexExpr(const std::shared_ptr& expr) = 0; + virtual Value visitPropertyExpr(const std::shared_ptr& expr) = 0; virtual Value visitArrayAssignExpr(const std::shared_ptr& expr) = 0; + virtual Value visitPropertyAssignExpr(const std::shared_ptr& expr) = 0; virtual Value visitDictLiteralExpr(const std::shared_ptr& expr) = 0; }; @@ -205,7 +209,19 @@ struct ArrayIndexExpr : Expr } }; - +struct PropertyExpr : Expr +{ + std::shared_ptr object; + Token name; + + PropertyExpr(std::shared_ptr object, Token name) + : object(object), name(name) {} + + Value accept(ExprVisitor* visitor) override + { + return visitor->visitPropertyExpr(std::static_pointer_cast(shared_from_this())); + } +}; struct ArrayAssignExpr : Expr { @@ -222,6 +238,21 @@ struct ArrayAssignExpr : Expr } }; +struct PropertyAssignExpr : Expr +{ + std::shared_ptr object; + Token name; + std::shared_ptr value; + + PropertyAssignExpr(std::shared_ptr object, Token name, std::shared_ptr value) + : object(object), name(name), value(value) {} + + Value accept(ExprVisitor* visitor) override + { + return visitor->visitPropertyAssignExpr(std::static_pointer_cast(shared_from_this())); + } +}; + struct DictLiteralExpr : Expr { std::vector>> pairs; diff --git a/src/headers/runtime/Environment.h b/src/headers/runtime/Environment.h index a430ea1..f291d19 100644 --- a/src/headers/runtime/Environment.h +++ b/src/headers/runtime/Environment.h @@ -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 getParent() const { return parent; } inline void clear() { variables.clear(); } diff --git a/src/headers/runtime/Evaluator.h b/src/headers/runtime/Evaluator.h index 7cc14ce..1e0a85d 100644 --- a/src/headers/runtime/Evaluator.h +++ b/src/headers/runtime/Evaluator.h @@ -33,6 +33,13 @@ public: Value visitTernaryExpr(const std::shared_ptr& expression) override; Value visitArrayLiteralExpr(const std::shared_ptr& expression) override; Value visitArrayIndexExpr(const std::shared_ptr& expression) override; + Value visitPropertyExpr(const std::shared_ptr& expression) override; Value visitArrayAssignExpr(const std::shared_ptr& expression) override; + Value visitPropertyAssignExpr(const std::shared_ptr& expression) override; Value visitDictLiteralExpr(const std::shared_ptr& 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); }; diff --git a/src/headers/runtime/Interpreter.h b/src/headers/runtime/Interpreter.h index 942f343..3fcbb76 100644 --- a/src/headers/runtime/Interpreter.h +++ b/src/headers/runtime/Interpreter.h @@ -64,6 +64,12 @@ private: std::vector> 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; std::unique_ptr executor; @@ -78,6 +84,7 @@ public: // Methods needed by Evaluator Value evaluate(const std::shared_ptr& expr); + Value evaluateCallExprInline(const std::shared_ptr& expression); // Inline TCO for performance void execute(const std::shared_ptr& statement, ExecutionContext* context = nullptr); void executeBlock(std::vector> statements, std::shared_ptr 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); diff --git a/src/headers/runtime/RuntimeDiagnostics.h b/src/headers/runtime/RuntimeDiagnostics.h index 471277c..e94bbd0 100644 --- a/src/headers/runtime/RuntimeDiagnostics.h +++ b/src/headers/runtime/RuntimeDiagnostics.h @@ -26,9 +26,13 @@ public: // Memory management utilities void cleanupUnusedFunctions(std::vector>& functions); + void cleanupUnusedFunctions(std::vector>& functions); void cleanupUnusedThunks(std::vector>& thunks); void forceCleanup(std::vector>& functions, std::vector>& thunks); + void forceCleanup(std::vector>& builtinFunctions, + std::vector>& functions, + std::vector>& thunks); private: // Helper methods for stringify diff --git a/src/headers/runtime/Value.h b/src/headers/runtime/Value.h index 38ebe38..91a72ad 100644 --- a/src/headers/runtime/Value.h +++ b/src/headers/runtime/Value.h @@ -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 > array_value; // Store arrays as shared_ptr for mutability std::shared_ptr > dict_value; // Store dictionaries as shared_ptr for mutability + + // Store functions as shared_ptr for proper reference counting + std::shared_ptr function; + std::shared_ptr builtin_function; + std::shared_ptr 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 f) : function(f), type(ValueType::VAL_FUNCTION) {} + Value(std::shared_ptr bf) : builtin_function(bf), type(ValueType::VAL_BUILTIN_FUNCTION) {} + Value(std::shared_ptr t) : thunk(t), type(ValueType::VAL_THUNK) {} Value(const std::vector& arr) : type(ValueType::VAL_ARRAY), array_value(std::make_shared >(arr)) {} Value(std::vector&& arr) : type(ValueType::VAL_ARRAY), array_value(std::make_shared >(std::move(arr))) {} Value(const std::unordered_map& dict) : type(ValueType::VAL_DICT), dict_value(std::make_shared >(dict)) {} Value(std::unordered_map&& dict) : type(ValueType::VAL_DICT), dict_value(std::make_shared >(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& 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 { diff --git a/src/runtime/Evaluator.cpp b/src/runtime/Evaluator.cpp index bf91bd1..f0f0b2c 100644 --- a/src/runtime/Evaluator.cpp +++ b/src/runtime/Evaluator.cpp @@ -430,5 +430,5 @@ Value Evaluator::visitFunctionExpr(const std::shared_ptr& expressi auto function = std::make_shared("", paramNames, expression->body, interpreter->getEnvironment()); interpreter->addFunction(function); - return Value(function.get()); + return Value(function); } diff --git a/src/runtime/Executor.cpp b/src/runtime/Executor.cpp index 7598c1e..5d7815d 100644 --- a/src/runtime/Executor.cpp +++ b/src/runtime/Executor.cpp @@ -63,7 +63,7 @@ void Executor::visitFunctionStmt(const std::shared_ptr& 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& statement, ExecutionContext* context) { diff --git a/src/runtime/Interpreter.cpp b/src/runtime/Interpreter.cpp deleted file mode 100644 index ac8b7a4..0000000 --- a/src/runtime/Interpreter.cpp +++ /dev/null @@ -1,96 +0,0 @@ -#include "Interpreter.h" -#include "Evaluator.h" -#include "Executor.h" -#include "BobStdLib.h" -#include - -Interpreter::Interpreter(bool isInteractive) - : isInteractive(isInteractive), errorReporter(nullptr) { - evaluator = std::make_unique(this); - executor = std::make_unique(this, evaluator.get()); - environment = std::make_shared(); -} - -Interpreter::~Interpreter() = default; - -void Interpreter::interpret(std::vector> statements) { - executor->interpret(statements); -} - -void Interpreter::execute(const std::shared_ptr& statement, ExecutionContext* context) { - statement->accept(executor.get(), context); -} - -void Interpreter::executeBlock(std::vector> statements, std::shared_ptr env, ExecutionContext* context) { - executor->executeBlock(statements, env, context); -} - -Value Interpreter::evaluate(const std::shared_ptr& 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 func) { - builtinFunctions.push_back(func); -} - -void Interpreter::addThunk(std::shared_ptr thunk) { - thunks.push_back(thunk); -} - -void Interpreter::addFunction(std::shared_ptr 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 Interpreter::getEnvironment() { - return environment; -} - -void Interpreter::setEnvironment(std::shared_ptr 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); - } -} diff --git a/src/sources/parsing/Parser.cpp b/src/sources/parsing/Parser.cpp index 88c22e9..5a9f395 100644 --- a/src/sources/parsing/Parser.cpp +++ b/src/sources/parsing/Parser.cpp @@ -138,6 +138,16 @@ sptr(Expr) Parser::assignmentExpression() auto arrayExpr = std::dynamic_pointer_cast(expr); return msptr(ArrayAssignExpr)(arrayExpr->array, arrayExpr->index, value, arrayExpr->bracket); } + else if(std::dynamic_pointer_cast(expr)) + { + auto propertyExpr = std::dynamic_pointer_cast(expr); + return msptr(PropertyAssignExpr)(propertyExpr->object, propertyExpr->name, value); + } + else if(std::dynamic_pointer_cast(expr)) + { + auto arrayExpr = std::dynamic_pointer_cast(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(expr) || + std::dynamic_pointer_cast(expr) || + std::dynamic_pointer_cast(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(); diff --git a/src/sources/runtime/Environment.cpp b/src/sources/runtime/Environment.cpp index f44d5f5..d3f122e 100644 --- a/src/sources/runtime/Environment.cpp +++ b/src/sources/runtime/Environment.cpp @@ -48,4 +48,20 @@ Value Environment::get(const std::string& name) { } throw std::runtime_error("Undefined variable '" + name + "'"); -} \ No newline at end of file +} + +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{}); + } else if (v.isDict()) { + // Replace with a new empty dict to avoid mutating original shared storage + entry.second = Value(std::unordered_map{}); + } + } + if (parent) { + parent->pruneForClosureCapture(); + } +} \ No newline at end of file diff --git a/src/sources/runtime/Evaluator.cpp b/src/sources/runtime/Evaluator.cpp index bf91bd1..b45cb2c 100644 --- a/src/sources/runtime/Evaluator.cpp +++ b/src/sources/runtime/Evaluator.cpp @@ -213,27 +213,74 @@ Value Evaluator::visitIncrementExpr(const std::shared_ptr& expres Value Evaluator::visitAssignExpr(const std::shared_ptr& 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& expression } Value Evaluator::visitCallExpr(const std::shared_ptr& expression) { - Value callee = expression->callee->accept(this); - - std::vector 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(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& expr) { @@ -364,6 +357,21 @@ Value Evaluator::visitArrayIndexExpr(const std::shared_ptr& expr } } +Value Evaluator::visitPropertyExpr(const std::shared_ptr& 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& expr) { Value array = expr->array->accept(this); Value index = expr->index->accept(this); @@ -422,13 +430,95 @@ Value Evaluator::visitDictLiteralExpr(const std::shared_ptr& ex return Value(dict); } +Value Evaluator::visitPropertyAssignExpr(const std::shared_ptr& 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& 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& expression) { std::vector paramNames; for (const Token& param : expression->params) { paramNames.push_back(param.lexeme); } - auto function = std::make_shared("", 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(*interpreter->getEnvironment()); + closureEnv->pruneForClosureCapture(); + + auto function = std::make_shared("", paramNames, expression->body, closureEnv); + return Value(function); +} + +Value Evaluator::getArrayProperty(const Value& arrayValue, const std::string& propertyName) { + const std::vector& arr = arrayValue.asArray(); + + // Create builtin array properties as an actual dictionary + std::unordered_map arrayProperties; + arrayProperties["length"] = Value(static_cast(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& 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 dictProperties; + dictProperties["length"] = Value(static_cast(dict.size())); + dictProperties["empty"] = Value(dict.empty()); + + // Create keys array + std::vector keysArray; + for (const auto& pair : dict) { + keysArray.push_back(Value(pair.first)); + } + dictProperties["keys"] = Value(keysArray); + + // Create values array + std::vector 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; } diff --git a/src/sources/runtime/Executor.cpp b/src/sources/runtime/Executor.cpp index 7598c1e..71eb015 100644 --- a/src/sources/runtime/Executor.cpp +++ b/src/sources/runtime/Executor.cpp @@ -63,7 +63,7 @@ void Executor::visitFunctionStmt(const std::shared_ptr& 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& statement, ExecutionContext* context) { @@ -194,11 +194,21 @@ void Executor::visitContinueStmt(const std::shared_ptr& statement, } void Executor::visitAssignStmt(const std::shared_ptr& 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& 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 + } } diff --git a/src/sources/runtime/Interpreter.cpp b/src/sources/runtime/Interpreter.cpp index ac8b7a4..be15274 100644 --- a/src/sources/runtime/Interpreter.cpp +++ b/src/sources/runtime/Interpreter.cpp @@ -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& expression) { + Value callee = evaluate(expression->callee); // Direct call instead of through evaluator + + if (callee.isBuiltinFunction()) { + // Handle builtin functions with direct evaluation + std::vector 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 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([this, function, arguments]() -> Value { + // Use RAII to manage environment (exactly like original) + ScopedEnv _env(environment); + environment = std::make_shared(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(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 +} diff --git a/src/sources/runtime/RuntimeDiagnostics.cpp b/src/sources/runtime/RuntimeDiagnostics.cpp index 1ce17e6..c4ee001 100644 --- a/src/sources/runtime/RuntimeDiagnostics.cpp +++ b/src/sources/runtime/RuntimeDiagnostics.cpp @@ -2,11 +2,17 @@ #include "Value.h" #include "TypeWrapper.h" // For Function and BuiltinFunction definitions #include +#if defined(__linux__) +#include +#elif defined(__APPLE__) +#include +#endif #include #include #include #include + bool RuntimeDiagnostics::isTruthy(Value object) { if(object.isBoolean()) { return object.asBoolean(); @@ -182,6 +188,18 @@ void RuntimeDiagnostics::cleanupUnusedFunctions(std::vector>& 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& func) { + return func.use_count() == 1; // Only referenced by this vector, nowhere else + }), + functions.end() + ); +} + void RuntimeDiagnostics::cleanupUnusedThunks(std::vector>& 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>& builtinFunctions, + std::vector>& functions, + std::vector>& 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& 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& 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) { + 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 + } } \ No newline at end of file diff --git a/src/sources/stdlib/BobStdLib.cpp b/src/sources/stdlib/BobStdLib.cpp index 9dd9662..c93a12f 100644 --- a/src/sources/stdlib/BobStdLib.cpp +++ b/src/sources/stdlib/BobStdLib.cpp @@ -9,6 +9,16 @@ #include #include +// Platform-specific includes for memory usage +#if defined(__APPLE__) && defined(__MACH__) + #include +#elif defined(__linux__) + // Uses /proc/self/status, no extra includes needed +#elif defined(_WIN32) + #include + #include +#endif + void BobStdLib::addToEnvironment(std::shared_ptr env, Interpreter& interpreter, ErrorReporter* errorReporter) { // Create a built-in toString function auto toStringFunc = std::make_shared("toString", @@ -23,7 +33,7 @@ void BobStdLib::addToEnvironment(std::shared_ptr 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 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 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 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 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 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 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 env, Interpreter& return Value(static_cast(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 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 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 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 env, Interpreter& double value = args[0].asNumber(); return Value(static_cast(static_cast(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 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 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 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 env, Interpreter& return Value(static_cast(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 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 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 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 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 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 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 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 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("memoryUsage", + [errorReporter](std::vector args, int line, int column) -> Value { + if (args.size() != 0) { + if (errorReporter) { + errorReporter->reportError(line, column, "StdLib Error", + "Expected 0 arguments but got " + std::to_string(args.size()) + ".", "", true); + } + throw std::runtime_error("Expected 0 arguments but got " + std::to_string(args.size()) + "."); + } + + // 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(memoryBytes) / (1024.0 * 1024.0); + return Value(memoryMB); + }); + env->define("memoryUsage", Value(memoryUsageFunc)); + interpreter.addBuiltinFunction(memoryUsageFunc); + } \ No newline at end of file diff --git a/test_bob_language.bob b/test_bob_language.bob index d85e09b..1000366 100644 --- a/test_bob_language.bob +++ b/test_bob_language.bob @@ -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)"); diff --git a/tests.bob b/tests.bob index a87c751..8f2211a 100644 --- a/tests.bob +++ b/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); -} \ No newline at end of file +} + +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...");