allocation related primitives
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user