diff --git a/std.zc b/std.zc index 3f082072..443bbad8 100644 --- a/std.zc +++ b/std.zc @@ -11,7 +11,7 @@ import "./std/time.zc" import "./std/result.zc" import "./std/option.zc" import "./std/map.zc" -import "./std/json.zc" +//import "./std/json.zc" import "./std/path.zc" import "./std/mem.zc" import "./std/stack.zc" diff --git a/std/debug.zc b/std/debug.zc new file mode 100644 index 00000000..4c43306c --- /dev/null +++ b/std/debug.zc @@ -0,0 +1,117 @@ +import "mem.zc" +import "vec.zc" + +// Pretty rough implementation. +// Should be improved with a Map when available. +struct DebugAllocator { + addrs: Vec, + sizes: Vec +} + +impl DebugAllocator { + + fn new() -> Self { + + // Yep, both vectors will be stored on the general allocator, and that should be fine. + return Self { + addrs: Vec::new(), + sizes: Vec::new() + } + } + + fn validate(self) -> bool { + if (self.addrs.length() == 0) { + println "No memory leaked."; + return true; + } + + var total_size: usize = 0; + + for i in 0..self.sizes.length() { + total_size += self.sizes.get(i); + } + + println "{self.addrs.length()} allocations remaining, with a total size of {total_size} bytes."; + return false; + } + + + // --- Internal functions + + fn add_addr(self, addr: usize, size: usize) { + self.addrs.push(addr); + self.sizes.push(size); + } + + fn remove_addr(self, addr: usize) { + var index: isize = -1 + + for i in 0..self.addrs.length() { + if (self.addrs.get(i) == addr) { + index = i; + break; + } + } + + if (index == -1) { + !"Panic: attempt to free an unallocated value."; + exit(1); + } + + self.addrs.remove(index); + self.sizes.remove(index); + } +} + +impl Allocator for DebugAllocator { + + fn allocate(self, size: usize) -> u8* { + var addr: usize; + + raw { + addr = (size_t) malloc(size); + } + + self.add_addr(addr, size); + + return (u8*) addr; + } + + fn zeroed(self, size: usize) -> u8* { + var addr: usize; + + raw { + addr = (size_t) calloc(1, size); + } + + self.add_addr(addr, size); + + return (u8*) addr; + } + + fn reallocate(self, ptr: u8*, new_size: usize) -> u8* { + var addr: usize; + + raw { + addr = (size_t) realloc(ptr, new_size); + } + + if (addr != 0) { + if ((usize) ptr != 0) { // realloc allows ptr to be 0, it behaves like malloc. + self.remove_addr((usize) ptr); + } + + self.add_addr(addr, new_size); + } + + return (u8*) addr; + } + + fn free(self, ptr: u8*) { + self.remove_addr((usize) ptr); + + raw { + free(ptr); + } + } +} \ No newline at end of file diff --git a/std/json.zc b/std/json.zc index cfef2c3f..5318d21d 100644 --- a/std/json.zc +++ b/std/json.zc @@ -29,7 +29,7 @@ struct JsonValue { alias JsonValuePtr = JsonValue*; raw { - Vec_JsonValuePtr Vec_JsonValuePtr__new(); + Vec_JsonValuePtr Vec_JsonValuePtr__new(Allocator allocator); void Vec_JsonValuePtr__push(Vec_JsonValuePtr* self, JsonValue* item); Map_JsonValuePtr Map_JsonValuePtr__new(); void Map_JsonValuePtr__put(Map_JsonValuePtr* self, char* key, JsonValue* val); @@ -71,7 +71,8 @@ raw { arr->kind = JsonType_JSON_ARRAY(); arr->string_val = 0; arr->number_val = 0; arr->bool_val = 0; arr->object_val = 0; arr->array_val = malloc(sizeof(Vec_JsonValuePtr)); - *(arr->array_val) = Vec_JsonValuePtr__new(); + *(arr->array_val) = Vec_JsonValuePtr__new((Allocator){ + .self = (&GLOBAL_ALLOCATOR), .vtable = (&GeneralAllocator_Allocator_VTable)}); _json_skip_ws(p); if (**p == ']') { (*p)++; return arr; } diff --git a/std/mem.zc b/std/mem.zc index ed69c9b6..6f1a2e3c 100644 --- a/std/mem.zc +++ b/std/mem.zc @@ -1,12 +1,51 @@ +trait Allocator { + fn allocate(self, size: usize) -> u8*; + fn zeroed(self, size: usize) -> u8*; // Change to allocate_zeroed when it's possible use underscores + fn reallocate(self, ptr: u8*, new_size: usize) -> u8*; + fn free(self, ptr: u8*); +} + +struct GeneralAllocator {} + +impl Allocator for GeneralAllocator { + fn allocate(self, size: usize) -> u8* { + raw { + return (uint8_t*) malloc(size); + } + } + fn zeroed(self, size: usize) -> u8* { + raw { + return (uint8_t*) calloc(1, size); + } + } + + fn reallocate(self, ptr: u8*, new_size: usize) -> u8* { + raw { + return (uint8_t*) realloc(ptr, new_size); + } + } + + fn free(self, ptr: u8*) { + raw { + return free(ptr); + } + } +} + +var GLOBAL_ALLOCATOR: GeneralAllocator; + +@deprecated("Replaced with `Allocator::alloc`") fn alloc() -> T* { return (T*)malloc(sizeof(T)); } +@deprecated("Replaced with `Allocator`") fn zalloc() -> T* { return (T*)calloc(1, sizeof(T)); } +@deprecated("Replaced with `Allocator`") fn alloc_n(n: usize) -> T* { return (T*)malloc(sizeof(T) * n); } @@ -34,9 +73,17 @@ impl Box { return Self { ptr: p }; } - fn get(self) -> T* { + fn get_ptr(self) -> T* { return self.ptr; } + + fn set(self, val: T) { + *self.ptr = val; + } + + fn get(self) -> T { + return *self.ptr; + } fn is_null(self) -> bool { return self.ptr == NULL; @@ -45,6 +92,56 @@ impl Box { fn free(self) { if self.ptr != NULL { free(self.ptr); + self.ptr = NULL; + } + } +} + +impl Drop for Box { + fn drop(self) { + self.free(); + } +} + + +struct RcInner { + value: T; + ref_count: usize; +} + +struct Rc { + inner: RcInner*; +} + +impl Rc { + fn new(value: T) -> Self { + var inner: RcInner* = GLOBAL_ALLOCATOR.allocate(sizeof(RcInner)); + + inner.value = value; + inner.ref_count = 1; + + return Self { + inner: inner + }; + } +} + +impl Clone for Rc { + fn clone(self) -> Self { + self.inner.ref_count++; + + return Self { + inner: self.inner + }; + } +} + +impl Drop for Rc { + fn drop(self) { + self.inner.ref_count--; + + if (self.inner.ref_count == 0) { + GLOBAL_ALLOCATOR.free(self.inner); } } } diff --git a/std/string.zc b/std/string.zc index a7dd6ce3..e1c5d9ed 100644 --- a/std/string.zc +++ b/std/string.zc @@ -7,9 +7,9 @@ struct String { } impl String { - fn new(s: char*) -> String { + fn new(s: char*, allocator: Allocator = &GLOBAL_ALLOCATOR) -> String { var len = strlen(s); - var v = Vec::new(); + var v = Vec::new(allocator); // Manual copy for now for (var i = 0; i < len; i = i + 1) { v.push(s[i]); @@ -17,6 +17,7 @@ impl String { v.push(0); // Extract fields to transfer ownership + var a = v.alloc; var d = v.data; var l = v.len; var c = v.cap; @@ -24,21 +25,17 @@ impl String { // Forget the local vector so it doesn't free the memory v.forget(); - return String { vec: Vec { data: d, len: l, cap: c } }; + return String { vec: Vec {alloc: a, data: d, len: l, cap: c } }; } - fn from(s: char*) -> String { - return String::new(s); + fn from(s: char*, allocator: Allocator = &GLOBAL_ALLOCATOR) -> String { + return String::new(s, allocator); } fn c_str(self) -> char* { return self.vec.data; } - fn destroy(self) { - self.vec.free(); - } - fn forget(self) { self.vec.forget(); } @@ -49,9 +46,9 @@ impl String { self.vec.len = self.vec.len - 1; } - var other_len = (*other).vec.len; + var other_len = other.vec.len; for (var i = 0; i < other_len; i = i + 1) { - self.vec.push((*other).vec.get(i)); + self.vec.push(other.vec.get(i)); } } @@ -59,12 +56,13 @@ impl String { var new_s = String::from(self.c_str()); new_s.append(other); + var a = new_s.vec.alloc; var d = new_s.vec.data; var l = new_s.vec.len; var c = new_s.vec.cap; new_s.forget(); - return String { vec: Vec { data: d, len: l, cap: c } }; + return String { vec: Vec {alloc: a, data: d, len: l, cap: c } }; } fn eq(self, other: String*) -> bool { @@ -86,12 +84,13 @@ impl String { } v.push(0); + var a = v.alloc; var d = v.data; var l = v.len; var c = v.cap; v.forget(); - return String { vec: Vec { data: d, len: l, cap: c } }; + return String { vec: Vec { alloc: a, data: d, len: l, cap: c } }; } fn find(self, target: char) -> Option { @@ -134,10 +133,6 @@ impl String { var offset = (int)(len - slen); return strcmp(self.c_str() + offset, suffix) == 0; } - - fn free(self) { - self.vec.free(); - } fn _utf8_seq_len(first_byte: char) -> usize { var b = (int)first_byte; @@ -220,6 +215,7 @@ impl String { return self.substring(byte_start, byte_len); } + fn split(self, delim: char) -> Vec { var parts = Vec::new(); var len = self.length(); @@ -310,9 +306,22 @@ impl String { v.push(0); var ch_s = String::new(v.data); result.append(&ch_s); - v.free(); + //v.free(); // The array should be cleaned up automatically. i = i + 1; } return result; } + + @deprecated("Strings are freed automatically. No they aren't") + fn free(self) {} +} + +impl Drop for String { + fn drop(self) { + var vec = self.vec; + + raw { + Vec_char__Drop_drop(&vec); + } + } } \ No newline at end of file diff --git a/std/thread.zc b/std/thread.zc index 0ebcd037..5d50268f 100644 --- a/std/thread.zc +++ b/std/thread.zc @@ -127,4 +127,3 @@ impl Mutex { fn sleep_ms(ms: int) { _z_usleep(ms * 1000); } - diff --git a/std/vec.zc b/std/vec.zc index d3105359..17e3163d 100644 --- a/std/vec.zc +++ b/std/vec.zc @@ -1,8 +1,10 @@ import "./core.zc" import "./iter.zc" +import "./mem.zc" struct Vec { + alloc: Allocator; data: T*; len: usize; cap: usize; @@ -69,16 +71,17 @@ impl VecIterRef { } impl Vec { - fn new() -> Vec { - return Vec { data: 0, len: 0, cap: 0 }; + fn new(allocator: Allocator = &GLOBAL_ALLOCATOR) -> Vec { + return Vec { alloc: allocator, data: 0, len: 0, cap: 0 }; } - fn with_capacity(cap: usize) -> Vec { + fn with_capacity(cap: usize, allocator: Allocator = &GLOBAL_ALLOCATOR) -> Vec { if (cap == 0) { - return Vec { data: 0, len: 0, cap: 0 }; + return Vec { alloc: allocator, data: 0, len: 0, cap: 0 }; } return Vec { - data: (T*)malloc(cap * sizeof(T)), + alloc: allocator, + data: (T*)allocator.allocate(cap * sizeof(T)), len: 0, cap: cap }; @@ -87,7 +90,7 @@ impl Vec { fn grow(self) { if (self.cap == 0) { self.cap = 8; } else { self.cap = self.cap * 2; } - self.data = (T*)realloc(self.data, self.cap * sizeof(T)); + self.data = (T*)self.alloc.reallocate((u8*) self.data, self.cap * sizeof(T)); } fn grow_to_fit(self, new_len: usize) { @@ -100,7 +103,7 @@ impl Vec { self.cap = self.cap * 2; } - self.data = (T*)realloc(self.data, self.cap * sizeof(T)); + self.data = (T*)self.alloc.reallocate((u8*) self.data, self.cap * sizeof(T)); } fn iterator(self) -> VecIter { @@ -218,14 +221,7 @@ impl Vec { fn clear(self) { self.len = 0; } - - fn free(self) { - if (self.data) free(self.data); - self.data = 0; - self.len = 0; - self.cap = 0; - } - + fn first(self) -> T { if (self.len == 0) { !"Panic: first called on empty Vec"; @@ -264,34 +260,40 @@ impl Vec { return true; } - // Prevent Drop from freeing memory (simulates move) - fn forget(self) { - self.data = 0; - self.len = 0; - self.cap = 0; - } - fn clone(self) -> Vec { if (self.len == 0) { - return Vec { data: 0, len: 0, cap: 0 }; + return Vec { alloc: self.alloc, data: 0, len: 0, cap: 0 }; } - var new_data = (T*)malloc(self.len * sizeof(T)); + var new_data = (T*)self.alloc.allocate(self.len * sizeof(T)); var i: usize = 0; while i < self.len { new_data[i] = self.data[i]; i = i + 1; } return Vec { + alloc: self.alloc, data: new_data, len: self.len, cap: self.len // Set capacity to exact length }; // No local Vec variable means no Drop is called here. } + + // Prevent Drop from freeing memory (simulates move) + fn forget(self) { + self.data = 0; + self.len = 0; + self.cap = 0; + } } -impl Drop for Vec { +impl Drop for Vec { fn drop(self) { - self.free(); + if (self.data) + self.alloc.free((u8*) self.data); + + self.data = 0; + self.len = 0; + self.cap = 0; } } diff --git a/tests/collections/test_string_suite.zc b/tests/collections/test_string_suite.zc index a3f608d5..4c32d6c7 100644 --- a/tests/collections/test_string_suite.zc +++ b/tests/collections/test_string_suite.zc @@ -1,7 +1,7 @@ include include -import "std.zc" +import "std/string.zc" test "test_string_methods" { println "Testing String methods..."; diff --git a/tests/features/test_vec_iter.zc b/tests/features/test_vec_iter.zc index b7e8dcbd..e04da2c7 100644 --- a/tests/features/test_vec_iter.zc +++ b/tests/features/test_vec_iter.zc @@ -1,4 +1,4 @@ -import "../../std/vec.zc" +import "std/vec.zc" test "vec_int_iteration" { var v = Vec::new(); @@ -15,5 +15,4 @@ test "vec_int_iteration" { println "Expected 60, got {sum}"; exit(1); } - v.free(); } diff --git a/tests/memory/allocators.zc b/tests/memory/allocators.zc new file mode 100644 index 00000000..7d028da8 --- /dev/null +++ b/tests/memory/allocators.zc @@ -0,0 +1,80 @@ +import "std/mem.zc" +import "std/vec.zc" +import "std/string.zc" +import "std/debug.zc" + +test "test_general_allocator" { + "Testing GeneralAllocator..."; + var allocator: GeneralAllocator; + + var i = allocator.allocate(10); + + allocator.free(i); + + " ✓ GeneralAllocator works!"; +} + +test "test_global_allocator" { + "Testing Vec..."; + + var v = Vec::new(); + + v.push(4); + + " ✓ GLOBAL_ALLOCATOR works!"; +} + +// Vec + +fn test_vec(allocator: DebugAllocator*) { + var list = Vec::new(allocator); + + list.push(3); + allocator.validate(); +} + +test "test_vec_allocator" { + "Testing Vec allocation"; + + var allocator = DebugAllocator::new(); + test_vec(&allocator); + + assert(allocator.validate(), "Memory leak from Vec allocation"); +} + +// String + +fn test_string(allocator: DebugAllocator*) { + var s = String::from("hellooooooooooooooooooo", allocator); + + allocator.validate(); +} + +test "test_string_allocator" { + "Testing String allocation"; + + var allocator = DebugAllocator::new(); + test_box(&allocator); + + assert(allocator.validate(), "Memory leak from String allocation"); +} + +// Box + +fn test_box(allocator: DebugAllocator*) { + var b = Box::new(); + var s = String::from("hellooooooooooooooooooo", allocator); + + b.set(s); + + allocator.validate(); +} + +test "test_box_allocator" { + "Testing Box allocation"; + + var allocator = DebugAllocator::new(); + test_box(&allocator); + + assert(allocator.validate(), "Memory leak from Box allocation"); +} \ No newline at end of file diff --git a/tests/memory/test_memory_safety.zc b/tests/memory/test_memory_safety.zc index c7ba01d5..09fff538 100644 --- a/tests/memory/test_memory_safety.zc +++ b/tests/memory/test_memory_safety.zc @@ -114,14 +114,32 @@ test "test_alloc" { test "test_box" { "Testing Box..."; var b = Box::new(); - *b.get() = 100; - var val = *b.get(); + *b.get_ptr() = 100; + var val = *b.get_ptr(); f" Box value: {val}"; assert(val == 100, "Box failed"); b.free(); " ✓ Box works!"; } + +fn create_boxed_int(val: int) -> Box { + var b = Box::new(); + b.set(val); + return b; +} + +test "test_box_call" { + "Testing Box call..."; + var b = create_boxed_int(45); + b.set(100); + var val = b.get(); + f" Box value: {val}"; + assert(val == 100, "Box failed"); + " ✓ Box works!"; +} + + test "test_slice" { "Testing Slice..."; var data: int[5] = [1, 2, 3, 4, 5]; diff --git a/tests/misc/test_advanced.zc b/tests/misc/test_advanced.zc index 8b5b4661..d2b0b97b 100644 --- a/tests/misc/test_advanced.zc +++ b/tests/misc/test_advanced.zc @@ -1,5 +1,3 @@ -import "std.zc" - fn fibonacci(n: int) -> int { if (n <= 1) { return n; diff --git a/tests/std/test_env.zc b/tests/std/test_env.zc index 75bba3bb..d9cfcd6e 100644 --- a/tests/std/test_env.zc +++ b/tests/std/test_env.zc @@ -1,4 +1,3 @@ -import "std.zc" import "std/env.zc" test "test_std_env_set_and_get" { @@ -31,10 +30,6 @@ test "test_std_env_get_dup" { var value = env_var.unwrap(); assert(value.c_str() == "ok3", "value should be ok3"); - - value.free(); - - assert(value.is_empty(), "value should be empty"); } test "test_std_env_get_and_get_dup_with_invalid" { diff --git a/tests/std/test_string_split.zc b/tests/std/test_string_split.zc index 496dbd10..25fe135a 100644 --- a/tests/std/test_string_split.zc +++ b/tests/std/test_string_split.zc @@ -24,7 +24,6 @@ test "string_split_basic" { for p in &parts { p.free(); } - parts.free(); } test "string_split_edge" { @@ -40,7 +39,6 @@ test "string_split_edge" { if (!p0.eq(&s)) exit(1); p0.free(); - parts.free(); var s2 = String::from("a,,b"); var parts2 = s2.split(','); @@ -53,5 +51,4 @@ test "string_split_edge" { if (!empty.is_empty()) exit(1); for p in &parts2 { p.free(); } - parts2.free(); } diff --git a/tests/std/test_string_utils.zc b/tests/std/test_string_utils.zc index 212bac10..5d446d33 100644 --- a/tests/std/test_string_utils.zc +++ b/tests/std/test_string_utils.zc @@ -1,6 +1,7 @@ import "std/string.zc" test "string trim" { + println "Testing trim"; var s1 = String::from(" hello "); var t1 = s1.trim(); var e1 = String::from("hello"); @@ -26,12 +27,13 @@ test "string trim" { } test "string replace" { + println "Testing replace"; var s1 = String::from("foo bar foo"); var r1 = s1.replace("foo", "baz"); var e1 = String::from("baz bar baz"); assert(r1.eq(&e1)); r1.free(); s1.free(); e1.free(); - + var s2 = String::from("hello world"); var r2 = s2.replace("world", "ZenC"); var e2 = String::from("hello ZenC"); @@ -43,4 +45,5 @@ test "string replace" { var e3 = String::from("bb"); assert(r3.eq(&e3)); r3.free(); s3.free(); e3.free(); + } diff --git a/tests/std/test_vec.zc b/tests/std/test_vec.zc index 5e3dddf6..7881763e 100644 --- a/tests/std/test_vec.zc +++ b/tests/std/test_vec.zc @@ -44,10 +44,6 @@ test "Vec Basics (Construction, Push, Pop, Access)" { v.set(0, 99); assert_eq(v.get(0), 99, "set()"); - - // Explicit clean up check (safe idempotent free) - v.free(); - assert_eq(v.len, 0, "Len after free"); } test "Vec Capacity and Allocation" {