diff --git a/.gitignore b/.gitignore index 84c048a..5160f36 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /build/ +.DS_Store diff --git a/TODO.md b/TODO.md index 86efbfc..c57b4d4 100644 --- a/TODO.md +++ b/TODO.md @@ -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. diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index 7cdf006..b23db84 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -277,6 +277,26 @@ is_ptr_cast_call :: proc(checker: ^Checker, expr: ast.Expr) -> bool { 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 { return types.is_valid(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)) } +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 { item, ok := types.node(&checker.module.types, value) return ok && item.name == u32(checker.type_symbol) && item.qualifier == 0 @@ -1828,7 +1891,7 @@ infer_compound_expr :: proc( case .Slice: value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types) 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 } for bound in expr.args { @@ -2098,6 +2161,11 @@ infer_expr :: proc( } continue } + if builtin := layout_builtin_call(checker, expr); builtin != .None { + last = types.USIZE + _ = pop(&stack) + continue + } if is_ptr_cast_call(checker, expr) { if len(expr.args) != 2 { last = types.INVALID @@ -3080,6 +3148,11 @@ infer_all :: proc(checker: ^Checker) { if global.external { 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) if constant.kind == .Value && fits_i64(constant.value) { 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_type := checker.module.exprs[container].type item, ok := types.container(container_type, store) - if !ok || item.kind == .Pointer { - id := source.add(checker.diagnostics, expr.span, "slicing requires an array, slice, or pointer-to-array") + if !ok { + 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) } 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}) 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 len(expr.args) != 2 { id := source.addf(checker.diagnostics, expr.span, "ptr_cast expects 2 arguments, got %d", len(expr.args)) diff --git a/compiler/checker/comptime.odin b/compiler/checker/comptime.odin index d74b9b9..bffa791 100644 --- a/compiler/checker/comptime.odin +++ b/compiler/checker/comptime.odin @@ -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) } + 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) if !available { return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "unavailable function package") diff --git a/compiler/llvm/llvm.odin b/compiler/llvm/llvm.odin index b9e6955..6edbcfd 100644 --- a/compiler/llvm/llvm.odin +++ b/compiler/llvm/llvm.odin @@ -1114,7 +1114,7 @@ emit_instruction_stream :: proc( } container := instructions[instruction.a] 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") 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) pointer_name = fmt.tprintf("%%slice_ptr%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) 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) 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_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) + 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_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) message := diagnostic_message(emitter, source.INVALID_DIAGNOSTIC, instruction.span, "slice bounds out of range") emit_trap_call(emitter, message) diff --git a/compiler/parser/parser.odin b/compiler/parser/parser.odin index bafa68b..c470aa4 100644 --- a/compiler/parser/parser.odin +++ b/compiler/parser/parser.odin @@ -653,6 +653,17 @@ parse_primary :: proc(parser: ^Parser, nesting: int) -> ast.Expr_Id { right=ast.INVALID_EXPR, 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: advance(parser) 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: return parse_function_literal(parser) 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) case .Dot: start := advance(parser) diff --git a/compiler_tests.odin b/compiler_tests.odin index aba0288..4b2c691 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -1294,6 +1294,160 @@ immutable_pointer_and_slice_bindings_preserve_mutable_pointees :: proc(t: ^testi 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) 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 @@ -3109,7 +3263,7 @@ allocator_contract_compiles_and_runs :: proc(t: ^testing.T) { } @(test) -allocator_contract_heap_global_lowers :: proc(t: ^testing.T) { +allocator_contract_c_allocator_global_lowers :: proc(t: ^testing.T) { sources := source.init_store() defer source.destroy_store(&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) defer ir.destroy_module(&ir_module) - found_heap := false + found_c_allocator := false found_anyopaque_context := false found_alloc_callback := false found_free_callback := false for global in hir_module.globals { - if symbol.resolve(&symbols, global.name) != "heap" { + if symbol.resolve(&symbols, global.name) != "c_allocator" { continue } - found_heap = true + found_c_allocator = true for field in types.fields_for(&hir_module.types, global.type) { name := symbol.resolve(&symbols, symbol.Id(field.name)) 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(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_alloc_callback) testing.expect(t, found_free_callback) } @(test) -milestone_25_heap_compiles_and_runs :: proc(t: ^testing.T) { - output := "/tmp/brolang-test-heap" +milestone_25_c_allocator_compiles_and_runs :: proc(t: ^testing.T) { + output := "/tmp/brolang-test-c-allocator" 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) state := run_executable(output) testing.expect_value(t, state.exit_code, 0) } @(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() defer source.destroy_store(&sources) diagnostics := source.init_store_diagnostics(&sources) defer source.destroy_diagnostics(&diagnostics) symbols := symbol.init_table() 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) 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(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)")) } diff --git a/examples/programs/heap/main.bro b/examples/programs/heap/main.bro deleted file mode 100644 index f3c12e4..0000000 --- a/examples/programs/heap/main.bro +++ /dev/null @@ -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 -} diff --git a/examples/programs/mem_allocator/main.bro b/examples/programs/mem_allocator/main.bro index ce2eba4..ca51099 100644 --- a/examples/programs/mem_allocator/main.bro +++ b/examples/programs/mem_allocator/main.bro @@ -1,20 +1,214 @@ mem :: import "@std/mem" -main func() i32 { - memory ?*mut u8 = mem.alloc(mem.heap, 4, 1) - defer mem.free(mem.heap, memory, 4, 1) +TaskList :: struct { + ids ?[]mut i32 + priorities ?[]mut i32 + durations ?[]mut i32 + len usize + capacity usize + allocator mem.Allocator +} - if memory |bytes| { - bytes[0] = 10 - bytes[1] = 20 - bytes[2] = bytes[0] + bytes[1] +task_list_init func(allocator mem.Allocator) TaskList { + return TaskList { + ids = none, + priorities = none, + durations = none, + len = 0, + capacity = 0, + allocator = allocator, + } +} - if (bytes[2] != 30) { - return 2 - } +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 +} - return 0 +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 } - return 1 + 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 { + zero_alignment ?*mut u8 = mem.alloc(mem.c_allocator, 8, 0) + if zero_alignment |memory| { + mem.free(mem.c_allocator, memory, 8, 0) + return 1 + } + + bad_alignment ?*mut u8 = mem.alloc(mem.c_allocator, 8, 24) + if bad_alignment |memory| { + mem.free(mem.c_allocator, memory, 8, 24) + return 2 + } + + 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 + } + + 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 } diff --git a/ffi/c/stdlib.bro b/ffi/c/stdlib.bro index c7a616e..c343c57 100644 --- a/ffi/c/stdlib.bro +++ b/ffi/c/stdlib.bro @@ -1,2 +1,3 @@ -malloc c_func(size usize) ?*mut anyopaque -free c_func(ptr ?*mut anyopaque) void +malloc c_func(__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 4251ff2..0037bec 100644 --- a/std/mem/mem.bro +++ b/std/mem/mem.bro @@ -6,14 +6,51 @@ Allocator :: struct { 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. + +_power_of_two func(value usize) bool { + if value == 0 { + return false + } + + 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_allocator Allocator :: Allocator { context = none, - alloc = func(_ ?*mut anyopaque, size usize, _ usize) ?*mut u8 { - return ptr_cast(u8, c.malloc(size)) - }, - free = func(_ ?*mut anyopaque, memory ?*mut u8, _ usize, _ usize) void { - c.free(memory) - }, + alloc = _c_alloc, + free = _c_free, } alloc func(allocator Allocator, size usize, alignment usize) ?*mut u8 {