allocation related primitives

This commit is contained in:
2026-07-08 20:29:25 +02:00
parent 5e18df9bc1
commit 2cda024614
11 changed files with 617 additions and 98 deletions
+64 -41
View File
@@ -628,9 +628,8 @@
25. dynamic heap allocation (implemented; v1)
- `std/mem` exposes a plain-data `Allocator` contract with `?*mut anyopaque` context, `alloc`, and `free` `@func` pointers
- `mem.heap` is the default libc-backed allocator; `mem.alloc(mem.heap, size, alignment)` returns nullable mutable byte memory
- `mem.free(mem.heap, ptr, size, alignment)` frees with the same allocator; size/alignment are part of the contract, though libc ignores them in v1
- `std/mem/heap` remains as legacy compatibility wrappers over `std/mem`
- `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
- typed allocation helpers, arenas/pools, build-mode heap policy, and escaping-allocation diagnostics remain deferred
26. import from project "root" (implemented)
@@ -732,6 +731,40 @@
- deferred: build graph / steps / caching, multiple artifacts, computed paths
(needs string building), struct field defaults to drop `&[]` on empty lists
29. disallow arbitrary integer division
- take inspiration from zig
- see also below for a word on unchecked casts
- the user should be explicit about what they mean with integer division (e.g. `div`, `rem`)
## A word on unchecked casts
For casts that bypass safety checks, Honey provides builtin functions:
| Builtin | Purpose | Traps when... |
| -- | -- | -- |
| `truncate(x, T)` | Keep low bits, discard rest | Never |
| `bitcast(x, T)` | Reinterpret bits, no cast | Sizes don't match (compile error) |
| `ptrcast(p, T)` | Change pointer type | Gaining mutability (compile error) |
```honey
# truncation
a: u32 = 0xDEADBEEF
b := truncate(a, u8) # b == 0xEF (low byte)
# bit reinterpretation
n: i32 = -1
m := bitcast(n, u32) # m == 0xFFFFFFFF (same bits)
f: f32 = 3.14
bits := bitcast(f, u32) # IEEE 754 representation
# pointer casts (element type, many ↔ single, pointer ↔ usize)
buf: *u8 = get_buffer()
ints := ptrcast(buf, *u32) # element type change
single := ptrcast(buf, @u8) # many → single (restricting)
addr := ptrcast(buf, usize) # pointer to integer
ptr := ptrcast(addr, @u8) # integer to pointer
```
## A word on multi-unwrap
Unwrap multiple optionals with `and`. This **short-circuits**: if the first optional is none, subsequent expressions are not evaluated.
@@ -1336,17 +1369,8 @@ Current v1 is intentionally byte-oriented and plain data:
```
mem :: import "@std/mem"
bytes := mem.alloc(mem.heap, 128, 1)
defer mem.free(mem.heap, bytes, 128, 1)
```
`@std/mem` is the preferred allocator API. `@std/mem/heap` remains as legacy compatibility wrappers over the same heap allocator:
```
heap :: import "@std/mem/heap"
bytes := heap.alloc(128)
defer heap.free(bytes)
bytes := mem.alloc(mem.c_allocator, 128, 1)
defer mem.free(mem.c_allocator, bytes, 128, 1)
```
Typed allocation helpers, arenas, pools, build-mode heap policy, and escaping-allocation diagnostics are future work. Older examples below are design sketches where noted, not committed syntax.
@@ -1368,11 +1392,11 @@ Memory allocation in Brolang is designed to be **explicit but not verbose**. We
└─────────────────────────────────────────────────────────────┘
```
### The Default Heap Allocator
### The Default C Allocator
Brolang provides a default heap allocator value:
Brolang provides a libc-backed allocator value:
* Available as `mem.heap`
* Available as `mem.c_allocator`
* Passed explicitly to `mem.alloc` and `mem.free`
* **Immutable at runtime** — cannot be reconfigured
@@ -1380,8 +1404,8 @@ Brolang provides a default heap allocator value:
mem :: import "@std/mem"
process func(input []u8) u64 {
temp ?*mut u8 = mem.alloc(mem.heap, input.len * 2, 1)
defer mem.free(mem.heap, temp, input.len * 2, 1)
temp ?*mut u8 = mem.alloc(mem.c_allocator, input.len * 2, 1)
defer mem.free(mem.c_allocator, temp, input.len * 2, 1)
# ... work with temp ...
@@ -1389,7 +1413,7 @@ process func(input []u8) u64 {
}
```
Future build modes can choose different implementations behind `mem.heap` without changing the allocator contract:
Future build modes can add other allocator values without changing the allocator contract:
| Build Mode | Allocator Behavior |
| -- | -- |
@@ -1397,7 +1421,7 @@ Future build modes can choose different implementations behind `mem.heap` withou
| Release | Fast allocator, zero overhead |
| ReleaseSafe | Bounds-checking allocator |
That policy is configured at compile time. You cannot change which allocator `mem.heap` uses at runtime. This is intentional — it prevents bugs where memory allocated with one allocator is freed with another.
Allocator policy is configured at compile time. You cannot change which allocator value an allocation used after the fact. This is intentional — it prevents bugs where memory allocated with one allocator is freed with another.
### The Escaping Allocation Rule
@@ -1423,8 +1447,8 @@ init func(obj @mut MyStruct, allocator mem.Allocator) void {
# No allocation escapes — no allocator needed
process func(input []u8) u64 {
temp ?*mut u8 = mem.alloc(mem.heap, input.len, 1)
defer mem.free(mem.heap, temp, input.len, 1)
temp ?*mut u8 = mem.alloc(mem.c_allocator, input.len, 1)
defer mem.free(mem.c_allocator, temp, input.len, 1)
# ... work with temp ...
return compute_hash(temp)
}
@@ -1435,12 +1459,12 @@ reset func(obj @mut MyStruct) void {
}
main func() void {
data := duplicate("hello", mem.heap)
defer mem.free(mem.heap, data, 5, 1)
data := duplicate("hello", mem.c_allocator)
defer mem.free(mem.c_allocator, data, 5, 1)
mut obj := MyStruct{ ... }
init(&obj, mem.heap)
defer mem.free(mem.heap, obj.buffer, 100, 1)
init(&obj, mem.c_allocator)
defer mem.free(mem.c_allocator, obj.buffer, 100, 1)
}
```
@@ -1448,7 +1472,7 @@ main func() void {
* Without the rule, a function like `init(obj: @mut MyStruct) void` is ambiguous — did it heap-allocate into `obj`, or just set some fields to stack/static data? The caller has no way to know without reading the implementation.
* With the rule, the allocator parameter is a clear signal: "this function produces heap memory that outlives its scope, and you are responsible for cleaning it up."
* Internal allocations (temporary buffers, scratch space) use `mem.heap` directly and are freed before the function returns. No allocator parameter needed, no burden on the caller.
* Internal allocations (temporary buffers, scratch space) use an explicit allocator value directly and are freed before the function returns. No allocator parameter needed, no burden on the caller.
The compiler should eventually enforce this rule. If a function heap-allocates memory that escapes without accepting an allocator parameter, the compiler should emit an error.
@@ -1457,20 +1481,20 @@ The compiler should eventually enforce this rule. If a function heap-allocates m
Consider what would happen if you could reconfigure the default allocator:
```
# ❌ THIS IS NOT ALLOWED (and doesn't exist in Brolang)
mem.heap_set(my_custom_heap)
# This is not allowed and does not exist in Brolang.
mem.default_allocator_set(my_custom_allocator)
# Somewhere else in the codebase...
data := mem.alloc(mem.heap, 100, 1)
data := mem.alloc(mem.default_allocator, 100, 1)
# Later, someone changes it again...
mem.heap_set(different_heap)
mem.default_allocator_set(different_allocator)
# Now who frees `data`? With which allocator?
mem.free(mem.heap, data, 100, 1) # wrong allocator - undefined behavior
mem.free(mem.default_allocator, data, 100, 1) # wrong allocator - undefined behavior
```
This is "action at a distance" — the behavior of `mem.free(mem.heap, ...)` depends on what some unrelated code did earlier. By making `mem.heap` immutable, Brolang guarantees:
This is "action at a distance" — the behavior of `mem.free(mem.default_allocator, ...)` depends on what some unrelated code did earlier. By making allocator values explicit and immutable, Brolang guarantees:
**Whatever you allocate with, you free with.**
@@ -1486,8 +1510,8 @@ The examples in this section are future typed API sketches. The v1 allocator con
mem :: import "@std/mem"
process_file func(path []u8, allocator mem.Allocator) !Data {
# arena manages its own backing memory via heap
arena := mem.Arena.init(mem.heap, capacity: mem.megabytes(1))
# arena manages its own backing memory via the supplied allocator
arena := mem.Arena.init(mem.c_allocator, capacity: mem.megabytes(1))
defer arena.deinit()
# all temporary allocations from arena (fast bump allocation)
@@ -1551,7 +1575,7 @@ parse func(input []u8, allocator mem.Allocator) !ParseResult {
# Caller decides which allocator to use
main func() void {
# use an arena for this parsing work
arena := mem.Arena.init(mem.heap, capacity: mem.kilobytes(64))
arena := mem.Arena.init(mem.c_allocator, capacity: mem.kilobytes(64))
defer arena.deinit()
result := parse(input, &arena) catch |err| {
# handle error
@@ -1570,13 +1594,12 @@ main func() void {
| What | How | When to Use |
| -- | -- | -- |
| `mem.alloc(mem.heap, n, a)` | Preferred heap allocator | General purpose byte allocation |
| `mem.free(mem.heap, ptr, n, a)` | Preferred heap allocator | Free byte allocation with original size/alignment |
| `heap.alloc(n)` / `heap.free(ptr)` | Legacy wrapper | Compatibility with older `@std/mem/heap` code |
| `mem.alloc(mem.c_allocator, n, a)` | Libc-backed allocator | General purpose byte allocation |
| `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 |
Note that `mem.heap` is a `mem.Allocator`, so callers can pass it as the allocator argument when they don't need a specialized allocator — which is most of the time.
Note that `mem.c_allocator` is a `mem.Allocator`, so callers can pass it as the allocator argument when they don't need a specialized allocator — which is most of the time.
**The golden rule:** Allocate and free with the same allocator. In v1 this is explicit in the call sites; future diagnostics should use the escaping allocation rule to ensure the caller always knows which allocator was used.