add constcast and immutable free

This commit is contained in:
2026-07-22 00:44:35 +02:00
parent 5f343ad2d3
commit 9c6215776e
12 changed files with 256 additions and 11 deletions
+1
View File
@@ -34,6 +34,7 @@ roadmap and milestone history.
- pointer mutability via `mut`, optional pointers as nullable pointers, pointer arithmetic for many-item pointers, postfix dereference `^`, and trapping optional unwrap `?` - 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 - 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
- `ptrcast!(T, ptr)` as a first-pass pointer-child retype that preserves optionality, pointer kind, mutability, and sentinel shape - `ptrcast!(T, ptr)` as a first-pass pointer-child retype that preserves optionality, pointer kind, mutability, and sentinel shape
- unsafe `constcast!(value)` for restoring mutability to pointers, optional pointers, and slices without changing their child type or shape
- UTF-8 string literals as immutable pointers to static zero-terminated byte arrays, plus raw backtick multiline strings - UTF-8 string literals as immutable pointers to static zero-terminated byte arrays, plus raw backtick multiline strings
- narrow immutable zero-terminated byte pointer/slice conversion to `*c_char` / `?*c_char` without general `u8`/`c_char` interchange - narrow immutable zero-terminated byte pointer/slice conversion to `*c_char` / `?*c_char` without general `u8`/`c_char` interchange
- optionals with `null`, `orelse`, postfix `?`, conditional unwraps, guarded unwraps, and left-to-right short-circuiting multi-unwraps - optionals with `null`, `orelse`, postfix `?`, conditional unwraps, guarded unwraps, and left-to-right short-circuiting multi-unwraps
+11 -7
View File
@@ -960,6 +960,11 @@
value-producing control-flow forms with or without an error capture value-producing control-flow forms with or without an error capture
- void fallthrough and diverging `noreturn` fallbacks remain valid - void fallthrough and diverging `noreturn` fallbacks remain valid
50. add `constcast!` and immutable deallocation (implemented)
- `constcast!` restores mutability only for pointers, optional pointers, and slices while
preserving child type, pointer kind, optionality, length, and sentinel shape
- `std/mem.free` accepts immutable slices and restores mutability only at the allocator boundary
## A word on unchecked casts ## A word on unchecked casts
For casts that bypass safety checks, Honey provides builtin functions: For casts that bypass safety checks, Honey provides builtin functions:
@@ -968,7 +973,8 @@ For casts that bypass safety checks, Honey provides builtin functions:
| -- | -- | -- | | -- | -- | -- |
| `truncate(x, T)` | Keep low bits, discard rest | Never | | `truncate(x, T)` | Keep low bits, discard rest | Never |
| `bitcast(x, T)` | Reinterpret bits, no cast | Sizes don't match (compile error) | | `bitcast(x, T)` | Reinterpret bits, no cast | Sizes don't match (compile error) |
| `ptrcast!(p, T)` | Change pointer type | Gaining mutability (compile error) | | `ptrcast!(T, p)` | Change a pointer's child type while preserving its shape | Invalid child or non-pointer operand (compile error) |
| `constcast!(p)` | Restore pointer or slice mutability | Non-pointer/slice operand (compile error) |
```honey ```honey
# truncation # truncation
@@ -981,12 +987,10 @@ m := bitcast(n, u32) # m == 0xFFFFFFFF (same bits)
f: f32 = 3.14 f: f32 = 3.14
bits := bitcast(f, u32) # IEEE 754 representation bits := bitcast(f, u32) # IEEE 754 representation
# pointer casts (element type, many ↔ single, pointer ↔ usize) # pointer casts
buf: *u8 = get_buffer() buf *u8 = get_buffer()
ints := ptrcast!(buf, *u32) # element type change ints *u32 = ptrcast!(u32, buf) # element type change, same pointer shape
single := ptrcast!(buf, @u8) # many → single (restricting) writable *mut u8 = constcast!(buf) # explicit unsafe mutability restoration
addr := ptrcast!(buf, usize) # pointer to integer
ptr := ptrcast!(addr, @u8) # integer to pointer
``` ```
## A word on multi-unwrap ## A word on multi-unwrap
+39
View File
@@ -5187,6 +5187,16 @@ infer_expr :: proc(
_ = pop(&stack) _ = pop(&stack)
continue continue
} }
if is_intrinsic_call(checker, expr, "constcast") {
if len(expr.args) == 1 {
operand := infer_nested_expr(checker, expr.args[0], locals, pkg, file, demanded, local_types)
last, _ = types.restore_mutability(&checker.module.types, operand)
} else {
last = types.INVALID
}
_ = pop(&stack)
continue
}
if is_intrinsic_call(checker, expr, "compile_error") { if is_intrinsic_call(checker, expr, "compile_error") {
last = types.VOID last = types.VOID
_ = pop(&stack) _ = pop(&stack)
@@ -9056,6 +9066,17 @@ build_expr :: proc(
append(&stack, Build_Expr_Frame{expr=expr.args[1], expected=types.INVALID, template=ast.INVALID_FUNCTION}) append(&stack, Build_Expr_Frame{expr=expr.args[1], expected=types.INVALID, template=ast.INVALID_FUNCTION})
continue continue
} }
if is_intrinsic_call(checker, expr, "constcast") {
if len(expr.args) != 1 {
id := source.addf(checker.diagnostics, expr.span, "constcast! expects 1 argument, got %d", len(expr.args))
last = invalid_hir_expr(checker, expr.span, id)
_ = pop(&stack)
continue
}
stack[frame_index].stage = 10
append(&stack, Build_Expr_Frame{expr=expr.args[0], expected=types.INVALID, template=ast.INVALID_FUNCTION})
continue
}
if is_intrinsic_call(checker, expr, "compile_error") { if is_intrinsic_call(checker, expr, "compile_error") {
message := "compile_error! requires one comptime string argument" message := "compile_error! requires one comptime string argument"
if len(expr.args) == 1 { if len(expr.args) == 1 {
@@ -9716,6 +9737,24 @@ build_expr :: proc(
} }
_ = pop(&stack) _ = pop(&stack)
} }
if frame.stage == 10 {
result, ok := types.restore_mutability(&checker.module.types, checker.module.exprs[last].type)
if !ok {
id := source.add(checker.diagnostics, expr.span, "constcast! operand must be a pointer, optional pointer, or slice")
last = invalid_hir_expr(checker, expr.span, id)
} else {
last = add_hir_expr(checker, hir.Expr{
kind=.Const_Cast,
span=expr.span,
type=result,
left=last,
target=hir.INVALID_REF,
right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
_ = pop(&stack)
}
} }
return last return last
} }
+3
View File
@@ -3243,6 +3243,9 @@ ct_eval_call_expr :: proc(state: ^Ct_State, expr: ast.Expr, expected: types.Type
if builtin := memory_builtin_call(checker, expr); builtin != .None { if builtin := memory_builtin_call(checker, expr); builtin != .None {
return ct_eval_memory_call(state, expr, builtin, depth+1) return ct_eval_memory_call(state, expr, builtin, depth+1)
} }
if is_intrinsic_call(checker, expr, "constcast") {
return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "constcast! is not available during comptime evaluation")
}
if expr.intrinsic { if expr.intrinsic {
if symbol.is_valid(expr.qualifier) { if symbol.is_valid(expr.qualifier) {
return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "intrinsic calls must be unqualified") return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "intrinsic calls must be unqualified")
+1
View File
@@ -107,6 +107,7 @@ Expr_Kind :: enum u8 {
Retype, Retype,
Scalar_Cast, Scalar_Cast,
Pointer_Cast, Pointer_Cast,
Const_Cast,
Weaken_Pointer, Weaken_Pointer,
Weaken_Slice, Weaken_Slice,
Decay_Array_Pointer, Decay_Array_Pointer,
+1
View File
@@ -102,6 +102,7 @@ Opcode :: enum u8 {
Retype, Retype,
Scalar_Cast, Scalar_Cast,
Pointer_Cast, Pointer_Cast,
Const_Cast,
Weaken_Pointer, Weaken_Pointer,
Weaken_Slice, Weaken_Slice,
Decay_Array_Pointer, Decay_Array_Pointer,
+11 -1
View File
@@ -287,7 +287,7 @@ valid_value :: proc(
.Load_Global, .Function_Address, .Address_Of, .Load, .Union_Tag, .Slice, .Length, .Slice_Ptr, .Load_Global, .Function_Address, .Address_Of, .Load, .Union_Tag, .Slice, .Length, .Slice_Ptr,
.Fallible_Error, .Extract, .Select, .Unwrap, .Fallible_Error, .Extract, .Select, .Unwrap,
.Optional_Is_Some, .Optional_Value, .Orelse, .Optional_Is_Some, .Optional_Value, .Orelse,
.Widen, .Sum_Widen, .C_Coerce, .C_Vararg_Promote, .Retype, .Scalar_Cast, .Pointer_Cast, .Weaken_Pointer, .Weaken_Slice, .Decay_Array_Pointer, .Widen, .Sum_Widen, .C_Coerce, .C_Vararg_Promote, .Retype, .Scalar_Cast, .Pointer_Cast, .Const_Cast, .Weaken_Pointer, .Weaken_Slice, .Decay_Array_Pointer,
.Neg_Checked, .Add_Checked, .Sub_Checked, .Mul_Checked, .Div_Checked, .Neg_Checked, .Add_Checked, .Sub_Checked, .Mul_Checked, .Div_Checked,
.Div_Trunc_Checked, .Div_Floor_Checked, .Div_Exact_Checked, .Div_Ceil_Checked, .Div_Trunc_Checked, .Div_Floor_Checked, .Div_Exact_Checked, .Div_Ceil_Checked,
.Rem_Checked, .Mod_Checked, .Rem_Checked, .Mod_Checked,
@@ -1930,6 +1930,16 @@ emit_instruction_stream :: proc(
continue continue
} }
fmt.sbprintf(&emitter.builder, " %%v%d = select i1 true, ptr %%v%d, ptr null\n", instruction_index, instruction.a) fmt.sbprintf(&emitter.builder, " %%v%d = select i1 true, ptr %%v%d, ptr null\n", instruction_index, instruction.a)
case .Const_Cast:
if !valid_instruction(instructions, instruction.a) ||
!types.same_constcast_shape(instructions[instruction.a].type, instruction.type, &emitter.module.types) {
emit_recovery_value(emitter, instruction_index, instruction, "invalid const cast operand")
continue
}
type_name := llvm_type(instruction.type, &emitter.module.types)
fmt.sbprintf(&emitter.builder, " %%v%d = select i1 true, %s ", instruction_index, type_name)
write_operand(&emitter.builder, instructions, instruction.a, instructions[instruction.a].type, &emitter.module.types)
fmt.sbprintf(&emitter.builder, ", %s zeroinitializer\n", type_name)
case .Weaken_Slice: case .Weaken_Slice:
if !valid_instruction(instructions, instruction.a) || if !valid_instruction(instructions, instruction.a) ||
!types.can_weaken_slice(instructions[instruction.a].type, instruction.type, &emitter.module.types) { !types.can_weaken_slice(instructions[instruction.a].type, instruction.type, &emitter.module.types) {
+2 -1
View File
@@ -768,7 +768,7 @@ lower_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instruction_Id {
}) })
} }
_ = pop(&stack) _ = pop(&stack)
case .Widen, .Sum_Widen, .C_Coerce, .C_Vararg_Promote, .Retype, .Scalar_Cast, .Pointer_Cast, .Weaken_Pointer, .Weaken_Slice, .Decay_Array_Pointer: case .Widen, .Sum_Widen, .C_Coerce, .C_Vararg_Promote, .Retype, .Scalar_Cast, .Pointer_Cast, .Const_Cast, .Weaken_Pointer, .Weaken_Slice, .Decay_Array_Pointer:
stack[frame_index].stage = 1 stack[frame_index].stage = 1
append(&stack, Lower_Expr_Frame{expr=expr.left}) append(&stack, Lower_Expr_Frame{expr=expr.left})
case .Negate, .Bit_Not: case .Negate, .Bit_Not:
@@ -837,6 +837,7 @@ lower_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instruction_Id {
case .Retype: op = .Retype case .Retype: op = .Retype
case .Scalar_Cast: op = .Scalar_Cast case .Scalar_Cast: op = .Scalar_Cast
case .Pointer_Cast: op = .Pointer_Cast case .Pointer_Cast: op = .Pointer_Cast
case .Const_Cast: op = .Const_Cast
case: op = .Widen case: op = .Widen
} }
last = append_instruction(state, ir.Instruction{ last = append_instruction(state, ir.Instruction{
+39
View File
@@ -1340,6 +1340,45 @@ replace_pointer_child :: proc(store: ^Store, value, child: Type) -> (Type, bool)
return intern(store, item), true return intern(store, item), true
} }
restore_mutability :: proc(store: ^Store, value: Type) -> (Type, bool) {
item, ok := node(store, resolve_alias(value, store))
if !ok {
return INVALID, false
}
if item.kind == .Optional {
restored, restored_ok := restore_mutability(store, item.child)
if !restored_ok || !is_pointer(restored, store) {
return INVALID, false
}
return optional(store, restored), true
}
if item.kind != .Pointer && item.kind != .Slice {
return INVALID, false
}
item.mutable = true
return intern(store, item), true
}
same_constcast_shape :: proc(from, to: Type, store: ^Store) -> bool {
from_item, from_ok := node(store, resolve_alias(from, store))
to_item, to_ok := node(store, resolve_alias(to, store))
if !from_ok || !to_ok {
return false
}
if from_item.kind == .Optional || to_item.kind == .Optional {
return from_item.kind == .Optional && to_item.kind == .Optional &&
is_pointer(from_item.child, store) && is_pointer(to_item.child, store) &&
same_constcast_shape(from_item.child, to_item.child, store)
}
return (from_item.kind == .Pointer || from_item.kind == .Slice) &&
from_item.kind == to_item.kind &&
from_item.child == to_item.child &&
from_item.many == to_item.many &&
to_item.mutable &&
from_item.has_sentinel == to_item.has_sentinel &&
(!from_item.has_sentinel || from_item.sentinel == to_item.sentinel)
}
same_pointer_shape :: proc(left, right: Type, store: ^Store) -> bool { same_pointer_shape :: proc(left, right: Type, store: ^Store) -> bool {
left_item, left_ok := node(store, left) left_item, left_ok := node(store, left)
right_item, right_ok := node(store, right) right_item, right_ok := node(store, right)
+142
View File
@@ -2184,6 +2184,148 @@ main func() void {
testing.expect(t, found_target_type) testing.expect(t, found_target_type)
} }
@(test)
constcast_restores_pointer_and_slice_mutability :: proc(t: ^testing.T) {
text := `main func() i32 {
values [2]mut u8 = [1, 2]
immutable_slice []u8 = values[..]
mutable_slice []mut u8 = constcast!(immutable_slice)
mutable_slice[0] = 3
immutable_many *u8 = immutable_slice.ptr
mutable_many *mut u8 = constcast!(immutable_many)
mutable_many[1] = 4
number i32 = 5
immutable_single @i32 = &number
mutable_single @mut i32 = constcast!(immutable_single)
mutable_single^ = 6
maybe ?@i32 = immutable_single
mutable_maybe ?@mut i32 = constcast!(maybe)
if mutable_maybe |pointer| { pointer^ = 7 }
already_mutable []mut u8 = constcast!(mutable_slice)
if already_mutable[0] != 3 or already_mutable[1] != 4 or number != 7 { return 1 }
return 0
}
`
source_file := source.Source{path="constcast.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, ir_casts := 0, 0
for expr in hir_module.exprs {
hir_casts += 1 if expr.kind == .Const_Cast else 0
}
for function in ir_module.functions {
for instruction in function.instructions {
ir_casts += 1 if instruction.op == .Const_Cast else 0
}
}
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, hir_casts, 5)
testing.expect_value(t, ir_casts, 5)
directory := "/tmp/brolang-test-constcast"
main_path := "/tmp/brolang-test-constcast/main.bro"
output := "/tmp/brolang-test-constcast-output"
_ = 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))
testing.expect_value(t, compiler_core.compile_package(directory, output), 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
invalid_constcasts_are_rejected :: proc(t: ^testing.T) {
text := `main func() void {
values [2]mut u8 = [1, 2]
_ = constcast!()
_ = constcast!(1, 2)
_ = constcast!(1)
_ = constcast!(values)
}
`
source_file := source.Source{path="invalid_constcast.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, bad_operand := false, false
for diagnostic in diagnostics.items {
bad_arity = bad_arity || strings.contains(diagnostic.message, "constcast! expects 1 argument")
bad_operand = bad_operand || strings.contains(diagnostic.message, "constcast! operand must be a pointer, optional pointer, or slice")
}
testing.expect(t, bad_arity && bad_operand)
}
@(test)
immutable_allocations_can_be_freed_and_constcast_keeps_slice_bounds_checks :: proc(t: ^testing.T) {
free_text := `mem :: import "@std/mem"
main func() i32 {
memory []mut u8 :: mem.alloc(u8, mem.c_allocator, 4) catch |_| { return 1 }
memory[0] = 42
immutable []u8 = memory
mem.free(mem.c_allocator, immutable)
empty []u8 = mem.empty(u8)
mem.free(mem.c_allocator, empty)
return 0
}
`
directory := "/tmp/brolang-test-immutable-free"
main_path := "/tmp/brolang-test-immutable-free/main.bro"
output := "/tmp/brolang-test-immutable-free-output"
_ = 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)free_text))
testing.expect_value(t, compiler_core.compile_package(
directory, output, nil, target.DEFAULT, cimport.Options{}, ".",
), 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
bounds_text := `main func() void {
values [2]mut u8 = [1, 2]
immutable []u8 = values[..]
mutable []mut u8 = constcast!(immutable)
_ = mutable[mutable.len]
}
`
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)bounds_text))
testing.expect_value(t, compiler_core.compile_package(directory, output), 0)
bounds_state, stdout, stderr, err := os2.process_exec(
os2.Process_Desc{command=[]string{output}}, context.allocator,
)
defer delete(stdout)
defer delete(stderr)
testing.expect(t, err == nil)
testing.expect(t, !bounds_state.success)
testing.expect(t, strings.contains(string(stderr), "index out of bounds"))
}
@(test) @(test)
old_intrinsic_spellings_are_not_recognized :: proc(t: ^testing.T) { old_intrinsic_spellings_are_not_recognized :: proc(t: ^testing.T) {
text := `main func() void { text := `main func() void {
+3 -2
View File
@@ -110,9 +110,10 @@ realloc func($T type, allocator Allocator, memory []mut T, new_count usize) []mu
return .out_of_memory return .out_of_memory
} }
free func($T type, allocator Allocator, memory []mut T) void { free func($T type, allocator Allocator, memory []T) void {
if memory.len != 0 and sizeof!(T) != 0 { if memory.len != 0 and sizeof!(T) != 0 {
raw_free(allocator, ptrcast!(u8, memory.ptr), memory.len * sizeof!(T), alignof!(T)) mutable_memory []mut T :: constcast!(memory)
raw_free(allocator, ptrcast!(u8, mutable_memory.ptr), memory.len * sizeof!(T), alignof!(T))
} }
} }
@@ -10,4 +10,7 @@ main func() void {
# ^ operator # ^ operator
_ = sizeof(i32) _ = sizeof(i32)
# ^^^^^^ function # ^^^^^^ function
_ = constcast!(memory)
# ^^^^^^^^^ function.builtin
# ^ operator
} }