add constcast and immutable free
This commit is contained in:
@@ -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-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
|
||||
- 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
|
||||
- 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
|
||||
|
||||
@@ -960,6 +960,11 @@
|
||||
value-producing control-flow forms with or without an error capture
|
||||
- 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
|
||||
|
||||
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 |
|
||||
| `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
|
||||
# truncation
|
||||
@@ -981,12 +987,10 @@ 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
|
||||
# pointer casts
|
||||
buf *u8 = get_buffer()
|
||||
ints *u32 = ptrcast!(u32, buf) # element type change, same pointer shape
|
||||
writable *mut u8 = constcast!(buf) # explicit unsafe mutability restoration
|
||||
```
|
||||
|
||||
## A word on multi-unwrap
|
||||
|
||||
@@ -5187,6 +5187,16 @@ infer_expr :: proc(
|
||||
_ = pop(&stack)
|
||||
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") {
|
||||
last = types.VOID
|
||||
_ = 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})
|
||||
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") {
|
||||
message := "compile_error! requires one comptime string argument"
|
||||
if len(expr.args) == 1 {
|
||||
@@ -9716,6 +9737,24 @@ build_expr :: proc(
|
||||
}
|
||||
_ = 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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
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 symbol.is_valid(expr.qualifier) {
|
||||
return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "intrinsic calls must be unqualified")
|
||||
|
||||
@@ -107,6 +107,7 @@ Expr_Kind :: enum u8 {
|
||||
Retype,
|
||||
Scalar_Cast,
|
||||
Pointer_Cast,
|
||||
Const_Cast,
|
||||
Weaken_Pointer,
|
||||
Weaken_Slice,
|
||||
Decay_Array_Pointer,
|
||||
|
||||
@@ -102,6 +102,7 @@ Opcode :: enum u8 {
|
||||
Retype,
|
||||
Scalar_Cast,
|
||||
Pointer_Cast,
|
||||
Const_Cast,
|
||||
Weaken_Pointer,
|
||||
Weaken_Slice,
|
||||
Decay_Array_Pointer,
|
||||
|
||||
+11
-1
@@ -287,7 +287,7 @@ valid_value :: proc(
|
||||
.Load_Global, .Function_Address, .Address_Of, .Load, .Union_Tag, .Slice, .Length, .Slice_Ptr,
|
||||
.Fallible_Error, .Extract, .Select, .Unwrap,
|
||||
.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,
|
||||
.Div_Trunc_Checked, .Div_Floor_Checked, .Div_Exact_Checked, .Div_Ceil_Checked,
|
||||
.Rem_Checked, .Mod_Checked,
|
||||
@@ -1930,6 +1930,16 @@ emit_instruction_stream :: proc(
|
||||
continue
|
||||
}
|
||||
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:
|
||||
if !valid_instruction(instructions, instruction.a) ||
|
||||
!types.can_weaken_slice(instructions[instruction.a].type, instruction.type, &emitter.module.types) {
|
||||
|
||||
@@ -768,7 +768,7 @@ lower_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instruction_Id {
|
||||
})
|
||||
}
|
||||
_ = 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
|
||||
append(&stack, Lower_Expr_Frame{expr=expr.left})
|
||||
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 .Scalar_Cast: op = .Scalar_Cast
|
||||
case .Pointer_Cast: op = .Pointer_Cast
|
||||
case .Const_Cast: op = .Const_Cast
|
||||
case: op = .Widen
|
||||
}
|
||||
last = append_instruction(state, ir.Instruction{
|
||||
|
||||
@@ -1340,6 +1340,45 @@ replace_pointer_child :: proc(store: ^Store, value, child: Type) -> (Type, bool)
|
||||
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 {
|
||||
left_item, left_ok := node(store, left)
|
||||
right_item, right_ok := node(store, right)
|
||||
|
||||
@@ -2184,6 +2184,148 @@ main func() void {
|
||||
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)
|
||||
old_intrinsic_spellings_are_not_recognized :: proc(t: ^testing.T) {
|
||||
text := `main func() void {
|
||||
|
||||
+3
-2
@@ -110,9 +110,10 @@ realloc func($T type, allocator Allocator, memory []mut T, new_count usize) []mu
|
||||
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 {
|
||||
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
|
||||
_ = sizeof(i32)
|
||||
# ^^^^^^ function
|
||||
_ = constcast!(memory)
|
||||
# ^^^^^^^^^ function.builtin
|
||||
# ^ operator
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user