diff --git a/LANGUAGE.md b/LANGUAGE.md index 4641034..a5dc8a9 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -23,6 +23,7 @@ roadmap and milestone history. - target-dependent C scalar primitives from `c_char` through `c_longdouble`, kept semantically distinct from native scalars - contextual integer/float/character literals, backward type-demand inference through names and arithmetic, and compile-time folding for numeric constant expressions - strict numeric conversion by default, widening where valid, C scalar coercions at C boundaries, and explicit scalar keyword casts such as `i32(x)` / `c_float(x)` +- compile-time `min_value(T)` and `max_value(T)` bounds for concrete native and C integer scalar types - arrays `[N]T`, inferred-count arrays `[_]T`, sentinel arrays `[N;S]T`, compile-time expression array counts, slices `[]T` / `[;S]T`, single-item pointers `@T`, many-item pointers `*T`, and sentinel many-item pointers `[*;S]T` - pointer mutability via `mut`, optional pointers as nullable pointers, pointer arithmetic for many-item pointers, postfix dereference `^`, and trapping optional unwrap `?` - pointer-to-array `.len`, indexing, slicing, `.ptr` on slices and pointers-to-arrays, implicit address-taking for array-variable slices, and pointer/slice sentinel weakening @@ -73,7 +74,7 @@ roadmap and milestone history. ### standard packages -- `std/mem` allocator contract over byte allocation: `Allocator` with `?*mut anyopaque` context plus `alloc`, `realloc`, and `free`; failed nonzero reallocation preserves the original allocation, while zero size frees it +- `std/mem` allocator contract with a context pointer plus shared `AllocatorVTable`, raw byte operations `raw_alloc` / `raw_realloc` / `raw_free`, and fallible typed `alloc(T, allocator, count)`; failed nonzero raw reallocation preserves the original allocation, while zero size frees it ### compiler behavior diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index 4d49533..5d1c8bd 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -281,13 +281,15 @@ is_ptr_cast_call :: proc(checker: ^Checker, expr: ast.Expr) -> bool { symbol_text(checker, expr.name) == "ptr_cast" } -Layout_Builtin :: enum u8 { +Type_Builtin :: enum u8 { None, Size_Of, Align_Of, + Min_Value, + Max_Value, } -layout_builtin_call :: proc(checker: ^Checker, expr: ast.Expr) -> Layout_Builtin { +type_builtin_call :: proc(checker: ^Checker, expr: ast.Expr) -> Type_Builtin { if expr.kind != .Call || expr.left != ast.INVALID_EXPR || symbol.is_valid(expr.qualifier) { return .None } @@ -298,6 +300,12 @@ layout_builtin_call :: proc(checker: ^Checker, expr: ast.Expr) -> Layout_Builtin if name == "align_of" { return .Align_Of } + if name == "min_value" { + return .Min_Value + } + if name == "max_value" { + return .Max_Value + } return .None } @@ -314,21 +322,30 @@ 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 { +type_builtin_value :: proc(checker: ^Checker, kind: Type_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 .Min_Value: + if types.is_unsigned(value, checker.target) { + return 0 + } + return -(i128(1) << u32(types.bits(value, checker.target)-1)) + case .Max_Value: + bit_count := types.bits(value, checker.target) + sign_bit_count := 1 if types.is_signed(value, checker.target) else 0 + return (i128(1) << u32(bit_count-sign_bit_count))-1 case: return 0 } } -build_layout_builtin :: proc( +build_type_builtin :: proc( checker: ^Checker, expr: ast.Expr, - kind: Layout_Builtin, + kind: Type_Builtin, pkg: ast.Package_Id, file: ast.File_Id, ) -> hir.Expr_Id { @@ -338,18 +355,24 @@ build_layout_builtin :: proc( } 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") + label := "layout" if kind == .Size_Of || kind == .Align_Of else "integer bound" + id := source.addf(checker.diagnostics, checker.ast_module.exprs[expr.args[0]].span, "%s target must be a type", label) return invalid_hir_expr(checker, expr.span, id, types.USIZE) } - if !valid_layout_type(checker, target) { + if (kind == .Size_Of || kind == .Align_Of) && !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) } + if (kind == .Min_Value || kind == .Max_Value) && !types.is_concrete_integer(target) { + id := source.addf(checker.diagnostics, checker.ast_module.exprs[expr.args[0]].span, "integer bound target must be a concrete integer type, got %s", type_label(checker, target)) + return invalid_hir_expr(checker, expr.span, id, types.USIZE) + } + result_type := types.USIZE if kind == .Size_Of || kind == .Align_Of else target return build_constant_expr( checker, expr, - Constant{kind=.Value, value=layout_builtin_value(checker, kind, target)}, - types.USIZE, + Constant{kind=.Value, value=type_builtin_value(checker, kind, target)}, + result_type, ) } @@ -2201,8 +2224,15 @@ infer_expr :: proc( } continue } - if builtin := layout_builtin_call(checker, expr); builtin != .None { - last = types.USIZE + if builtin := type_builtin_call(checker, expr); builtin != .None { + if builtin == .Size_Of || builtin == .Align_Of { + last = types.USIZE + } else if len(expr.args) == 1 { + target, ok := resolve_type_argument(checker, expr.args[0], pkg, file) + last = target if ok && types.is_concrete_integer(target) else types.INVALID + } else { + last = types.INVALID + } _ = pop(&stack) continue } @@ -3188,10 +3218,19 @@ 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 + if global.expr != ast.INVALID_EXPR && int(global.expr) < len(checker.ast_module.exprs) { + expr := checker.ast_module.exprs[global.expr] + if builtin := type_builtin_call(checker, expr); builtin != .None { + if builtin == .Size_Of || builtin == .Align_Of { + checker.global_types[index] = types.USIZE + } else if len(expr.args) == 1 { + target, ok := resolve_type_argument(checker, expr.args[0], global.pkg, global.file) + if ok && types.is_concrete_integer(target) { + checker.global_types[index] = target + } + } + continue + } } constant := eval_integer_constant_in_context(checker, global.expr, global.pkg, global.file) if constant.kind == .Value && fits_i64(constant.value) { @@ -4998,8 +5037,8 @@ 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) + if builtin := type_builtin_call(checker, expr); builtin != .None { + last = build_type_builtin(checker, expr, builtin, pkg, file) _ = pop(&stack) continue } diff --git a/compiler/checker/comptime.odin b/compiler/checker/comptime.odin index 74fc34b..5ea2060 100644 --- a/compiler/checker/comptime.odin +++ b/compiler/checker/comptime.odin @@ -1909,18 +1909,23 @@ 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 builtin := type_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") + label := "layout" if builtin == .Size_Of || builtin == .Align_Of else "integer bound" + return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, checker.ast_module.exprs[expr.args[0]].span, "%s target must be a type", label) } - if !valid_layout_type(checker, target) { + if (builtin == .Size_Of || builtin == .Align_Of) && !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 + if (builtin == .Min_Value || builtin == .Max_Value) && !types.is_concrete_integer(target) { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, checker.ast_module.exprs[expr.args[0]].span, "integer bound target must be a concrete integer type, got %s", type_label(checker, target)) + } + result_type := types.USIZE if builtin == .Size_Of || builtin == .Align_Of else target + return ct_add_value(state, Ct_Value{kind=.Integer, type=result_type, integer=type_builtin_value(checker, builtin, target)}), ct_flow(.Normal), true } target_pkg, available := expr_package(checker, expr, state.pkg, state.file, false) if !available { diff --git a/compiler_tests.odin b/compiler_tests.odin index 1abf768..c43c40c 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -1413,6 +1413,83 @@ main func() i32 { testing.expect_value(t, state.exit_code, 0) } +@(test) +integer_bound_builtins_compile_and_run :: proc(t: ^testing.T) { + directory := "/tmp/brolang-test-integer-bounds" + main_path := "/tmp/brolang-test-integer-bounds/main.bro" + output := "/tmp/brolang-test-integer-bounds-output" + text := `MAX_U64 u64 :: max_value(u64) + +maximum func($T type) T { + return max_value(T) +} + +main func() i32 { + if (min_value(i8) != -128) return 1 + if (max_value(i8) != 127) return 2 + if (min_value(u8) != 0) return 3 + if (max_value(u8) != 255) return 4 + if (min_value(isize) != -9223372036854775808) return 5 + if (max_value(usize) != 18446744073709551615) return 6 + if (MAX_U64 != 18446744073709551615) return 7 + if (maximum(u16) != 65535) return 8 + if (min_value(c_int) != -2147483648) return 9 + if (max_value(c_ulong) != 18446744073709551615) return 10 + 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) +integer_bound_builtins_reject_invalid_targets :: proc(t: ^testing.T) { + text := `Named :: distinct u8 +Choice :: enum { one } + +main func() void { + _ = min_value() + _ = max_value(u8, u16) + _ = min_value(1) + _ = max_value(int) + _ = max_value(f32) + _ = max_value(bool) + _ = max_value(Named) + _ = max_value(Choice) +} +` + 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_arity := 0 + bad_type := false + bad_target := 0 + for diagnostic in diagnostics.items { + bad_arity += 1 if strings.contains(diagnostic.message, "expects 1 argument") else 0 + bad_type = bad_type || strings.contains(diagnostic.message, "integer bound target must be a type") + bad_target += 1 if strings.contains(diagnostic.message, "integer bound target must be a concrete integer type") else 0 + } + testing.expect_value(t, bad_arity, 2) + testing.expect(t, bad_type) + testing.expect_value(t, bad_target, 5) +} + @(test) layout_builtins_reject_unsized_targets :: proc(t: ^testing.T) { text := `Opaque :: opaque @@ -3281,6 +3358,7 @@ allocator_contract_c_allocator_global_lowers :: proc(t: ^testing.T) { found_c_allocator := false found_anyopaque_context := false + found_vtable_pointer := false found_alloc_callback := false found_realloc_callback := false found_free_callback := false @@ -3291,7 +3369,6 @@ allocator_contract_c_allocator_global_lowers :: proc(t: ^testing.T) { 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) if name == "context" { optional_item, optional_ok := types.node(&hir_module.types, field.type) if optional_ok && optional_item.kind == .Optional { @@ -3302,10 +3379,19 @@ allocator_contract_c_allocator_global_lowers :: proc(t: ^testing.T) { pointer_item.many && pointer_item.child == types.ANYOPAQUE } + } else if name == "vtable" { + pointer_item, pointer_ok := types.node(&hir_module.types, field.type) + found_vtable_pointer = pointer_ok && pointer_item.kind == .Pointer && !pointer_item.mutable && !pointer_item.many + if found_vtable_pointer { + for callback in types.fields_for(&hir_module.types, pointer_item.child) { + callback_name := symbol.resolve(&symbols, symbol.Id(callback.name)) + callback_pointer, _, _, callable := types.function_pointer(callback.type, &hir_module.types) + found_alloc_callback = found_alloc_callback || callback_name == "alloc" && callable && !callback_pointer.many + found_realloc_callback = found_realloc_callback || callback_name == "realloc" && callable && !callback_pointer.many + found_free_callback = found_free_callback || callback_name == "free" && callable && !callback_pointer.many + } + } } - found_alloc_callback = found_alloc_callback || name == "alloc" && callable && !callback_pointer.many - found_realloc_callback = found_realloc_callback || name == "realloc" && callable && !callback_pointer.many - found_free_callback = found_free_callback || name == "free" && callable && !callback_pointer.many } } @@ -3313,6 +3399,7 @@ allocator_contract_c_allocator_global_lowers :: proc(t: ^testing.T) { testing.expect(t, len(ir_module.functions) > 0) testing.expect(t, found_c_allocator) testing.expect(t, found_anyopaque_context) + testing.expect(t, found_vtable_pointer) testing.expect(t, found_alloc_callback) testing.expect(t, found_realloc_callback) testing.expect(t, found_free_callback) diff --git a/examples/programs/mem_allocator/main.bro b/examples/programs/mem_allocator/main.bro index efb65f8..5caafdb 100644 --- a/examples/programs/mem_allocator/main.bro +++ b/examples/programs/mem_allocator/main.bro @@ -1,5 +1,32 @@ mem :: import "@std/mem" +_probe_count func(context ?*mut anyopaque) void { + if context |raw| { + counts *mut usize :: ptr_cast(usize, raw) + counts[0] += 1 + } +} + +_probe_alloc func(context ?*mut anyopaque, _ usize, _ usize) ?*mut u8 { + _probe_count(context) + return none +} + +_probe_realloc func(context ?*mut anyopaque, _ ?*mut u8, _ usize, _ usize, _ usize) ?*mut u8 { + _probe_count(context) + return none +} + +_probe_free func(context ?*mut anyopaque, _ ?*mut u8, _ usize, _ usize) void { + _probe_count(context) +} + +_probe_vtable mem.AllocatorVTable :: mem.AllocatorVTable { + alloc = _probe_alloc, + realloc = _probe_realloc, + free = _probe_free, +} + TaskList :: struct { ids ?[]mut i32 priorities ?[]mut i32 @@ -21,17 +48,19 @@ task_list_init func(allocator mem.Allocator) TaskList { } 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] + fallback [1]mut i32 = undefined + failed bool = false + values []mut i32 = mem.alloc(i32, allocator, count) catch |_| { + failed = true + yield (&fallback).ptr[..0] } - return none + if (failed) return none + return values } 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)) + mem.raw_free(allocator, ptr_cast(u8, slice.ptr), slice.len * size_of(i32), align_of(i32)) } } @@ -150,7 +179,68 @@ task_list_deinit func(list @mut TaskList) void { } main func() i32 { - resized ?*mut u8 = mem.realloc(mem.c_allocator, none, 0, 4, 1) + if (size_of(mem.Allocator) != 16) return 31 + + first_calls [1]mut usize = [0] + second_calls [1]mut usize = [0] + first_allocator mem.Allocator :: mem.Allocator { + context = (&first_calls).ptr, + vtable = &_probe_vtable, + } + second_allocator mem.Allocator :: mem.Allocator { + context = (&second_calls).ptr, + vtable = &_probe_vtable, + } + + _ = mem.raw_alloc(first_allocator, 1, 1) + _ = mem.raw_realloc(first_allocator, none, 0, 1, 1) + mem.raw_free(first_allocator, none, 0, 1) + _ = mem.raw_alloc(second_allocator, 1, 1) + if (first_calls[0] != 3 or second_calls[0] != 1) return 32 + + i32_fallback [1]mut i32 = undefined + empty_failed bool = false + empty []mut i32 = mem.alloc(i32, first_allocator, 0) catch |_| { + empty_failed = true + yield (&i32_fallback).ptr[..0] + } + if (empty_failed or empty.len != 0 or first_calls[0] != 3) return 34 + + zero_sized_fallback [1]mut [0]u8 = undefined + zero_sized_failed bool = false + zero_sized []mut [0]u8 = mem.alloc([0]u8, first_allocator, 3) catch |_| { + zero_sized_failed = true + yield (&zero_sized_fallback).ptr[..0] + } + if (zero_sized_failed or zero_sized.len != 3 or first_calls[0] != 3) return 36 + _ = zero_sized[2] + + u64_fallback [1]mut u64 = undefined + overflow_fallback_failed bool = false + overflow_fallback []mut u64 = mem.alloc(u64, first_allocator, 0) catch |_| { + overflow_fallback_failed = true + yield (&u64_fallback).ptr[..0] + } + if (overflow_fallback_failed) return 37 + overflow_failed bool = false + _ = mem.alloc(u64, first_allocator, max_value(usize)) catch |_| { + overflow_failed = true + yield overflow_fallback + } + if (overflow_failed == false or first_calls[0] != 3) return 40 + + typed_failed bool = false + typed []mut i32 = mem.alloc(i32, mem.c_allocator, 4) catch |_| { + typed_failed = true + yield (&i32_fallback).ptr[..0] + } + if (typed_failed) return 38 + defer mem.raw_free(mem.c_allocator, ptr_cast(u8, typed.ptr), typed.len * size_of(i32), align_of(i32)) + typed[0] = 10 + typed[3] = 20 + if (typed[0] + typed[3] != 30) return 39 + + resized ?*mut u8 = mem.raw_realloc(mem.c_allocator, none, 0, 4, 1) if resized |bytes| { bytes[0] = 10 bytes[1] = 20 @@ -160,82 +250,82 @@ main func() i32 { return 20 } - grown ?*mut u8 = mem.realloc(mem.c_allocator, resized, 4, 8, 1) + grown ?*mut u8 = mem.raw_realloc(mem.c_allocator, resized, 4, 8, 1) if grown |bytes| { resized = grown if bytes[0] != 10 or bytes[1] != 20 or bytes[2] != 30 or bytes[3] != 40 { - mem.free(mem.c_allocator, grown, 8, 1) + mem.raw_free(mem.c_allocator, grown, 8, 1) return 21 } } else { - mem.free(mem.c_allocator, resized, 4, 1) + mem.raw_free(mem.c_allocator, resized, 4, 1) return 22 } - shrunk ?*mut u8 = mem.realloc(mem.c_allocator, resized, 8, 2, 1) + shrunk ?*mut u8 = mem.raw_realloc(mem.c_allocator, resized, 8, 2, 1) if shrunk |bytes| { resized = shrunk if bytes[0] != 10 or bytes[1] != 20 { - mem.free(mem.c_allocator, shrunk, 2, 1) + mem.raw_free(mem.c_allocator, shrunk, 2, 1) return 23 } } else { - mem.free(mem.c_allocator, resized, 8, 1) + mem.raw_free(mem.c_allocator, resized, 8, 1) return 24 } - invalid ?*mut u8 = mem.realloc(mem.c_allocator, resized, 2, 4, 24) + invalid ?*mut u8 = mem.raw_realloc(mem.c_allocator, resized, 2, 4, 24) if invalid |memory| { - mem.free(mem.c_allocator, memory, 4, 24) - mem.free(mem.c_allocator, resized, 2, 1) + mem.raw_free(mem.c_allocator, memory, 4, 24) + mem.raw_free(mem.c_allocator, resized, 2, 1) return 25 } if resized |bytes| { if bytes[0] != 10 or bytes[1] != 20 { - mem.free(mem.c_allocator, resized, 2, 1) + mem.raw_free(mem.c_allocator, resized, 2, 1) return 26 } } - resized = mem.realloc(mem.c_allocator, resized, 2, 0, 1) + resized = mem.raw_realloc(mem.c_allocator, resized, 2, 0, 1) if resized |memory| { - mem.free(mem.c_allocator, memory, 0, 1) + mem.raw_free(mem.c_allocator, memory, 0, 1) return 27 } - over_aligned ?*mut u8 = mem.alloc(mem.c_allocator, 4, 32) + over_aligned ?*mut u8 = mem.raw_alloc(mem.c_allocator, 4, 32) if over_aligned |bytes| { bytes[0] = 11 bytes[1] = 22 } else { return 28 } - over_aligned_grown ?*mut u8 = mem.realloc(mem.c_allocator, over_aligned, 4, 8, 32) + over_aligned_grown ?*mut u8 = mem.raw_realloc(mem.c_allocator, over_aligned, 4, 8, 32) if over_aligned_grown |bytes| { if bytes[0] != 11 or bytes[1] != 22 { - mem.free(mem.c_allocator, over_aligned_grown, 8, 32) + mem.raw_free(mem.c_allocator, over_aligned_grown, 8, 32) return 29 } - mem.free(mem.c_allocator, over_aligned_grown, 8, 32) + mem.raw_free(mem.c_allocator, over_aligned_grown, 8, 32) } else { - mem.free(mem.c_allocator, over_aligned, 4, 32) + mem.raw_free(mem.c_allocator, over_aligned, 4, 32) return 30 } - zero_alignment ?*mut u8 = mem.alloc(mem.c_allocator, 8, 0) + zero_alignment ?*mut u8 = mem.raw_alloc(mem.c_allocator, 8, 0) if zero_alignment |memory| { - mem.free(mem.c_allocator, memory, 8, 0) + mem.raw_free(mem.c_allocator, memory, 8, 0) return 1 } - bad_alignment ?*mut u8 = mem.alloc(mem.c_allocator, 8, 24) + bad_alignment ?*mut u8 = mem.raw_alloc(mem.c_allocator, 8, 24) if bad_alignment |memory| { - mem.free(mem.c_allocator, memory, 8, 24) + mem.raw_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) + aligned ?*mut u8 = mem.raw_alloc(mem.c_allocator, 64, 32) + defer mem.raw_free(mem.c_allocator, aligned, 64, 32) if aligned |bytes| { bytes[0] = 1 bytes[63] = 2 diff --git a/std/mem/mem.bro b/std/mem/mem.bro index 55aa389..b62dae9 100644 --- a/std/mem/mem.bro +++ b/std/mem/mem.bro @@ -1,22 +1,58 @@ c :: import "@ffi/c" -Allocator :: struct { - context ?*mut anyopaque +AllocatorVTable :: struct { alloc @func(context ?*mut anyopaque, size usize, alignment usize) ?*mut u8 realloc @func(context ?*mut anyopaque, memory ?*mut u8, old_size usize, new_size usize, alignment usize) ?*mut u8 free @func(context ?*mut anyopaque, memory ?*mut u8, size usize, alignment usize) void } -alloc func(allocator Allocator, size usize, alignment usize) ?*mut u8 { - return allocator.alloc(allocator.context, size, alignment) +Allocator :: struct { + context ?*mut anyopaque + vtable @AllocatorVTable } -realloc func(allocator Allocator, memory ?*mut u8, old_size usize, new_size usize, alignment usize) ?*mut u8 { - return allocator.realloc(allocator.context, memory, old_size, new_size, alignment) +AllocError :: enum { + out_of_memory } -free func(allocator Allocator, memory ?*mut u8, size usize, alignment usize) void { - allocator.free(allocator.context, memory, size, alignment) +raw_alloc func(allocator Allocator, size usize, alignment usize) ?*mut u8 { + return allocator.vtable.alloc(allocator.context, size, alignment) +} + +raw_realloc func(allocator Allocator, memory ?*mut u8, old_size usize, new_size usize, alignment usize) ?*mut u8 { + return allocator.vtable.realloc(allocator.context, memory, old_size, new_size, alignment) +} + +raw_free func(allocator Allocator, memory ?*mut u8, size usize, alignment usize) void { + allocator.vtable.free(allocator.context, memory, size, alignment) +} + +_empty_storage [1]mut u64 = [0] + +_empty_slice func($T type, count usize) []mut T { + pointer *mut T :: ptr_cast(T, (&_empty_storage).ptr) + return pointer[..count] +} + +alloc func($T type, allocator Allocator, count usize) []mut T ! AllocError { + if count == 0 { + return _empty_slice(T, 0) + } + + element_size usize :: size_of(T) + if element_size == 0 { + return _empty_slice(T, count) + } + if count > max_value(usize) / element_size { + return .out_of_memory + } + + memory ?*mut u8 = raw_alloc(allocator, count * element_size, align_of(T)) + if memory |bytes| { + pointer *mut T :: ptr_cast(T, bytes) + return pointer[..count] + } + return .out_of_memory } _malloc_alignment usize :: 16 # ponytail: aarch64-macos libc malloc alignment assumption. @@ -93,9 +129,13 @@ _c_free func(_ ?*mut anyopaque, memory ?*mut u8, _ usize, _ usize) void { c.free(memory) } -c_allocator Allocator :: Allocator { - context = none, +_c_vtable AllocatorVTable :: AllocatorVTable { alloc = _c_alloc, realloc = _c_realloc, free = _c_free, } + +c_allocator Allocator :: Allocator { + context = none, + vtable = &_c_vtable, +}