From 6dd6b7ff54afd630a79249724eb3f8d15c1ce880 Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Sat, 11 Jul 2026 13:42:55 +0200 Subject: [PATCH] add realloc (and update c_allocator) --- LANGUAGE.md | 3 +- TODO.md | 8 ++- compiler_tests.odin | 4 ++ examples/programs/mem_allocator/main.bro | 72 ++++++++++++++++++++++++ ffi/c/stdlib.bro | 1 + std/mem/mem.bro | 39 +++++++++++++ 6 files changed, 122 insertions(+), 5 deletions(-) diff --git a/LANGUAGE.md b/LANGUAGE.md index 00a9635..2905f34 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -72,8 +72,7 @@ roadmap and milestone history. ### standard packages -- `std/mem` allocator contract over byte allocation: `Allocator` with `?*mut anyopaque` context, `heap`, `alloc(allocator, size, alignment) ?*mut u8`, and `free(allocator, ptr, size, alignment)` -- `std/mem/heap` legacy compatibility wrappers: `alloc(size usize) ?*mut u8` and `free(ptr ?*mut u8)` +- `std/mem` allocator contract over byte allocation: `Allocator` with `?*mut anyopaque` context plus `alloc`, `realloc`, and `free`; failed nonzero reallocation preserves the original allocation, while zero size frees it ### compiler behavior diff --git a/TODO.md b/TODO.md index c57b4d4..34e9644 100644 --- a/TODO.md +++ b/TODO.md @@ -627,9 +627,10 @@ parameters 25. dynamic heap allocation (implemented; v1) - - `std/mem` exposes a plain-data `Allocator` contract with `?*mut anyopaque` context, `alloc`, and `free` `@func` pointers + - `std/mem` exposes a plain-data `Allocator` contract with `?*mut anyopaque` context and `alloc`, `realloc`, and `free` `@func` pointers - `mem.c_allocator` is the libc-backed allocator; `mem.alloc(mem.c_allocator, size, alignment)` returns nullable mutable byte memory - `mem.free(mem.c_allocator, ptr, size, alignment)` frees with the same allocator; `malloc` handles default-aligned requests and `posix_memalign` handles larger power-of-two alignments + - `mem.realloc` preserves alignment and the original allocation on failure; zero size frees, and over-aligned blocks use allocate/copy/free - typed allocation helpers, arenas/pools, build-mode heap policy, and escaping-allocation diagnostics remain deferred 26. import from project "root" (implemented) @@ -718,7 +719,7 @@ - native function pointers are non-variadic v1; C variadic function pointers stay under `*c_func(...) R` -28. brolang build system — v0 shipped +28. brolang build system (implemented; v0 shipped) - `brolang build [root]` reads a declarative `config` constant from `root/build.bro` (importing `@std/build`'s `BuildConfig`) and compiles the program package it names. The config is read from the checked HIR — build.bro @@ -1397,7 +1398,7 @@ Memory allocation in Brolang is designed to be **explicit but not verbose**. We Brolang provides a libc-backed allocator value: * Available as `mem.c_allocator` -* Passed explicitly to `mem.alloc` and `mem.free` +* Passed explicitly to `mem.alloc`, `mem.realloc`, and `mem.free` * **Immutable at runtime** — cannot be reconfigured ``` @@ -1595,6 +1596,7 @@ main func() void { | What | How | When to Use | | -- | -- | -- | | `mem.alloc(mem.c_allocator, n, a)` | Libc-backed allocator | General purpose byte allocation | +| `mem.realloc(mem.c_allocator, ptr, old_n, new_n, a)` | Libc-backed allocator | Resize while preserving alignment and up to `min(old_n, new_n)` bytes | | `mem.free(mem.c_allocator, ptr, n, a)` | Libc-backed allocator | Free byte allocation with original size/alignment | | `mem.alloc(allocator, n, a)` | Caller-provided allocator | Escaping allocations (returned or written to caller's data) | | typed helpers / arenas / pools | Future APIs | Higher-level allocation patterns | diff --git a/compiler_tests.odin b/compiler_tests.odin index 4b2c691..ac750e8 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -3282,6 +3282,7 @@ allocator_contract_c_allocator_global_lowers :: proc(t: ^testing.T) { found_c_allocator := false found_anyopaque_context := false found_alloc_callback := false + found_realloc_callback := false found_free_callback := false for global in hir_module.globals { if symbol.resolve(&symbols, global.name) != "c_allocator" { @@ -3303,6 +3304,7 @@ allocator_contract_c_allocator_global_lowers :: proc(t: ^testing.T) { } } found_alloc_callback = found_alloc_callback || name == "alloc" && callable && !callback_pointer.many + found_realloc_callback = found_realloc_callback || name == "realloc" && callable && !callback_pointer.many found_free_callback = found_free_callback || name == "free" && callable && !callback_pointer.many } } @@ -3312,6 +3314,7 @@ allocator_contract_c_allocator_global_lowers :: proc(t: ^testing.T) { testing.expect(t, found_c_allocator) testing.expect(t, found_anyopaque_context) testing.expect(t, found_alloc_callback) + testing.expect(t, found_realloc_callback) testing.expect(t, found_free_callback) } @@ -3346,6 +3349,7 @@ milestone_25_c_allocator_emits_libc_alloc_declarations :: proc(t: ^testing.T) { testing.expect_value(t, len(diagnostics.items), 0) testing.expect(t, strings.contains(llvm_text, "declare ptr @malloc(i64)")) + testing.expect(t, strings.contains(llvm_text, "declare ptr @realloc(ptr, i64)")) testing.expect(t, strings.contains(llvm_text, "declare i32 @posix_memalign(ptr, i64, i64)")) testing.expect(t, strings.contains(llvm_text, "declare void @free(ptr)")) } diff --git a/examples/programs/mem_allocator/main.bro b/examples/programs/mem_allocator/main.bro index ca51099..efb65f8 100644 --- a/examples/programs/mem_allocator/main.bro +++ b/examples/programs/mem_allocator/main.bro @@ -150,6 +150,78 @@ task_list_deinit func(list @mut TaskList) void { } main func() i32 { + resized ?*mut u8 = mem.realloc(mem.c_allocator, none, 0, 4, 1) + if resized |bytes| { + bytes[0] = 10 + bytes[1] = 20 + bytes[2] = 30 + bytes[3] = 40 + } else { + return 20 + } + + grown ?*mut u8 = mem.realloc(mem.c_allocator, resized, 4, 8, 1) + if grown |bytes| { + resized = grown + if bytes[0] != 10 or bytes[1] != 20 or bytes[2] != 30 or bytes[3] != 40 { + mem.free(mem.c_allocator, grown, 8, 1) + return 21 + } + } else { + mem.free(mem.c_allocator, resized, 4, 1) + return 22 + } + + shrunk ?*mut u8 = mem.realloc(mem.c_allocator, resized, 8, 2, 1) + if shrunk |bytes| { + resized = shrunk + if bytes[0] != 10 or bytes[1] != 20 { + mem.free(mem.c_allocator, shrunk, 2, 1) + return 23 + } + } else { + mem.free(mem.c_allocator, resized, 8, 1) + return 24 + } + + invalid ?*mut u8 = mem.realloc(mem.c_allocator, resized, 2, 4, 24) + if invalid |memory| { + mem.free(mem.c_allocator, memory, 4, 24) + mem.free(mem.c_allocator, resized, 2, 1) + return 25 + } + if resized |bytes| { + if bytes[0] != 10 or bytes[1] != 20 { + mem.free(mem.c_allocator, resized, 2, 1) + return 26 + } + } + + resized = mem.realloc(mem.c_allocator, resized, 2, 0, 1) + if resized |memory| { + mem.free(mem.c_allocator, memory, 0, 1) + return 27 + } + + over_aligned ?*mut u8 = mem.alloc(mem.c_allocator, 4, 32) + if over_aligned |bytes| { + bytes[0] = 11 + bytes[1] = 22 + } else { + return 28 + } + over_aligned_grown ?*mut u8 = mem.realloc(mem.c_allocator, over_aligned, 4, 8, 32) + if over_aligned_grown |bytes| { + if bytes[0] != 11 or bytes[1] != 22 { + mem.free(mem.c_allocator, over_aligned_grown, 8, 32) + return 29 + } + mem.free(mem.c_allocator, over_aligned_grown, 8, 32) + } else { + mem.free(mem.c_allocator, over_aligned, 4, 32) + return 30 + } + zero_alignment ?*mut u8 = mem.alloc(mem.c_allocator, 8, 0) if zero_alignment |memory| { mem.free(mem.c_allocator, memory, 8, 0) diff --git a/ffi/c/stdlib.bro b/ffi/c/stdlib.bro index c343c57..c2a0b3b 100644 --- a/ffi/c/stdlib.bro +++ b/ffi/c/stdlib.bro @@ -1,3 +1,4 @@ malloc c_func(__size c_ulong) ?*mut anyopaque +realloc c_func(__ptr ?*mut anyopaque, __size c_ulong) ?*mut anyopaque free c_func(_ ?*mut anyopaque) void posix_memalign c_func(__memptr ?*mut ?*mut anyopaque, __alignment c_ulong, __size c_ulong) c_int diff --git a/std/mem/mem.bro b/std/mem/mem.bro index 0037bec..167676e 100644 --- a/std/mem/mem.bro +++ b/std/mem/mem.bro @@ -3,6 +3,7 @@ c :: import "@ffi/c" Allocator :: struct { context ?*mut anyopaque alloc @func(context ?*mut anyopaque, size usize, alignment usize) ?*mut u8 + realloc @func(context ?*mut anyopaque, memory ?*mut u8, old_size usize, new_size usize, alignment usize) ?*mut u8 free @func(context ?*mut anyopaque, memory ?*mut u8, size usize, alignment usize) void } @@ -43,6 +44,39 @@ _c_alloc func(_ ?*mut anyopaque, size usize, alignment usize) ?*mut u8 { return ptr_cast(u8, memory[0]) } +_c_realloc func(_ ?*mut anyopaque, memory ?*mut u8, old_size usize, new_size usize, alignment usize) ?*mut u8 { + if _power_of_two(alignment) == false { + return none + } + + if new_size == 0 { + c.free(memory) + return none + } + + if memory |old_memory| { + if alignment <= _malloc_alignment { + return ptr_cast(u8, c.realloc(old_memory, c_ulong(new_size))) + } + + new_memory ?*mut u8 = _c_alloc(none, new_size, alignment) + if new_memory |new_bytes| { + copy_size usize = old_size + if new_size < copy_size { + copy_size = new_size + } + i usize = 0 + while i < copy_size : i += 1 { + new_bytes[i] = old_memory[i] + } + c.free(old_memory) + } + return new_memory + } + + return _c_alloc(none, new_size, alignment) +} + _c_free func(_ ?*mut anyopaque, memory ?*mut u8, _ usize, _ usize) void { c.free(memory) } @@ -50,6 +84,7 @@ _c_free func(_ ?*mut anyopaque, memory ?*mut u8, _ usize, _ usize) void { c_allocator Allocator :: Allocator { context = none, alloc = _c_alloc, + realloc = _c_realloc, free = _c_free, } @@ -57,6 +92,10 @@ alloc func(allocator Allocator, size usize, alignment usize) ?*mut u8 { return allocator.alloc(allocator.context, size, alignment) } +realloc func(allocator Allocator, memory ?*mut u8, old_size usize, new_size usize, alignment usize) ?*mut u8 { + return allocator.realloc(allocator.context, memory, old_size, new_size, alignment) +} + free func(allocator Allocator, memory ?*mut u8, size usize, alignment usize) void { allocator.free(allocator.context, memory, size, alignment) }