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
+1
View File
@@ -1 +1,2 @@
/build/ /build/
.DS_Store
+64 -41
View File
@@ -628,9 +628,8 @@
25. dynamic heap allocation (implemented; v1) 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, `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.c_allocator` is the libc-backed allocator; `mem.alloc(mem.c_allocator, 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 - `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
- `std/mem/heap` remains as legacy compatibility wrappers over `std/mem`
- typed allocation helpers, arenas/pools, build-mode heap policy, and escaping-allocation diagnostics remain deferred - typed allocation helpers, arenas/pools, build-mode heap policy, and escaping-allocation diagnostics remain deferred
26. import from project "root" (implemented) 26. import from project "root" (implemented)
@@ -732,6 +731,40 @@
- deferred: build graph / steps / caching, multiple artifacts, computed paths - deferred: build graph / steps / caching, multiple artifacts, computed paths
(needs string building), struct field defaults to drop `&[]` on empty lists (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 ## A word on multi-unwrap
Unwrap multiple optionals with `and`. This **short-circuits**: if the first optional is none, subsequent expressions are not evaluated. 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" mem :: import "@std/mem"
bytes := mem.alloc(mem.heap, 128, 1) bytes := mem.alloc(mem.c_allocator, 128, 1)
defer mem.free(mem.heap, bytes, 128, 1) defer mem.free(mem.c_allocator, 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)
``` ```
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. 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` * Passed explicitly to `mem.alloc` and `mem.free`
* **Immutable at runtime** — cannot be reconfigured * **Immutable at runtime** — cannot be reconfigured
@@ -1380,8 +1404,8 @@ Brolang provides a default heap allocator value:
mem :: import "@std/mem" mem :: import "@std/mem"
process func(input []u8) u64 { process func(input []u8) u64 {
temp ?*mut u8 = mem.alloc(mem.heap, input.len * 2, 1) temp ?*mut u8 = mem.alloc(mem.c_allocator, input.len * 2, 1)
defer mem.free(mem.heap, temp, input.len * 2, 1) defer mem.free(mem.c_allocator, temp, input.len * 2, 1)
# ... work with temp ... # ... 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 | | Build Mode | Allocator Behavior |
| -- | -- | | -- | -- |
@@ -1397,7 +1421,7 @@ Future build modes can choose different implementations behind `mem.heap` withou
| Release | Fast allocator, zero overhead | | Release | Fast allocator, zero overhead |
| ReleaseSafe | Bounds-checking allocator | | 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 ### The Escaping Allocation Rule
@@ -1423,8 +1447,8 @@ init func(obj @mut MyStruct, allocator mem.Allocator) void {
# No allocation escapes — no allocator needed # No allocation escapes — no allocator needed
process func(input []u8) u64 { process func(input []u8) u64 {
temp ?*mut u8 = mem.alloc(mem.heap, input.len, 1) temp ?*mut u8 = mem.alloc(mem.c_allocator, input.len, 1)
defer mem.free(mem.heap, temp, input.len, 1) defer mem.free(mem.c_allocator, temp, input.len, 1)
# ... work with temp ... # ... work with temp ...
return compute_hash(temp) return compute_hash(temp)
} }
@@ -1435,12 +1459,12 @@ reset func(obj @mut MyStruct) void {
} }
main func() void { main func() void {
data := duplicate("hello", mem.heap) data := duplicate("hello", mem.c_allocator)
defer mem.free(mem.heap, data, 5, 1) defer mem.free(mem.c_allocator, data, 5, 1)
mut obj := MyStruct{ ... } mut obj := MyStruct{ ... }
init(&obj, mem.heap) init(&obj, mem.c_allocator)
defer mem.free(mem.heap, obj.buffer, 100, 1) 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. * 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." * 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. 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: Consider what would happen if you could reconfigure the default allocator:
``` ```
# ❌ THIS IS NOT ALLOWED (and doesn't exist in Brolang) # This is not allowed and does not exist in Brolang.
mem.heap_set(my_custom_heap) mem.default_allocator_set(my_custom_allocator)
# Somewhere else in the codebase... # 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... # Later, someone changes it again...
mem.heap_set(different_heap) mem.default_allocator_set(different_allocator)
# Now who frees `data`? With which 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.** **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" mem :: import "@std/mem"
process_file func(path []u8, allocator mem.Allocator) !Data { process_file func(path []u8, allocator mem.Allocator) !Data {
# arena manages its own backing memory via heap # arena manages its own backing memory via the supplied allocator
arena := mem.Arena.init(mem.heap, capacity: mem.megabytes(1)) arena := mem.Arena.init(mem.c_allocator, capacity: mem.megabytes(1))
defer arena.deinit() defer arena.deinit()
# all temporary allocations from arena (fast bump allocation) # 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 # Caller decides which allocator to use
main func() void { main func() void {
# use an arena for this parsing work # 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() defer arena.deinit()
result := parse(input, &arena) catch |err| { result := parse(input, &arena) catch |err| {
# handle error # handle error
@@ -1570,13 +1594,12 @@ main func() void {
| What | How | When to Use | | What | How | When to Use |
| -- | -- | -- | | -- | -- | -- |
| `mem.alloc(mem.heap, n, a)` | Preferred heap allocator | General purpose byte allocation | | `mem.alloc(mem.c_allocator, n, a)` | Libc-backed allocator | General purpose byte allocation |
| `mem.free(mem.heap, ptr, n, a)` | Preferred heap allocator | Free byte allocation with original size/alignment | | `mem.free(mem.c_allocator, ptr, n, a)` | Libc-backed 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(allocator, n, a)` | Caller-provided allocator | Escaping allocations (returned or written to caller's data) | | `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 | | 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. **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.
+85 -3
View File
@@ -277,6 +277,26 @@ is_ptr_cast_call :: proc(checker: ^Checker, expr: ast.Expr) -> bool {
symbol_text(checker, expr.name) == "ptr_cast" symbol_text(checker, expr.name) == "ptr_cast"
} }
Layout_Builtin :: enum u8 {
None,
Size_Of,
Align_Of,
}
layout_builtin_call :: proc(checker: ^Checker, expr: ast.Expr) -> Layout_Builtin {
if expr.kind != .Call || expr.left != ast.INVALID_EXPR || symbol.is_valid(expr.qualifier) {
return .None
}
name := symbol_text(checker, expr.name)
if name == "size_of" {
return .Size_Of
}
if name == "align_of" {
return .Align_Of
}
return .None
}
valid_ptr_cast_child :: proc(checker: ^Checker, value: types.Type) -> bool { valid_ptr_cast_child :: proc(checker: ^Checker, value: types.Type) -> bool {
return types.is_valid(value) && return types.is_valid(value) &&
!types.is_void(value) && !types.is_void(value) &&
@@ -286,6 +306,49 @@ valid_ptr_cast_child :: proc(checker: ^Checker, value: types.Type) -> bool {
types.is_opaque_struct(value, &checker.module.types)) types.is_opaque_struct(value, &checker.module.types))
} }
valid_layout_type :: proc(checker: ^Checker, value: types.Type) -> bool {
return types.is_runtime_value(value, &checker.module.types)
}
layout_builtin_value :: proc(checker: ^Checker, kind: Layout_Builtin, value: types.Type) -> i128 {
#partial switch kind {
case .Size_Of:
return i128(types.size(value, &checker.module.types, checker.target))
case .Align_Of:
return i128(types.alignment_of(value, &checker.module.types, checker.target))
case:
return 0
}
}
build_layout_builtin :: proc(
checker: ^Checker,
expr: ast.Expr,
kind: Layout_Builtin,
pkg: ast.Package_Id,
file: ast.File_Id,
) -> hir.Expr_Id {
if len(expr.args) != 1 {
id := source.addf(checker.diagnostics, expr.span, "%s expects 1 argument, got %d", symbol_text(checker, expr.name), len(expr.args))
return invalid_hir_expr(checker, expr.span, id, types.USIZE)
}
target, target_ok := resolve_type_argument(checker, expr.args[0], pkg, file)
if !target_ok {
id := source.add(checker.diagnostics, checker.ast_module.exprs[expr.args[0]].span, "layout target must be a type")
return invalid_hir_expr(checker, expr.span, id, types.USIZE)
}
if !valid_layout_type(checker, target) {
id := source.addf(checker.diagnostics, checker.ast_module.exprs[expr.args[0]].span, "layout target must be a sized runtime value type, got %s", type_label(checker, target))
return invalid_hir_expr(checker, expr.span, id, types.USIZE)
}
return build_constant_expr(
checker,
expr,
Constant{kind=.Value, value=layout_builtin_value(checker, kind, target)},
types.USIZE,
)
}
is_type_metatype_syntax :: proc(checker: ^Checker, value: ast.Type_Syntax) -> bool { is_type_metatype_syntax :: proc(checker: ^Checker, value: ast.Type_Syntax) -> bool {
item, ok := types.node(&checker.module.types, value) item, ok := types.node(&checker.module.types, value)
return ok && item.name == u32(checker.type_symbol) && item.qualifier == 0 return ok && item.name == u32(checker.type_symbol) && item.qualifier == 0
@@ -1828,7 +1891,7 @@ infer_compound_expr :: proc(
case .Slice: case .Slice:
value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types) value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
item, ok := types.container(value, store) item, ok := types.container(value, store)
if !ok || item.kind == .Pointer { if !ok || (item.kind == .Pointer && expr.args[1] == ast.INVALID_EXPR) {
return types.INVALID return types.INVALID
} }
for bound in expr.args { for bound in expr.args {
@@ -2098,6 +2161,11 @@ infer_expr :: proc(
} }
continue continue
} }
if builtin := layout_builtin_call(checker, expr); builtin != .None {
last = types.USIZE
_ = pop(&stack)
continue
}
if is_ptr_cast_call(checker, expr) { if is_ptr_cast_call(checker, expr) {
if len(expr.args) != 2 { if len(expr.args) != 2 {
last = types.INVALID last = types.INVALID
@@ -3080,6 +3148,11 @@ infer_all :: proc(checker: ^Checker) {
if global.external { if global.external {
continue continue
} }
if global.expr != ast.INVALID_EXPR && int(global.expr) < len(checker.ast_module.exprs) &&
layout_builtin_call(checker, checker.ast_module.exprs[global.expr]) != .None {
checker.global_types[index] = types.USIZE
continue
}
constant := eval_integer_constant_in_context(checker, global.expr, global.pkg, global.file) constant := eval_integer_constant_in_context(checker, global.expr, global.pkg, global.file)
if constant.kind == .Value && fits_i64(constant.value) { if constant.kind == .Value && fits_i64(constant.value) {
checker.global_open_const[index] = true checker.global_open_const[index] = true
@@ -4227,8 +4300,12 @@ build_compound_expr :: proc(
container := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file) container := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file)
container_type := checker.module.exprs[container].type container_type := checker.module.exprs[container].type
item, ok := types.container(container_type, store) item, ok := types.container(container_type, store)
if !ok || item.kind == .Pointer { if !ok {
id := source.add(checker.diagnostics, expr.span, "slicing requires an array, slice, or pointer-to-array") id := source.add(checker.diagnostics, expr.span, "slicing requires an array, slice, pointer-to-array, or many-item pointer")
return invalid_hir_expr(checker, expr.span, id)
}
if item.kind == .Pointer && expr.args[1] == ast.INVALID_EXPR {
id := source.add(checker.diagnostics, expr.span, "many-item pointer slicing requires an explicit end bound")
return invalid_hir_expr(checker, expr.span, id) return invalid_hir_expr(checker, expr.span, id)
} }
bounds := make([]hir.Expr_Id, 2, checker.allocator) bounds := make([]hir.Expr_Id, 2, checker.allocator)
@@ -4881,6 +4958,11 @@ build_expr :: proc(
append(&stack, Build_Expr_Frame{expr=expr.left, expected=types.INVALID, template=ast.INVALID_FUNCTION}) append(&stack, Build_Expr_Frame{expr=expr.left, expected=types.INVALID, template=ast.INVALID_FUNCTION})
continue continue
} }
if builtin := layout_builtin_call(checker, expr); builtin != .None {
last = build_layout_builtin(checker, expr, builtin, pkg, file)
_ = pop(&stack)
continue
}
if is_ptr_cast_call(checker, expr) { if is_ptr_cast_call(checker, expr) {
if len(expr.args) != 2 { if len(expr.args) != 2 {
id := source.addf(checker.diagnostics, expr.span, "ptr_cast expects 2 arguments, got %d", len(expr.args)) id := source.addf(checker.diagnostics, expr.span, "ptr_cast expects 2 arguments, got %d", len(expr.args))
+13
View File
@@ -1909,6 +1909,19 @@ ct_eval_call_expr :: proc(state: ^Ct_State, expr: ast.Expr, expected: types.Type
} }
return ct_eval_template_call(state, ast.Function_Id(u32(state.values[callee].index)), expr.args, expr.span, expected, depth+1) return ct_eval_template_call(state, ast.Function_Id(u32(state.values[callee].index)), expr.args, expr.span, expected, depth+1)
} }
if builtin := layout_builtin_call(checker, expr); builtin != .None {
if len(expr.args) != 1 {
return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, expr.span, "%s expects 1 argument, got %d", symbol_text(checker, expr.name), len(expr.args))
}
target, target_ok := resolve_type_argument(checker, expr.args[0], state.pkg, state.file)
if !target_ok {
return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, checker.ast_module.exprs[expr.args[0]].span, "layout target must be a type")
}
if !valid_layout_type(checker, target) {
return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, checker.ast_module.exprs[expr.args[0]].span, "layout target must be a sized runtime value type, got %s", type_label(checker, target))
}
return ct_add_value(state, Ct_Value{kind=.Integer, type=types.USIZE, integer=layout_builtin_value(checker, builtin, target)}), ct_flow(.Normal), true
}
target_pkg, available := expr_package(checker, expr, state.pkg, state.file, false) target_pkg, available := expr_package(checker, expr, state.pkg, state.file, false)
if !available { if !available {
return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "unavailable function package") return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "unavailable function package")
+11 -1
View File
@@ -1114,7 +1114,7 @@ emit_instruction_stream :: proc(
} }
container := instructions[instruction.a] container := instructions[instruction.a]
item, ok := types.container(container.type, &emitter.module.types) item, ok := types.container(container.type, &emitter.module.types)
if !ok || (item.kind != .Array && item.kind != .Slice) { if !ok || (item.kind != .Array && item.kind != .Slice && item.kind != .Pointer) {
emit_recovery_value(emitter, instruction_index, instruction, "invalid slice container") emit_recovery_value(emitter, instruction_index, instruction, "invalid slice container")
continue continue
} }
@@ -1132,6 +1132,12 @@ emit_instruction_stream :: proc(
fmt.sbprintf(&emitter.builder, " %%slice_len%d = extractvalue %s %%v%d, 1\n", instruction_index, llvm_type(container.type, &emitter.module.types), instruction.a) fmt.sbprintf(&emitter.builder, " %%slice_len%d = extractvalue %s %%v%d, 1\n", instruction_index, llvm_type(container.type, &emitter.module.types), instruction.a)
pointer_name = fmt.tprintf("%%slice_ptr%d", instruction_index) pointer_name = fmt.tprintf("%%slice_ptr%d", instruction_index)
length_name = fmt.tprintf("%%slice_len%d", instruction_index) length_name = fmt.tprintf("%%slice_len%d", instruction_index)
} else if item.kind == .Pointer {
if len(instruction.args) <= 1 || instruction.args[1] == ir.INVALID_INSTRUCTION {
emit_recovery_value(emitter, instruction_index, instruction, "invalid many-item pointer slice")
continue
}
length_name = "0"
} }
fmt.sbprintf(&emitter.builder, " %%slice_bound_start%d = add i64 0, ", instruction_index) fmt.sbprintf(&emitter.builder, " %%slice_bound_start%d = add i64 0, ", instruction_index)
if len(instruction.args) > 0 && instruction.args[0] != ir.INVALID_INSTRUCTION { if len(instruction.args) > 0 && instruction.args[0] != ir.INVALID_INSTRUCTION {
@@ -1150,8 +1156,12 @@ emit_instruction_stream :: proc(
start_name := fmt.tprintf("%%slice_bound_start%d", instruction_index) start_name := fmt.tprintf("%%slice_bound_start%d", instruction_index)
end_name := fmt.tprintf("%%slice_bound_end%d", instruction_index) end_name := fmt.tprintf("%%slice_bound_end%d", instruction_index)
fmt.sbprintf(&emitter.builder, " %%slice_order%d = icmp ule i64 %s, %s\n", instruction_index, start_name, end_name) fmt.sbprintf(&emitter.builder, " %%slice_order%d = icmp ule i64 %s, %s\n", instruction_index, start_name, end_name)
if item.kind == .Pointer {
fmt.sbprintf(&emitter.builder, " %%slice_ok%d = or i1 false, %%slice_order%d\n", instruction_index, instruction_index)
} else {
fmt.sbprintf(&emitter.builder, " %%slice_end_ok%d = icmp ule i64 %s, %s\n", instruction_index, end_name, length_name) fmt.sbprintf(&emitter.builder, " %%slice_end_ok%d = icmp ule i64 %s, %s\n", instruction_index, end_name, length_name)
fmt.sbprintf(&emitter.builder, " %%slice_ok%d = and i1 %%slice_order%d, %%slice_end_ok%d\n", instruction_index, instruction_index, instruction_index) fmt.sbprintf(&emitter.builder, " %%slice_ok%d = and i1 %%slice_order%d, %%slice_end_ok%d\n", instruction_index, instruction_index, instruction_index)
}
fmt.sbprintf(&emitter.builder, " br i1 %%slice_ok%d, label %%slice_continue%d, label %%slice_trap%d\nslice_trap%d:\n", instruction_index, instruction_index, instruction_index, instruction_index) fmt.sbprintf(&emitter.builder, " br i1 %%slice_ok%d, label %%slice_continue%d, label %%slice_trap%d\nslice_trap%d:\n", instruction_index, instruction_index, instruction_index, instruction_index)
message := diagnostic_message(emitter, source.INVALID_DIAGNOSTIC, instruction.span, "slice bounds out of range") message := diagnostic_message(emitter, source.INVALID_DIAGNOSTIC, instruction.span, "slice bounds out of range")
emit_trap_call(emitter, message) emit_trap_call(emitter, message)
+23
View File
@@ -653,6 +653,17 @@ parse_primary :: proc(parser: ^Parser, nesting: int) -> ast.Expr_Id {
right=ast.INVALID_EXPR, right=ast.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC, diagnostic=source.INVALID_DIAGNOSTIC,
}) })
case .Question, .At, .Star:
start := tok
target := parse_type_atom(parser)
return add_expr(parser, ast.Expr{
kind=.Type,
span=span_from(start.span, previous(parser).span),
type=target,
left=ast.INVALID_EXPR,
right=ast.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
case .Integer: case .Integer:
advance(parser) advance(parser)
value, ok := parse_integer_magnitude(token_text(parser, tok)) value, ok := parse_integer_magnitude(token_text(parser, tok))
@@ -739,6 +750,18 @@ parse_primary :: proc(parser: ^Parser, nesting: int) -> ast.Expr_Id {
case .Keyword_Func: case .Keyword_Func:
return parse_function_literal(parser) return parse_function_literal(parser)
case .Left_Bracket: case .Left_Bracket:
if starts_declared_type(parser) {
start := tok
target := parse_type_atom(parser)
return add_expr(parser, ast.Expr{
kind=.Type,
span=span_from(start.span, previous(parser).span),
type=target,
left=ast.INVALID_EXPR,
right=ast.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
return parse_array_literal(parser, nesting) return parse_array_literal(parser, nesting)
case .Dot: case .Dot:
start := advance(parser) start := advance(parser)
+165 -10
View File
@@ -1294,6 +1294,160 @@ immutable_pointer_and_slice_bindings_preserve_mutable_pointees :: proc(t: ^testi
testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, len(diagnostics.items), 0)
} }
@(test)
many_item_pointer_slices_compile_and_run :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-pointer-slices"
main_path := "/tmp/brolang-test-pointer-slices/main.bro"
output := "/tmp/brolang-test-pointer-slices-output"
text := `main func() i32 {
values [4]mut i32 = [3, 4, 5, 6]
pointer *mut i32 :: (&values).ptr
const_pointer *i32 :: pointer
all []mut i32 :: pointer[..4]
middle []mut i32 :: pointer[1..3]
readonly []i32 :: const_pointer[..2]
if (all.len != 4) return 1
if (middle.len != 2) return 2
all[0] = 10
if (readonly[0] != 10) return 3
if (middle.ptr[0] != 4) return 4
return 0
}
`
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
status := compiler_core.compile_package(directory, output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
many_item_pointer_slices_require_end_bound :: proc(t: ^testing.T) {
text := `main func() void {
values [2]i32 = [1, 2]
pointer *i32 :: (&values).ptr
_ = pointer[..]
_ = pointer[1..]
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
end_bound_errors := 0
for diagnostic in diagnostics.items {
if strings.contains(diagnostic.message, "many-item pointer slicing requires an explicit end bound") {
end_bound_errors += 1
}
}
testing.expect_value(t, end_bound_errors, 2)
}
@(test)
layout_builtins_compile_and_run :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-layout-builtins"
main_path := "/tmp/brolang-test-layout-builtins/main.bro"
output := "/tmp/brolang-test-layout-builtins-output"
text := `Point :: struct {
x i32
y u8
}
Opaque :: opaque
Color :: enum {
red
blue
}
UserID :: distinct u32
SIZE_GLOBAL :: size_of(i32)
needs_usize func(value usize) usize {
return value
}
buffer func($T type) [size_of(T)]u8 {
data [size_of(T)]u8 = undefined
return data
}
main func() i32 {
bytes [_]u8 :: buffer(i32)
if (needs_usize(SIZE_GLOBAL) != 4) return 1
if (bytes.len != 4) return 2
if (size_of([3]u8) != 3) return 3
if (size_of([]u8) != 16) return 4
if (align_of([]u8) != 8) return 5
if (size_of(*anyopaque) != 8) return 6
if (size_of(?*i32) != 8) return 7
if (size_of(*Opaque) != 8) return 8
if (size_of(Color) != 2) return 9
if (align_of(Color) != 2) return 10
if (size_of(Point) != 8) return 11
if (align_of(Point) != 4) return 12
if (size_of(UserID) != 4) return 13
if (align_of(UserID) != 4) return 14
return 0
}
`
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
status := compiler_core.compile_package(directory, output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
layout_builtins_reject_unsized_targets :: proc(t: ^testing.T) {
text := `Opaque :: opaque
Fn :: alias func() void
main func() void {
_ = size_of(void)
_ = align_of(anyopaque)
_ = size_of(Fn)
_ = size_of(Opaque)
_ = align_of(1)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
bad_layout_targets := 0
target_type_error := false
for diagnostic in diagnostics.items {
bad_layout_targets += 1 if strings.contains(diagnostic.message, "layout target must be a sized runtime value type") else 0
target_type_error = target_type_error || strings.contains(diagnostic.message, "layout target must be a type")
}
testing.expect_value(t, bad_layout_targets, 4)
testing.expect(t, target_type_error)
}
@(test) @(test)
slicing_an_array_variable_takes_its_address_implicitly :: proc(t: ^testing.T) { slicing_an_array_variable_takes_its_address_implicitly :: proc(t: ^testing.T) {
// Milestone 10: `arr[a..b]` on an array variable slices without an explicit // Milestone 10: `arr[a..b]` on an array variable slices without an explicit
@@ -3109,7 +3263,7 @@ allocator_contract_compiles_and_runs :: proc(t: ^testing.T) {
} }
@(test) @(test)
allocator_contract_heap_global_lowers :: proc(t: ^testing.T) { allocator_contract_c_allocator_global_lowers :: proc(t: ^testing.T) {
sources := source.init_store() sources := source.init_store()
defer source.destroy_store(&sources) defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources) diagnostics := source.init_store_diagnostics(&sources)
@@ -3125,15 +3279,15 @@ allocator_contract_heap_global_lowers :: proc(t: ^testing.T) {
ir_module := lower.lower(&hir_module) ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module) defer ir.destroy_module(&ir_module)
found_heap := false found_c_allocator := false
found_anyopaque_context := false found_anyopaque_context := false
found_alloc_callback := false found_alloc_callback := false
found_free_callback := false found_free_callback := false
for global in hir_module.globals { for global in hir_module.globals {
if symbol.resolve(&symbols, global.name) != "heap" { if symbol.resolve(&symbols, global.name) != "c_allocator" {
continue continue
} }
found_heap = true found_c_allocator = true
for field in types.fields_for(&hir_module.types, global.type) { for field in types.fields_for(&hir_module.types, global.type) {
name := symbol.resolve(&symbols, symbol.Id(field.name)) name := symbol.resolve(&symbols, symbol.Id(field.name))
callback_pointer, _, _, callable := types.function_pointer(field.type, &hir_module.types) callback_pointer, _, _, callable := types.function_pointer(field.type, &hir_module.types)
@@ -3155,31 +3309,31 @@ allocator_contract_heap_global_lowers :: proc(t: ^testing.T) {
testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, len(ir_module.functions) > 0) testing.expect(t, len(ir_module.functions) > 0)
testing.expect(t, found_heap) testing.expect(t, found_c_allocator)
testing.expect(t, found_anyopaque_context) testing.expect(t, found_anyopaque_context)
testing.expect(t, found_alloc_callback) testing.expect(t, found_alloc_callback)
testing.expect(t, found_free_callback) testing.expect(t, found_free_callback)
} }
@(test) @(test)
milestone_25_heap_compiles_and_runs :: proc(t: ^testing.T) { milestone_25_c_allocator_compiles_and_runs :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-heap" output := "/tmp/brolang-test-c-allocator"
defer _ = os.remove(output) defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/heap", output, nil, target.DEFAULT, cimport.Options{}, ".") status := compiler_core.compile_package("examples/programs/mem_allocator", output, nil, target.DEFAULT, cimport.Options{}, ".")
testing.expect_value(t, status, 0) testing.expect_value(t, status, 0)
state := run_executable(output) state := run_executable(output)
testing.expect_value(t, state.exit_code, 0) testing.expect_value(t, state.exit_code, 0)
} }
@(test) @(test)
milestone_25_heap_emits_libc_alloc_declarations :: proc(t: ^testing.T) { milestone_25_c_allocator_emits_libc_alloc_declarations :: proc(t: ^testing.T) {
sources := source.init_store() sources := source.init_store()
defer source.destroy_store(&sources) defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources) diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics) defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table() symbols := symbol.init_table()
defer symbol.destroy_table(&symbols) defer symbol.destroy_table(&symbols)
ast_module, loaded := loader.load("examples/programs/heap", &sources, &diagnostics, &symbols, context.allocator, context.allocator, cimport.Options{}, target.DEFAULT, ".") ast_module, loaded := loader.load("examples/programs/mem_allocator", &sources, &diagnostics, &symbols, context.allocator, context.allocator, cimport.Options{}, target.DEFAULT, ".")
defer ast.destroy_module(&ast_module) defer ast.destroy_module(&ast_module)
testing.expect(t, loaded) testing.expect(t, loaded)
@@ -3192,6 +3346,7 @@ milestone_25_heap_emits_libc_alloc_declarations :: proc(t: ^testing.T) {
testing.expect_value(t, len(diagnostics.items), 0) 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 @malloc(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)")) testing.expect(t, strings.contains(llvm_text, "declare void @free(ptr)"))
} }
-20
View File
@@ -1,20 +0,0 @@
heap :: import "@std/mem/heap"
main func() i32 {
memory ?*mut u8 = heap.alloc(4)
defer heap.free(memory)
if memory |bytes| {
bytes[0] = 10
bytes[1] = 20
bytes[2] = bytes[0] + bytes[1]
if (bytes[2] != 30) {
return 2
}
return 0
}
return 1
}
+204 -10
View File
@@ -1,20 +1,214 @@
mem :: import "@std/mem" mem :: import "@std/mem"
TaskList :: struct {
ids ?[]mut i32
priorities ?[]mut i32
durations ?[]mut i32
len usize
capacity usize
allocator mem.Allocator
}
task_list_init func(allocator mem.Allocator) TaskList {
return TaskList {
ids = none,
priorities = none,
durations = none,
len = 0,
capacity = 0,
allocator = allocator,
}
}
alloc_i32s func(allocator mem.Allocator, count usize) ?[]mut i32 {
raw ?*mut u8 = mem.alloc(allocator, count * size_of(i32), align_of(i32))
if raw |bytes| {
values *mut i32 = ptr_cast(i32, bytes)
return values[..count]
}
return none
}
free_i32s func(allocator mem.Allocator, values ?[]mut i32) void {
if values |slice| {
mem.free(allocator, ptr_cast(u8, slice.ptr), slice.len * size_of(i32), align_of(i32))
}
}
task_list_reserve func(list @mut TaskList, capacity usize) bool {
if capacity <= list.capacity {
return true
}
new_ids ?[]mut i32 = alloc_i32s(list.allocator, capacity)
new_priorities ?[]mut i32 = alloc_i32s(list.allocator, capacity)
new_durations ?[]mut i32 = alloc_i32s(list.allocator, capacity)
if (new_ids and new_priorities and new_durations) |ids, priorities, durations| {
if list.len > 0 {
if (list.ids and list.priorities and list.durations) |old_ids, old_priorities, old_durations| {
i usize = 0
while i < list.len : i += 1 {
ids[i] = old_ids[i]
priorities[i] = old_priorities[i]
durations[i] = old_durations[i]
}
} else {
free_i32s(list.allocator, new_ids)
free_i32s(list.allocator, new_priorities)
free_i32s(list.allocator, new_durations)
return false
}
}
free_i32s(list.allocator, list.ids)
free_i32s(list.allocator, list.priorities)
free_i32s(list.allocator, list.durations)
list.ids = new_ids
list.priorities = new_priorities
list.durations = new_durations
list.capacity = capacity
return true
}
free_i32s(list.allocator, new_ids)
free_i32s(list.allocator, new_priorities)
free_i32s(list.allocator, new_durations)
return false
}
task_list_push func(list @mut TaskList, id i32, priority i32, duration i32) bool {
if list.len == list.capacity {
new_capacity usize = 2
if list.capacity != 0 {
new_capacity = list.capacity * 2
}
if task_list_reserve(list, new_capacity) == false {
return false
}
}
if (list.ids and list.priorities and list.durations) |ids, priorities, durations| {
index usize = list.len
ids[index] = id
priorities[index] = priority
durations[index] = duration
list.len += 1
return true
}
return false
}
task_score func(priority i32, duration i32) i32 {
return priority * 10 - duration
}
task_list_best_id func(list @mut TaskList) i32 {
if list.len == 0 {
return -1
}
if (list.ids and list.priorities and list.durations) |ids, priorities, durations| {
best_index usize = 0
best_score i32 = task_score(priorities[0], durations[0])
i usize = 1
while i < list.len : i += 1 {
score i32 = task_score(priorities[i], durations[i])
if score > best_score {
best_score = score
best_index = i
}
}
return ids[best_index]
}
return -1
}
task_list_total_duration func(list @mut TaskList) i32 {
total i32 = 0
if list.durations |durations| {
i usize = 0
while i < list.len : i += 1 {
total += durations[i]
}
}
return total
}
task_list_deinit func(list @mut TaskList) void {
free_i32s(list.allocator, list.ids)
free_i32s(list.allocator, list.priorities)
free_i32s(list.allocator, list.durations)
list.ids = none
list.priorities = none
list.durations = none
list.len = 0
list.capacity = 0
}
main func() i32 { main func() i32 {
memory ?*mut u8 = mem.alloc(mem.heap, 4, 1) zero_alignment ?*mut u8 = mem.alloc(mem.c_allocator, 8, 0)
defer mem.free(mem.heap, memory, 4, 1) if zero_alignment |memory| {
mem.free(mem.c_allocator, memory, 8, 0)
return 1
}
if memory |bytes| { bad_alignment ?*mut u8 = mem.alloc(mem.c_allocator, 8, 24)
bytes[0] = 10 if bad_alignment |memory| {
bytes[1] = 20 mem.free(mem.c_allocator, memory, 8, 24)
bytes[2] = bytes[0] + bytes[1]
if (bytes[2] != 30) {
return 2 return 2
} }
return 0 aligned ?*mut u8 = mem.alloc(mem.c_allocator, 64, 32)
defer mem.free(mem.c_allocator, aligned, 64, 32)
if aligned |bytes| {
bytes[0] = 1
bytes[63] = 2
if bytes[0] + bytes[63] != 3 {
return 4
}
} else {
return 3
} }
return 1 tasks TaskList = task_list_init(mem.c_allocator)
defer task_list_deinit(&tasks)
if task_list_push(&tasks, 101, 3, 5) == false {
return 5
}
if tasks.capacity != 2 {
return 6
}
if task_list_push(&tasks, 202, 1, 4) == false {
return 7
}
if tasks.capacity != 2 {
return 8
}
if task_list_push(&tasks, 303, 4, 8) == false {
return 9
}
if tasks.capacity != 4 {
return 10
}
if tasks.len != 3 {
return 11
}
if task_list_best_id(&tasks) != 303 {
return 12
}
if task_list_total_duration(&tasks) != 17 {
return 13
}
return 0
} }
+3 -2
View File
@@ -1,2 +1,3 @@
malloc c_func(size usize) ?*mut anyopaque malloc c_func(__size c_ulong) ?*mut anyopaque
free c_func(ptr ?*mut anyopaque) void free c_func(_ ?*mut anyopaque) void
posix_memalign c_func(__memptr ?*mut ?*mut anyopaque, __alignment c_ulong, __size c_ulong) c_int
+44 -7
View File
@@ -6,14 +6,51 @@ Allocator :: struct {
free @func(context ?*mut anyopaque, memory ?*mut u8, size usize, alignment usize) void free @func(context ?*mut anyopaque, memory ?*mut u8, size usize, alignment usize) void
} }
heap Allocator :: Allocator { _malloc_alignment usize :: 16 # ponytail: aarch64-macos libc malloc alignment assumption.
context = none,
alloc = func(_ ?*mut anyopaque, size usize, _ usize) ?*mut u8 { _power_of_two func(value usize) bool {
return ptr_cast(u8, c.malloc(size)) if value == 0 {
}, return false
free = func(_ ?*mut anyopaque, memory ?*mut u8, _ usize, _ usize) void { }
current usize = value
while current > 1 {
half usize = current / 2
if half * 2 != current {
return false
}
current = half
}
return true
}
_c_alloc func(_ ?*mut anyopaque, size usize, alignment usize) ?*mut u8 {
if _power_of_two(alignment) == false {
return none
}
if alignment <= _malloc_alignment {
return ptr_cast(u8, c.malloc(c_ulong(size)))
}
memory [1]mut ?*mut anyopaque = [none]
status c_int = c.posix_memalign((&memory).ptr, c_ulong(alignment), c_ulong(size))
if status != 0 {
return none
}
return ptr_cast(u8, memory[0])
}
_c_free func(_ ?*mut anyopaque, memory ?*mut u8, _ usize, _ usize) void {
c.free(memory) c.free(memory)
}, }
c_allocator Allocator :: Allocator {
context = none,
alloc = _c_alloc,
free = _c_free,
} }
alloc func(allocator Allocator, size usize, alignment usize) ?*mut u8 { alloc func(allocator Allocator, size usize, alignment usize) ?*mut u8 {