allocator interface (first pass)

This commit is contained in:
2026-07-06 21:24:49 +02:00
parent 7ce3917c15
commit 95c61311ca
24 changed files with 833 additions and 161 deletions
+256 -9
View File
@@ -298,7 +298,7 @@ parser_accepts_native_function_pointer_types :: proc(t: ^testing.T) {
text := `Error :: enum {
bad
}
take func(callback ?*func(value i32) i32, fallible *func() i32 ! Error) void
take func(callback ?@func(value i32) i32, fallible @func() i32 ! Error) void
main func() void {}
`
source_file := source.Source{path="test.bro", text=text}
@@ -320,12 +320,12 @@ main func() void {}
fallible, fallible_ok := types.node(&module.type_store, fallible_function.child)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, optional_ok && optional.kind == .Optional)
testing.expect(t, pointer_ok && pointer.kind == .Pointer && pointer.many && !pointer.mutable)
testing.expect(t, pointer_ok && pointer.kind == .Pointer && !pointer.many && !pointer.mutable)
testing.expect(t, function_ok && function.kind == .Function && !function.c_abi && !function.variadic)
testing.expect(t, function.child == types.I32)
testing.expect_value(t, len(params), 1)
testing.expect(t, params[0].type == types.I32)
testing.expect(t, fallible_pointer_ok && fallible_pointer.kind == .Pointer)
testing.expect(t, fallible_pointer_ok && fallible_pointer.kind == .Pointer && !fallible_pointer.many)
testing.expect(t, fallible_function_ok && fallible_function.kind == .Function && !fallible_function.c_abi)
testing.expect(t, fallible_ok && fallible.kind == .Fallible && fallible.child == types.I32)
}
@@ -1526,11 +1526,12 @@ variadicness_is_part_of_c_function_signature_compatibility :: proc(t: ^testing.T
}
@(test)
c_structs_are_by_value_and_may_be_opaque :: proc(t: ^testing.T) {
c_structs_are_by_value_and_bodyless_c_struct_uses_opaque :: proc(t: ^testing.T) {
text := `Defined :: c_struct {
value c_int
}
Opaque :: c_struct
Opaque :: opaque
Bodyless :: c_struct
Empty :: c_struct {}
Bad :: c_struct {
values []i32
@@ -1555,18 +1556,117 @@ main func() void {
defer hir.destroy_module(&hir_module)
found_opaque := false
found_bodyless := false
found_bad_layout := false
found_empty := false
for diagnostic in diagnostics.items {
found_opaque = found_opaque || strings.contains(diagnostic.message, "cannot be passed by value")
found_bodyless = found_bodyless || strings.contains(diagnostic.message, "use 'opaque'")
found_bad_layout = found_bad_layout || strings.contains(diagnostic.message, "C-layout-compatible")
found_empty = found_empty || strings.contains(diagnostic.message, "at least one field")
}
testing.expect(t, found_opaque)
testing.expect(t, found_bodyless)
testing.expect(t, found_bad_layout)
testing.expect(t, found_empty)
}
@(test)
opaque_anyopaque_and_ptr_cast_compile_and_lower :: proc(t: ^testing.T) {
text := `Handle :: opaque
take func(value ?*mut anyopaque) void {}
use_handle func(handle ?@mut Handle) void {}
main func() void {
values [2]mut u8 = [1, 2]
raw ?*mut anyopaque = (&values).ptr
bytes ?*mut u8 = ptr_cast(u8, raw)
take(bytes)
if bytes |p| {
p[1] = 5
}
one u8 = 1
single ?@mut anyopaque = &one
typed ?@mut u8 = ptr_cast(u8, single)
if typed |p| {
p^ = 2
}
handle ?@mut Handle = none
use_handle(handle)
}
`
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)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
hir_casts := 0
for expr in hir_module.exprs {
hir_casts += 1 if expr.kind == .Pointer_Cast else 0
}
ir_casts := 0
for function in ir_module.functions {
for instruction in function.instructions {
ir_casts += 1 if instruction.op == .Pointer_Cast else 0
}
}
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, hir_casts, 2)
testing.expect_value(t, ir_casts, 2)
}
@(test)
anyopaque_by_value_and_invalid_ptr_casts_are_rejected :: proc(t: ^testing.T) {
text := `Callback :: alias c_func() void
main func() void {
raw ?*mut anyopaque = none
value anyopaque = undefined
_ = ptr_cast(void, raw)
_ = ptr_cast(anyopaque, raw)
_ = ptr_cast(Callback, raw)
_ = ptr_cast(u8, 1)
_ = ptr_cast(1, raw)
}
`
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)
found_by_value := false
found_bad_target := false
found_bad_operand := false
found_target_type := false
for diagnostic in diagnostics.items {
found_by_value = found_by_value || strings.contains(diagnostic.message, "could not infer a concrete type")
found_bad_target = found_bad_target || strings.contains(diagnostic.message, "ptr_cast target must be a sized runtime object type")
found_bad_operand = found_bad_operand || strings.contains(diagnostic.message, "ptr_cast operand must be a pointer")
found_target_type = found_target_type || strings.contains(diagnostic.message, "ptr_cast target must be a type")
}
testing.expect(t, found_by_value)
testing.expect(t, found_bad_target)
testing.expect(t, found_bad_operand)
testing.expect(t, found_target_type)
}
@(test)
aarch64_c_record_abi_classifies_fixed_parameters_and_results :: proc(t: ^testing.T) {
text := `Small :: c_struct {
@@ -2448,7 +2548,7 @@ main func() void {
@(test)
native_function_pointer_type_restrictions_are_diagnosed :: proc(t: ^testing.T) {
text := `main func() void {
callback *func(...) void = undefined
callback @func(...) void = undefined
}
`
source_file := source.Source{path="test.bro", text=text}
@@ -2812,6 +2912,140 @@ milestone_24_rejects_invalid_forms :: proc(t: ^testing.T) {
}
}
@(test)
field_function_pointer_calls_lower_as_indirect_calls :: proc(t: ^testing.T) {
text := `Callbacks :: struct {
call @func(value i32) i32
value i32
}
plus_one func(value i32) i32 {
return value + 1
}
run func(callbacks Callbacks) i32 {
return callbacks.call(callbacks.value)
}
main func() i32 {
callbacks Callbacks = Callbacks { call = plus_one, value = 41 }
return run(callbacks) - 42
}
`
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)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
indirect_calls := 0
for expr in hir_module.exprs {
if expr.kind == .Call && expr.left != hir.INVALID_EXPR {
indirect_calls += 1
}
}
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, len(ir_module.functions) > 0)
testing.expect(t, indirect_calls > 0)
}
@(test)
field_function_pointer_calls_reject_non_callable_fields :: proc(t: ^testing.T) {
text := `Box :: struct {
value i32
}
main func() void {
box Box = Box { value = 1 }
box.value()
}
`
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)
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, "call target is not a function pointer")
}
testing.expect(t, found)
}
@(test)
allocator_contract_compiles_and_runs :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-mem-allocator"
defer _ = os.remove(output)
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)
allocator_contract_heap_global_lowers :: 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/mem_allocator", &sources, &diagnostics, &symbols, context.allocator, context.allocator, cimport.Options{}, target.DEFAULT, ".")
defer ast.destroy_module(&ast_module)
testing.expect(t, loaded)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
found_heap := 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" {
continue
}
found_heap = 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 {
pointer_item, pointer_ok := types.node(&hir_module.types, optional_item.child)
found_anyopaque_context = pointer_ok &&
pointer_item.kind == .Pointer &&
pointer_item.mutable &&
pointer_item.many &&
pointer_item.child == types.ANYOPAQUE
}
}
found_alloc_callback = found_alloc_callback || name == "alloc" && callable && !callback_pointer.many
found_free_callback = found_free_callback || name == "free" && callable && !callback_pointer.many
}
}
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_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"
@@ -8571,7 +8805,7 @@ main func() i32 {
@(test)
distinct_types_reject_implicit_conversions_operators_and_invalid_backings :: proc(t: ^testing.T) {
text := `Opaque :: c_struct
text := `Opaque :: opaque
UserID :: distinct u32
OtherID :: distinct u32
BadInt :: distinct int
@@ -8881,13 +9115,16 @@ translate_c_emits_native_bindings_and_round_trips :: proc(t: ^testing.T) {
result := cimport.init_result(context.allocator)
// type table: [0]=c_int, [1]=c_ulong, [2]=Record(Pair),
// [3]=Function(c_int)->c_int, [4]=Pointer->Function (callback)
// [3]=Function(c_int)->c_int, [4]=Pointer->Function (callback),
// [5]=void, [6]=Pointer->void
append(&result.types, cimport.Type{kind = .C_Int, child = cimport.INVALID_TYPE})
append(&result.types, cimport.Type{kind = .C_Ulong, child = cimport.INVALID_TYPE})
append(&result.types, cimport.Type{kind = .Record, record = 0, child = cimport.INVALID_TYPE})
func_params := []cimport.Type_Id{cimport.Type_Id(0)}
append(&result.types, cimport.Type{kind = .Function, params = func_params, child = cimport.Type_Id(0)})
append(&result.types, cimport.Type{kind = .Pointer, child = cimport.Type_Id(3)})
append(&result.types, cimport.Type{kind = .Void, child = cimport.INVALID_TYPE})
append(&result.types, cimport.Type{kind = .Pointer, child = cimport.Type_Id(5), mutable = true})
// record 0: Pair { left c_int; right c_int }
pair_fields: [dynamic]cimport.Field
@@ -8900,13 +9137,20 @@ translate_c_emits_native_bindings_and_round_trips :: proc(t: ^testing.T) {
append(&choice_fields, cimport.Field{name = "tag", type = cimport.Type_Id(0)})
append(&result.records, cimport.Record{name = "Choice", fields = choice_fields, kind = .Union, complete = true})
// typedef aliases: a scalar and a callback function pointer
// record 2: incomplete struct -> opaque
append(&result.records, cimport.Record{name = "Handle", kind = .Struct, complete = false})
// typedef aliases: a scalar, a callback function pointer, and a C void pointer
append(&result.aliases, cimport.Alias{name = "Size", type = cimport.Type_Id(1)})
append(&result.aliases, cimport.Alias{name = "Mapper", type = cimport.Type_Id(4)})
append(&result.aliases, cimport.Alias{name = "RawPtr", type = cimport.Type_Id(6)})
add_params := []cimport.Type_Id{cimport.Type_Id(0), cimport.Type_Id(0)}
add_param_names := []string{"a", "b"}
append(&result.functions, cimport.Function{name = "imported_add", params = add_params, param_names = add_param_names, result = cimport.Type_Id(0)})
raw_params := []cimport.Type_Id{cimport.Type_Id(6)}
raw_param_names := []string{"ptr"}
append(&result.functions, cimport.Function{name = "consume_raw", params = raw_params, param_names = raw_param_names, result = cimport.Type_Id(5)})
append(&result.macros, cimport.Macro_Constant{
name = "MAX_LEN",
@@ -8938,8 +9182,11 @@ translate_c_emits_native_bindings_and_round_trips :: proc(t: ^testing.T) {
testing.expect(t, strings.contains(output, "Size :: alias c_ulong"))
// Function-pointer types carry no parameter names, so the callback renders `_`.
testing.expect(t, strings.contains(output, "Mapper :: alias ?*c_func(_ c_int) c_int"))
testing.expect(t, strings.contains(output, "RawPtr :: alias ?*mut anyopaque"))
testing.expect(t, strings.contains(output, "Handle :: opaque"))
// Real C parameter names are used when present.
testing.expect(t, strings.contains(output, "imported_add c_func(a c_int, b c_int) c_int"))
testing.expect(t, strings.contains(output, "consume_raw c_func(ptr ?*mut anyopaque) void"))
testing.expect(t, strings.contains(output, "MAX_LEN c_int :: 256"))
testing.expect(t, strings.contains(output, "# unsupported in bindings: C union 'Choice'"))
testing.expect(t, strings.contains(output, "# unsupported in bindings: external variable 'some_global'"))