diff --git a/LANGUAGE.md b/LANGUAGE.md index 57e4969..27d5590 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -180,6 +180,25 @@ infinity/NaN behavior. Only these six division bang calls select integer-division behavior. Bare calls such as `divfloor(a, b)` and qualified calls such as `math.divfloor(a, b)` resolve to ordinary functions. +#### typed memory operations + +`memcopy!(destination, source)` and `memset!(destination, value)` are available at runtime and +comptime. A destination must be a mutable slice or mutable pointer-to-array. A `memcopy!` source +may be a slice or pointer-to-array; many-item pointers must first be sliced. Array pointers are +treated as regions containing their explicit logical elements, including a sentinel only when it +is part of that array region. + +`memcopy!` requires the same element type after alias resolution and the same element count. Its +non-empty regions must not overlap. Comptime calls diagnose unequal lengths and overlap; runtime +calls trap for either condition or if the element count cannot be converted to a byte count. +Zero-sized elements still require equal counts. Empty copies are no-ops and may name the same +region. + +`memset!` coerces `value` to the destination element type. Use `memset!(destination, 0)` to zero a +region; there is no separate `memzero!`, and `memset!` does not promise secure zeroing. Copying or +filling with `undefined` transfers undefined state without reading it. Each operand is evaluated +exactly once. Bare functions named `memcopy` or `memset` remain ordinary user functions. + ### functions, C interop, and linking - demand-monomorphized Brolang and C-ABI functions diff --git a/README.md b/README.md index a707353..e1781d4 100644 --- a/README.md +++ b/README.md @@ -244,6 +244,7 @@ Current prototype features: - Ordered linking of additional C sources, objects, archives, and libraries - Checked signed addition and unary negation - Float-only `/` plus explicit `divtrunc!`, `divfloor!`, `divexact!`, `divceil!`, `rem!`, and `mod!` scalar intrinsics +- Runtime/comptime typed `memcopy!` and `memset!` over slices and pointers-to-arrays, with checked lengths and overlap - Static, eager runtime, mutable runtime, and deferred problematic globals - Runtime diagnostics followed by `llvm.trap` diff --git a/TODO.md b/TODO.md index d9261df..5d826cb 100644 --- a/TODO.md +++ b/TODO.md @@ -934,6 +934,20 @@ - fields without defaults remain required; C-layout records, unions, tuples, and anonymous generated structs do not accept defaults +45. typed memory intrinsics (implemented) + - `memcopy!` copies equal-length, non-overlapping slices or pointers-to-arrays with identical + element types; comptime diagnoses invalid regions and runtime guards length, size, and overlap + - `memset!` fills a mutable region with a value coerced to its element type; bytes lower to LLVM + memset and wider values use typed stores + - both operations evaluate operands once, preserve undefined state without observing it, and work + identically during comptime evaluation; zeroing is `memset!(destination, 0)` + - `std/mem` aligned reallocation uses `memcopy!` + +46. fix `EnumFieldStruct` in `std/meta` + - currently, the `|info|` `.enum` payload capture doesn't preserve the comptime-ness in the match statement + this results in having to type other values in the scope like `[field!(typeinfo!(E), "enum").fields.len]mut []u8` + which is obviously absurd + ## A word on unchecked casts For casts that bypass safety checks, Honey provides builtin functions: diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index 4b01df0..cfeae47 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -811,6 +811,68 @@ Division_Builtin :: enum u8 { Mod, } +Memory_Builtin :: enum u8 { + None, + Copy, + Set, +} + +memory_builtin_call :: proc(checker: ^Checker, expr: ast.Expr) -> Memory_Builtin { + if !expr.intrinsic || expr.kind != .Call || expr.left != ast.INVALID_EXPR || symbol.is_valid(expr.qualifier) { + return .None + } + switch symbol_text(checker, expr.name) { + case "memcopy": return .Copy + case "memset": return .Set + } + return .None +} + +memory_region_type :: proc(checker: ^Checker, value: types.Type) -> (child: types.Type, mutable: bool, ok: bool) { + store := &checker.module.types + resolved := types.resolve_alias(value, store) + if item, item_ok := types.node(store, resolved); item_ok && item.kind == .Slice { + return item.child, item.mutable, true + } + pointer, pointer_ok := types.node(store, resolved) + if pointer_ok && pointer.kind == .Pointer && !pointer.many { + array, array_ok := types.node(store, types.resolve_alias(pointer.child, store)) + if array_ok && array.kind == .Array { + return array.child, pointer.mutable && array.mutable, true + } + } + return types.INVALID, false, false +} + +infer_memory_builtin :: proc( + checker: ^Checker, + expr: ast.Expr, + kind: Memory_Builtin, + locals: []Infer_Local, + pkg: ast.Package_Id, + file: ast.File_Id, + demanded: ^[dynamic]Spec_Id, + local_types: []types.Type, +) -> types.Type { + if len(expr.args) != 2 { + return types.INVALID + } + destination := infer_nested_expr(checker, expr.args[0], locals, pkg, file, demanded, local_types) + child, _, ok := memory_region_type(checker, destination) + if !ok { + _ = infer_nested_expr(checker, expr.args[1], locals, pkg, file, demanded, local_types) + return types.INVALID + } + if kind == .Set { + if !is_undefined_expr(checker, expr.args[1]) { + _ = infer_nested_expr(checker, expr.args[1], locals, pkg, file, demanded, local_types, child) + } + } else { + _ = infer_nested_expr(checker, expr.args[1], locals, pkg, file, demanded, local_types) + } + return types.VOID +} + division_builtin_call :: proc(checker: ^Checker, expr: ast.Expr) -> Division_Builtin { if !expr.intrinsic || expr.kind != .Call || expr.left != ast.INVALID_EXPR || symbol.is_valid(expr.qualifier) { return .None @@ -5036,6 +5098,11 @@ infer_expr :: proc( _ = pop(&stack) continue } + if builtin := memory_builtin_call(checker, expr); builtin != .None { + last = infer_memory_builtin(checker, expr, builtin, locals, pkg, file, demanded, local_types) + _ = pop(&stack) + continue + } if is_intrinsic_call(checker, expr, "some") { if len(expr.args) == 1 && types.is_optional(frame.expected, &checker.module.types) { child := types.child_type(frame.expected, &checker.module.types) @@ -7548,6 +7615,71 @@ build_division_builtin :: proc( }) } +build_memory_builtin :: proc( + checker: ^Checker, + expr: ast.Expr, + kind: Memory_Builtin, + locals: []Build_Local, + global_reads: ^[dynamic]hir.Global_Id, + calls: ^[dynamic]hir.Function_Id, + pkg: ast.Package_Id, + file: ast.File_Id, +) -> hir.Expr_Id { + name := symbol_text(checker, expr.name) + if len(expr.args) != 2 { + id := source.addf(checker.diagnostics, expr.span, "%s! expects 2 arguments, got %d", name, len(expr.args)) + return invalid_hir_expr(checker, expr.span, id, types.VOID) + } + destination := build_nested_expr(checker, expr.args[0], locals, global_reads, calls, types.INVALID, pkg, file) + destination_type := checker.module.exprs[destination].type + destination_child, destination_mutable, destination_ok := memory_region_type(checker, destination_type) + if !destination_ok || !destination_mutable { + id := source.addf( + checker.diagnostics, checker.ast_module.exprs[expr.args[0]].span, + "%s! destination must be a mutable slice or mutable pointer-to-array", name, + ) + return invalid_hir_expr(checker, expr.span, id, types.VOID) + } + right := hir.INVALID_EXPR + result_kind := hir.Expr_Kind.Mem_Copy + if kind == .Set { + if is_undefined_expr(checker, expr.args[1]) { + right = add_hir_expr(checker, hir.Expr{ + kind=.Undefined, span=checker.ast_module.exprs[expr.args[1]].span, type=destination_child, + target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + } else { + right = build_nested_expr(checker, expr.args[1], locals, global_reads, calls, destination_child, pkg, file) + right = coerce_expr(checker, right, destination_child, checker.ast_module.exprs[expr.args[1]].span) + } + result_kind = .Mem_Set + } else { + source_expr := build_nested_expr(checker, expr.args[1], locals, global_reads, calls, types.INVALID, pkg, file) + source_type := checker.module.exprs[source_expr].type + source_child, _, source_ok := memory_region_type(checker, source_type) + if !source_ok { + id := source.add( + checker.diagnostics, checker.ast_module.exprs[expr.args[1]].span, + "memcopy! source must be a slice or pointer-to-array", + ) + return invalid_hir_expr(checker, expr.span, id, types.VOID) + } + if !types.equal( + types.resolve_alias(destination_child, &checker.module.types), + types.resolve_alias(source_child, &checker.module.types), + ) { + id := source.add(checker.diagnostics, expr.span, "memcopy! source and destination element types must match") + return invalid_hir_expr(checker, expr.span, id, types.VOID) + } + right = source_expr + } + return add_hir_expr(checker, hir.Expr{ + kind=result_kind, span=expr.span, type=types.VOID, left=destination, right=right, + target=hir.INVALID_REF, diagnostic=source.INVALID_DIAGNOSTIC, + }) +} + fallible_aggregate :: proc( checker: ^Checker, span: source.Span, @@ -8793,6 +8925,11 @@ build_expr :: proc( _ = pop(&stack) continue } + if builtin := memory_builtin_call(checker, expr); builtin != .None { + last = build_memory_builtin(checker, expr, builtin, locals, global_reads, calls, pkg, file) + _ = pop(&stack) + continue + } if is_intrinsic_call(checker, expr, "some") { if len(expr.args) != 1 { id := source.addf(checker.diagnostics, expr.span, "some! expects 1 argument, got %d", len(expr.args)) diff --git a/compiler/checker/comptime.odin b/compiler/checker/comptime.odin index 4242602..f95514c 100644 --- a/compiler/checker/comptime.odin +++ b/compiler/checker/comptime.odin @@ -2421,6 +2421,166 @@ ct_eval_division_call :: proc( return ct_eval_division_builtin(state, kind, left, right, expr.span) } +ct_memory_region_info :: proc(state: ^Ct_State, value: Ct_Value) -> (child: types.Type, count: int, mutable: bool, ok: bool) { + store := &state.checker.module.types + if value.kind == .Slice { + item, item_ok := types.node(store, types.resolve_alias(value.type, store)) + if item_ok && item.kind == .Slice { + return item.child, int(value.count), item.mutable, true + } + } + if value.kind == .Pointer { + pointer, pointer_ok := types.node(store, types.resolve_alias(value.type, store)) + if pointer_ok && pointer.kind == .Pointer && !pointer.many { + array, array_ok := types.node(store, types.resolve_alias(pointer.child, store)) + if array_ok && array.kind == .Array { + return array.child, int(array.count), pointer.mutable && array.mutable, true + } + } + } + if value.kind == .String && value.index < u64(len(state.checker.ast_module.strings)) { + return types.U8, len(state.checker.ast_module.strings[value.index]), false, true + } + return types.INVALID, 0, false, false +} + +ct_memory_element_place :: proc(state: ^Ct_State, value: Ct_Value, index: int) -> Ct_Place_Id { + if value.kind == .Slice { + place, _, _ := ct_slice_element_place(state, value, index) + return place + } + if value.kind == .Pointer { + base, array_type, writable := ct_pointer_place(state, value) + array, ok := types.node(&state.checker.module.types, types.resolve_alias(array_type, &state.checker.module.types)) + if base != INVALID_CT_PLACE && ok && array.kind == .Array && index >= 0 && index < int(array.count) { + return ct_extend_place( + state, base, Ct_Path_Elem{kind=.Index, index=u32(index)}, array.child, writable && array.mutable, + ) + } + } + return INVALID_CT_PLACE +} + +ct_places_equal :: proc(state: ^Ct_State, left_id, right_id: Ct_Place_Id) -> bool { + if left_id == INVALID_CT_PLACE || right_id == INVALID_CT_PLACE || + int(left_id) >= len(state.places) || int(right_id) >= len(state.places) { + return false + } + left, right := state.places[left_id], state.places[right_id] + if left.cell != right.cell || left.count != right.count { + return false + } + left_path, right_path := ct_place_path(state, left), ct_place_path(state, right) + for elem, index in left_path { + if elem != right_path[index] { + return false + } + } + return true +} + +ct_eval_memory_call :: proc( + state: ^Ct_State, + expr: ast.Expr, + kind: Memory_Builtin, + depth: int, +) -> (Ct_Value_Id, Ct_Flow, bool) { + name := symbol_text(state.checker, expr.name) + if len(expr.args) != 2 { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, expr.span, "%s! expects 2 arguments, got %d", name, len(expr.args)) + } + destination_id, destination_flow, destination_ok := ct_eval_expr(state, expr.args[0], types.INVALID, depth+1) + if !destination_ok || destination_flow.kind != .Normal || destination_id == INVALID_CT_VALUE || int(destination_id) >= len(state.values) { + return INVALID_CT_VALUE, destination_flow, destination_ok + } + destination := state.values[destination_id] + destination_child, destination_count, destination_mutable, region_ok := ct_memory_region_info(state, destination) + if !region_ok || !destination_mutable { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf( + state, .Not_Comptime, state.checker.ast_module.exprs[expr.args[0]].span, + "%s! destination must be a mutable slice or mutable pointer-to-array", name, + ) + } + destination_places := make([]Ct_Place_Id, destination_count, state.checker.allocator) + defer delete(destination_places, state.checker.allocator) + for &place, index in destination_places { + place = ct_memory_element_place(state, destination, index) + if place == INVALID_CT_PLACE || !state.places[place].writable { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "comptime memory destination no longer points to writable storage") + } + } + + if kind == .Set { + value, value_flow, value_ok := ct_eval_expr(state, expr.args[1], destination_child, depth+1) + if !value_ok || value_flow.kind != .Normal { + return INVALID_CT_VALUE, value_flow, value_ok + } + value, value_ok = ct_coerce_value(state, value, destination_child, state.checker.ast_module.exprs[expr.args[1]].span) + if !value_ok { + return INVALID_CT_VALUE, ct_flow(.Normal), false + } + for place in destination_places { + if !ct_place_set(state, place, value) { + return INVALID_CT_VALUE, ct_flow(.Normal), false + } + } + return ct_add_value(state, Ct_Value{kind=.Void, type=types.VOID}), ct_flow(.Normal), true + } + + source_id, source_flow, source_ok := ct_eval_expr(state, expr.args[1], types.INVALID, depth+1) + if !source_ok || source_flow.kind != .Normal || source_id == INVALID_CT_VALUE || int(source_id) >= len(state.values) { + return INVALID_CT_VALUE, source_flow, source_ok + } + source := state.values[source_id] + source_child, source_count, _, source_region_ok := ct_memory_region_info(state, source) + if !source_region_ok { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, state.checker.ast_module.exprs[expr.args[1]].span, "memcopy! source must be a slice or pointer-to-array") + } + if !types.equal( + types.resolve_alias(destination_child, &state.checker.module.types), + types.resolve_alias(source_child, &state.checker.module.types), + ) { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "memcopy! source and destination element types must match") + } + if destination_count != source_count { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "memcopy! source and destination lengths differ") + } + + source_places := make([]Ct_Place_Id, source_count, state.checker.allocator) + values := make([]Ct_Value_Id, source_count, state.checker.allocator) + defer delete(source_places, state.checker.allocator) + defer delete(values, state.checker.allocator) + for index in 0.. (Ct_Value_Id, Ct_Flow, bool) { if id == INVALID_CT_VALUE || int(id) >= len(state.values) { return INVALID_CT_VALUE, ct_flow(.Normal), false @@ -3075,6 +3235,9 @@ ct_eval_call_expr :: proc(state: ^Ct_State, expr: ast.Expr, expected: types.Type if builtin := division_builtin_call(checker, expr); builtin != .None { return ct_eval_division_call(state, expr, builtin, expected, depth+1) } + if builtin := memory_builtin_call(checker, expr); builtin != .None { + return ct_eval_memory_call(state, expr, builtin, depth+1) + } 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") diff --git a/compiler/hir/hir.odin b/compiler/hir/hir.odin index 7bcf481..da692b2 100644 --- a/compiler/hir/hir.odin +++ b/compiler/hir/hir.odin @@ -79,6 +79,7 @@ Expr_Kind :: enum u8 { Float, String, Bool, + Undefined, Array, Struct, None, @@ -137,6 +138,8 @@ Expr_Kind :: enum u8 { And, Or, Range, + Mem_Copy, + Mem_Set, Call, } diff --git a/compiler/ir/ir.odin b/compiler/ir/ir.odin index fa7f9a0..0e943cb 100644 --- a/compiler/ir/ir.odin +++ b/compiler/ir/ir.odin @@ -68,6 +68,7 @@ Linkage :: enum u8 { Opcode :: enum u8 { Param, Const, + Poison, String, Aggregate, None, @@ -125,6 +126,8 @@ Opcode :: enum u8 { Shift_Right, Shift_Left_Saturating, Compare, + Mem_Copy, + Mem_Set, Label, Br, Cond_Br, diff --git a/compiler/llvm/llvm.odin b/compiler/llvm/llvm.odin index 047832f..749a6b3 100644 --- a/compiler/llvm/llvm.odin +++ b/compiler/llvm/llvm.odin @@ -240,6 +240,29 @@ valid_instruction :: proc(instructions: []ir.Instruction, instruction_id: ir.Ins return instruction_id != ir.INVALID_INSTRUCTION && int(instruction_id) < len(instructions) } +memory_region :: proc(value: types.Type, store: ^types.Store) -> ( + child, array_type: types.Type, + count: u64, + mutable, is_slice, ok: bool, +) { + resolved := types.resolve_alias(value, store) + item, item_ok := types.node(store, resolved) + if !item_ok { + return types.INVALID, types.INVALID, 0, false, false, false + } + if item.kind == .Slice { + return item.child, types.INVALID, 0, item.mutable, true, true + } + if item.kind == .Pointer && !item.many { + array_type = types.resolve_alias(item.child, store) + array, array_ok := types.node(store, array_type) + if array_ok && array.kind == .Array { + return array.child, array_type, array.count, item.mutable && array.mutable, false, true + } + } + return types.INVALID, types.INVALID, 0, false, false, false +} + valid_value :: proc( instructions: []ir.Instruction, value_id: ir.Instruction_Id, @@ -252,7 +275,7 @@ valid_value :: proc( return false } switch instructions[value_id].op { - case .Param, .Const, .String, .Aggregate, .None, .Optional_Some, + case .Param, .Const, .Poison, .String, .Aggregate, .None, .Optional_Some, .Load_Global, .Function_Address, .Address_Of, .Load, .Union_Tag, .Slice, .Length, .Slice_Ptr, .Fallible_Error, .Extract, .Select, .Unwrap, .Optional_Is_Some, .Optional_Value, .Orelse, @@ -264,7 +287,7 @@ valid_value :: proc( .Shift_Left, .Shift_Right, .Shift_Left_Saturating, .Compare, .Call: return true case .Address_Global, .Alloca, .Index_Address, .Field_Address, .Orelse_Begin, - .Store, .Fill, .Trap, .Label, .Br, .Cond_Br, .Return, .Return_Void: + .Store, .Fill, .Mem_Copy, .Mem_Set, .Trap, .Label, .Br, .Cond_Br, .Return, .Return_Void: return false } return false @@ -333,6 +356,8 @@ write_operand :: proc( value := instructions[value_id] if value.op == .Const { write_constant(builder, value.integer, expected, store) + } else if value.op == .Poison { + strings.write_string(builder, "poison") } else { fmt.sbprintf(builder, "%%v%d", value_id) } @@ -925,7 +950,7 @@ emit_instruction_stream :: proc( after_terminator = false } switch instruction.op { - case .Param, .Const: + case .Param, .Const, .Poison: case .String: string_id := int(instruction.integer) _, array, pointer_ok := types.array_pointer(instruction.type, &emitter.module.types) @@ -1368,6 +1393,109 @@ emit_instruction_stream :: proc( instruction.a, types.size(instruction.type, &emitter.module.types, emitter.module.target), ) + case .Mem_Copy: + if !valid_instruction(instructions, instruction.a) || !valid_instruction(instructions, instruction.b) { + emit_recovery_value(emitter, instruction_index, instruction, "invalid memcopy operands") + continue + } + destination_type := instructions[instruction.a].type + source_type := instructions[instruction.b].type + destination_child, destination_array, destination_count, destination_mutable, destination_is_slice, destination_ok := memory_region(destination_type, &emitter.module.types) + source_child, source_array, source_count, _, source_is_slice, source_ok := memory_region(source_type, &emitter.module.types) + if !destination_ok || !destination_mutable || !source_ok || + !types.equal(types.resolve_alias(destination_child, &emitter.module.types), types.resolve_alias(source_child, &emitter.module.types)) || + !types.equal(types.resolve_alias(destination_child, &emitter.module.types), types.resolve_alias(instruction.type, &emitter.module.types)) { + emit_recovery_value(emitter, instruction_index, instruction, "invalid memcopy region types") + continue + } + if destination_is_slice { + fmt.sbprintf(&emitter.builder, " %%memcopy_dst%d = extractvalue %s %%v%d, 0\n", instruction_index, llvm_type(destination_type, &emitter.module.types), instruction.a) + fmt.sbprintf(&emitter.builder, " %%memcopy_dst_len%d = extractvalue %s %%v%d, 1\n", instruction_index, llvm_type(destination_type, &emitter.module.types), instruction.a) + } else { + fmt.sbprintf(&emitter.builder, " %%memcopy_dst%d = getelementptr %s, ptr %%v%d, i64 0, i64 0\n", instruction_index, llvm_type(destination_array, &emitter.module.types), instruction.a) + fmt.sbprintf(&emitter.builder, " %%memcopy_dst_len%d = add i64 0, %d\n", instruction_index, destination_count) + } + if source_is_slice { + fmt.sbprintf(&emitter.builder, " %%memcopy_src%d = extractvalue %s %%v%d, 0\n", instruction_index, llvm_type(source_type, &emitter.module.types), instruction.b) + fmt.sbprintf(&emitter.builder, " %%memcopy_src_len%d = extractvalue %s %%v%d, 1\n", instruction_index, llvm_type(source_type, &emitter.module.types), instruction.b) + } else { + fmt.sbprintf(&emitter.builder, " %%memcopy_src%d = getelementptr %s, ptr %%v%d, i64 0, i64 0\n", instruction_index, llvm_type(source_array, &emitter.module.types), instruction.b) + fmt.sbprintf(&emitter.builder, " %%memcopy_src_len%d = add i64 0, %d\n", instruction_index, source_count) + } + fmt.sbprintf(&emitter.builder, " %%memcopy_len_ok%d = icmp eq i64 %%memcopy_dst_len%d, %%memcopy_src_len%d\n", instruction_index, instruction_index, instruction_index) + fmt.sbprintf(&emitter.builder, " br i1 %%memcopy_len_ok%d, label %%memcopy_size_check%d, label %%memcopy_len_trap%d\nmemcopy_len_trap%d:\n", instruction_index, instruction_index, instruction_index, instruction_index) + message := diagnostic_message(emitter, source.INVALID_DIAGNOSTIC, instruction.span, "memcopy! source and destination lengths differ") + emit_trap_call(emitter, message) + fmt.sbprintf(&emitter.builder, " unreachable\nmemcopy_size_check%d:\n", instruction_index) + element_size := types.size(instruction.type, &emitter.module.types, emitter.module.target) + if element_size == 0 { + continue + } + max_count := u64(0xffff_ffff_ffff_ffff)/element_size + fmt.sbprintf(&emitter.builder, " %%memcopy_size_ok%d = icmp ule i64 %%memcopy_dst_len%d, %d\n", instruction_index, instruction_index, max_count) + fmt.sbprintf(&emitter.builder, " br i1 %%memcopy_size_ok%d, label %%memcopy_overlap_check%d, label %%memcopy_size_trap%d\nmemcopy_size_trap%d:\n", instruction_index, instruction_index, instruction_index, instruction_index) + message = diagnostic_message(emitter, source.INVALID_DIAGNOSTIC, instruction.span, "memory operation size overflow") + emit_trap_call(emitter, message) + fmt.sbprintf(&emitter.builder, " unreachable\nmemcopy_overlap_check%d:\n", instruction_index) + fmt.sbprintf(&emitter.builder, " %%memcopy_bytes%d = mul i64 %%memcopy_dst_len%d, %d\n", instruction_index, instruction_index, element_size) + fmt.sbprintf(&emitter.builder, " %%memcopy_dst_end%d = getelementptr i8, ptr %%memcopy_dst%d, i64 %%memcopy_bytes%d\n", instruction_index, instruction_index, instruction_index) + fmt.sbprintf(&emitter.builder, " %%memcopy_src_end%d = getelementptr i8, ptr %%memcopy_src%d, i64 %%memcopy_bytes%d\n", instruction_index, instruction_index, instruction_index) + fmt.sbprintf(&emitter.builder, " %%memcopy_before%d = icmp ule ptr %%memcopy_dst_end%d, %%memcopy_src%d\n", instruction_index, instruction_index, instruction_index) + fmt.sbprintf(&emitter.builder, " %%memcopy_after%d = icmp ule ptr %%memcopy_src_end%d, %%memcopy_dst%d\n", instruction_index, instruction_index, instruction_index) + fmt.sbprintf(&emitter.builder, " %%memcopy_disjoint%d = or i1 %%memcopy_before%d, %%memcopy_after%d\n", instruction_index, instruction_index, instruction_index) + fmt.sbprintf(&emitter.builder, " %%memcopy_empty%d = icmp eq i64 %%memcopy_bytes%d, 0\n", instruction_index, instruction_index) + fmt.sbprintf(&emitter.builder, " %%memcopy_ok%d = or i1 %%memcopy_empty%d, %%memcopy_disjoint%d\n", instruction_index, instruction_index, instruction_index) + fmt.sbprintf(&emitter.builder, " br i1 %%memcopy_ok%d, label %%memcopy_continue%d, label %%memcopy_overlap_trap%d\nmemcopy_overlap_trap%d:\n", instruction_index, instruction_index, instruction_index, instruction_index) + message = diagnostic_message(emitter, source.INVALID_DIAGNOSTIC, instruction.span, "memcopy! source and destination overlap") + emit_trap_call(emitter, message) + fmt.sbprintf(&emitter.builder, " unreachable\nmemcopy_continue%d:\n", instruction_index) + fmt.sbprintf(&emitter.builder, " call void @llvm.memcpy.p0.p0.i64(ptr %%memcopy_dst%d, ptr %%memcopy_src%d, i64 %%memcopy_bytes%d, i1 false)\n", instruction_index, instruction_index, instruction_index) + case .Mem_Set: + if !valid_instruction(instructions, instruction.a) || !valid_value(instructions, instruction.b, instruction.type, &emitter.module.types) { + emit_recovery_value(emitter, instruction_index, instruction, "invalid memset operands") + continue + } + destination_type := instructions[instruction.a].type + destination_child, destination_array, destination_count, destination_mutable, destination_is_slice, destination_ok := memory_region(destination_type, &emitter.module.types) + if !destination_ok || !destination_mutable || + !types.equal(types.resolve_alias(destination_child, &emitter.module.types), types.resolve_alias(instruction.type, &emitter.module.types)) { + emit_recovery_value(emitter, instruction_index, instruction, "invalid memset destination") + continue + } + if destination_is_slice { + fmt.sbprintf(&emitter.builder, " %%memset_dst%d = extractvalue %s %%v%d, 0\n", instruction_index, llvm_type(destination_type, &emitter.module.types), instruction.a) + fmt.sbprintf(&emitter.builder, " %%memset_len%d = extractvalue %s %%v%d, 1\n", instruction_index, llvm_type(destination_type, &emitter.module.types), instruction.a) + } else { + fmt.sbprintf(&emitter.builder, " %%memset_dst%d = getelementptr %s, ptr %%v%d, i64 0, i64 0\n", instruction_index, llvm_type(destination_array, &emitter.module.types), instruction.a) + fmt.sbprintf(&emitter.builder, " %%memset_len%d = add i64 0, %d\n", instruction_index, destination_count) + } + element_size := types.size(instruction.type, &emitter.module.types, emitter.module.target) + if element_size == 0 { + continue + } + max_count := u64(0xffff_ffff_ffff_ffff)/element_size + fmt.sbprintf(&emitter.builder, " %%memset_size_ok%d = icmp ule i64 %%memset_len%d, %d\n", instruction_index, instruction_index, max_count) + fmt.sbprintf(&emitter.builder, " br i1 %%memset_size_ok%d, label %%memset_start%d, label %%memset_size_trap%d\nmemset_size_trap%d:\n", instruction_index, instruction_index, instruction_index, instruction_index) + message := diagnostic_message(emitter, source.INVALID_DIAGNOSTIC, instruction.span, "memory operation size overflow") + emit_trap_call(emitter, message) + fmt.sbprintf(&emitter.builder, " unreachable\nmemset_start%d:\n", instruction_index) + representation := types.runtime_representation(instruction.type, &emitter.module.types) + if types.is_concrete_integer(representation) && types.bits(representation, emitter.module.target) == 8 { + fmt.sbprintf(&emitter.builder, " %%memset_bytes%d = mul i64 %%memset_len%d, %d\n", instruction_index, instruction_index, element_size) + fmt.sbprintf(&emitter.builder, " call void @llvm.memset.p0.i64(ptr %%memset_dst%d, i8 ", instruction_index) + write_operand(&emitter.builder, instructions, instruction.b, instruction.type, &emitter.module.types) + fmt.sbprintf(&emitter.builder, ", i64 %%memset_bytes%d, i1 false)\n", instruction_index) + continue + } + fmt.sbprintf(&emitter.builder, " %%memset_index_slot%d = alloca i64\n store i64 0, ptr %%memset_index_slot%d\n br label %%memset_loop%d\nmemset_loop%d:\n", instruction_index, instruction_index, instruction_index, instruction_index) + fmt.sbprintf(&emitter.builder, " %%memset_index%d = load i64, ptr %%memset_index_slot%d\n", instruction_index, instruction_index) + fmt.sbprintf(&emitter.builder, " %%memset_more%d = icmp ult i64 %%memset_index%d, %%memset_len%d\n", instruction_index, instruction_index, instruction_index) + fmt.sbprintf(&emitter.builder, " br i1 %%memset_more%d, label %%memset_body%d, label %%memset_done%d\nmemset_body%d:\n", instruction_index, instruction_index, instruction_index, instruction_index) + fmt.sbprintf(&emitter.builder, " %%memset_element%d = getelementptr %s, ptr %%memset_dst%d, i64 %%memset_index%d\n", instruction_index, llvm_type(instruction.type, &emitter.module.types), instruction_index, instruction_index) + fmt.sbprintf(&emitter.builder, " store %s ", llvm_type(instruction.type, &emitter.module.types)) + write_operand(&emitter.builder, instructions, instruction.b, instruction.type, &emitter.module.types) + fmt.sbprintf(&emitter.builder, ", ptr %%memset_element%d\n", instruction_index) + fmt.sbprintf(&emitter.builder, " %%memset_next%d = add i64 %%memset_index%d, 1\n store i64 %%memset_next%d, ptr %%memset_index_slot%d\n br label %%memset_loop%d\nmemset_done%d:\n", instruction_index, instruction_index, instruction_index, instruction_index, instruction_index, instruction_index) case .Slice: if !valid_instruction(instructions, instruction.a) { emit_recovery_value(emitter, instruction_index, instruction, "invalid slice container") diff --git a/compiler/lower/lower.odin b/compiler/lower/lower.odin index 6c15c82..0d17639 100644 --- a/compiler/lower/lower.odin +++ b/compiler/lower/lower.odin @@ -647,6 +647,24 @@ lower_compound_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instructi return append_recovery_value(state, expr.span, expr.type, expr.diagnostic) } +memory_region_element_type :: proc(value: types.Type, store: ^types.Store) -> types.Type { + resolved := types.resolve_alias(value, store) + item, ok := types.node(store, resolved) + if !ok { + return types.INVALID + } + if item.kind == .Slice { + return item.child + } + if item.kind == .Pointer && !item.many { + array, array_ok := types.node(store, types.resolve_alias(item.child, store)) + if array_ok && array.kind == .Array { + return array.child + } + } + return types.INVALID +} + lower_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instruction_Id { stack := state.expr_stack state.expr_stack = nil @@ -691,6 +709,13 @@ lower_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instruction_Id { target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC, }) _ = pop(&stack) + case .Undefined: + last = append_instruction(state, ir.Instruction{ + op=.Poison, span=expr.span, type=expr.type, + target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + _ = pop(&stack) case .String, .Array, .Struct, .Range, .None, .Optional_Some, .Address, .Deref, .Index, .Slice, .Field, .Union_Tag, .Length, .Slice_Ptr, .Unwrap, .Orelse, .Try, .Catch, .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or: @@ -744,7 +769,7 @@ lower_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instruction_Id { append(&stack, Lower_Expr_Frame{expr=expr.left}) case .Add, .Sub, .Mul, .Div, .Div_Trunc, .Div_Floor, .Div_Exact, .Div_Ceil, .Rem, .Mod, .Pointer_Add, .Bit_And, .Bit_Or, .Bit_Xor, .Shift_Left, - .Shift_Right, .Shift_Left_Saturating: + .Shift_Right, .Shift_Left_Saturating, .Mem_Copy, .Mem_Set: stack[frame_index].stage = 2 append(&stack, Lower_Expr_Frame{expr=expr.left}) case .Call: @@ -823,6 +848,7 @@ lower_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instruction_Id { } if frame.stage == 3 { op := ir.Opcode.Add_Checked + result_type := expr.type #partial switch expr.kind { case .Sub: op = .Sub_Checked case .Mul: op = .Mul_Checked @@ -840,10 +866,16 @@ lower_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instruction_Id { case .Shift_Left: op = .Shift_Left case .Shift_Right: op = .Shift_Right case .Shift_Left_Saturating: op = .Shift_Left_Saturating + case .Mem_Copy: + op = .Mem_Copy + result_type = memory_region_element_type(state.hir_module.exprs[expr.left].type, &state.hir_module.types) + case .Mem_Set: + op = .Mem_Set + result_type = memory_region_element_type(state.hir_module.exprs[expr.left].type, &state.hir_module.types) } last = append_instruction(state, ir.Instruction{ op=op, - span=expr.span, type=expr.type, target=ir.INVALID_REF, + span=expr.span, type=result_type, target=ir.INVALID_REF, a=frame.left, b=last, diagnostic=source.INVALID_DIAGNOSTIC, }) _ = pop(&stack) diff --git a/compiler_tests.odin b/compiler_tests.odin index 7e9c0a8..f95152b 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -2230,7 +2230,9 @@ old_intrinsic_spellings_are_not_recognized :: proc(t: ^testing.T) { @(test) bare_intrinsic_names_are_available_to_user_functions :: proc(t: ^testing.T) { - text := `ptrcast func() i32 { return 1 } + text := `memory :: import "./memory" + +ptrcast func() i32 { return 1 } sizeof func() i32 { return 2 } alignof func() i32 { return 3 } minval func() i32 { return 4 } @@ -2241,19 +2243,29 @@ divexact func() i32 { return 8 } divceil func() i32 { return 9 } rem func() i32 { return 10 } mod func() i32 { return 11 } +memcopy func() i32 { return 12 } +memset func() i32 { return 13 } main func() i32 { return ptrcast() + sizeof() + alignof() + minval() + maxval() + - divtrunc() + divfloor() + divexact() + divceil() + rem() + mod() - 66 + divtrunc() + divfloor() + divexact() + divceil() + rem() + mod() + + memcopy() + memset() + memory.memcopy() + memory.memset() - 120 } ` directory := "/tmp/brolang-test-user-intrinsic-names" main_path := "/tmp/brolang-test-user-intrinsic-names/main.bro" + memory_directory := "/tmp/brolang-test-user-intrinsic-names/memory" + memory_path := "/tmp/brolang-test-user-intrinsic-names/memory/memory.bro" output := "/tmp/brolang-test-user-intrinsic-names-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.make_directory(memory_directory) == nil) testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text)) + memory_text := `memcopy func() i32 { return 14 } +memset func() i32 { return 15 } +` + testing.expect(t, os.write_entire_file(memory_path, transmute([]byte)memory_text)) testing.expect_value(t, compiler_core.compile_package(directory, output), 0) } @@ -2286,6 +2298,215 @@ intrinsic_call_diagnostics_are_precise :: proc(t: ^testing.T) { testing.expect(t, found_qualified) } +@(test) +memory_intrinsics_compile_run_and_lower :: proc(t: ^testing.T) { + text := `Pair :: struct { x i32, y i32 } + +calls i32 = 0 + +destination_view func(value []mut u8) []mut u8 { + calls += 1 + return value +} + +source_view func(value []u8) []u8 { + calls += 1 + return value +} + +compile_value func() [4]mut u8 { + source [4]mut u8 = [1, 2, 3, 4] + destination [4]mut u8 = undefined + memset!(&destination, undefined) + memcopy!(&destination, &source) + memset!(destination[1..3], 9) + memcopy!(destination[..0], destination[..0]) + return destination +} + +known :: $compile_value() + +main func() i32 { + if known[0] != 1 or known[1] != 9 or known[2] != 9 or known[3] != 4 { return 1 } + + bytes [4]mut u8 = undefined + source [4]mut u8 = [4, 3, 2, 1] + memset!(&bytes, undefined) + memcopy!(destination_view(bytes[..]), source_view(source[..])) + if calls != 2 or bytes[0] != 4 or bytes[3] != 1 { return 2 } + memset!(bytes[1..3], 7) + if bytes[1] != 7 or bytes[2] != 7 { return 3 } + + wide [2]mut i32 = undefined + memset!(&wide, 42) + if wide[0] != 42 or wide[1] != 42 { return 4 } + + pairs [2]mut Pair = undefined + pair_source [2]mut Pair = [Pair{x = 1, y = 2}, Pair{x = 3, y = 4}] + memcopy!(&pairs, &pair_source) + memset!(pairs[1..], Pair{x = 7, y = 8}) + if pairs[0].x != 1 or pairs[1].y != 8 { return 5 } + + memcopy!(bytes[..0], bytes[..0]) + return 0 +} +` + 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) + llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols) + defer delete(llvm_text) + + copy_count, set_count := 0, 0 + for function in ir_module.functions { + for instruction in function.instructions { + copy_count += 1 if instruction.op == .Mem_Copy else 0 + set_count += 1 if instruction.op == .Mem_Set else 0 + } + } + testing.expect_value(t, len(diagnostics.items), 0) + testing.expect(t, copy_count >= 3) + testing.expect(t, set_count >= 3) + testing.expect(t, strings.contains(llvm_text, "memcopy_len_ok")) + testing.expect(t, strings.contains(llvm_text, "memcopy_size_ok")) + testing.expect(t, strings.contains(llvm_text, "memcopy_disjoint")) + testing.expect(t, strings.contains(llvm_text, "call void @llvm.memcpy.p0.p0.i64")) + testing.expect(t, strings.contains(llvm_text, "call void @llvm.memset.p0.i64")) + testing.expect(t, strings.contains(llvm_text, "memset_loop")) + + directory := "/tmp/brolang-test-memory-intrinsics" + main_path := "/tmp/brolang-test-memory-intrinsics/main.bro" + output := "/tmp/brolang-test-memory-intrinsics-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) +memory_intrinsic_diagnostics_are_precise :: proc(t: ^testing.T) { + text := `bad_length func() [1]mut u8 { + destination [1]mut u8 = undefined + source [2]mut u8 = [1, 2] + memcopy!(&destination, &source) + return destination +} + +bad_overlap func() [3]mut u8 { + items [3]mut u8 = [1, 2, 3] + memcopy!(items[..2], items[1..]) + return items +} + +bad_undefined func() u8 { + items [1]mut u8 = [1] + memset!(&items, undefined) + return items[0] +} + +length_value :: $bad_length() +overlap_value :: $bad_overlap() +undefined_value :: $bad_undefined() + +main func() void { + immutable [2]u8 = [1, 2] + mutable [2]mut u8 = undefined + wide [2]mut u16 = undefined + memcopy!(&immutable, &mutable) + memcopy!(&mutable, &wide) + memcopy!(1, 2) + memset!(1, 0) + memcopy!() + memset!(&mutable) +} +` + 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) + + expected := [?]string{ + "memcopy! source and destination lengths differ", + "memcopy! source and destination overlap", + "cannot read an undefined value at comptime", + "memcopy! destination must be a mutable slice or mutable pointer-to-array", + "memcopy! source and destination element types must match", + "memset! destination must be a mutable slice or mutable pointer-to-array", + "memcopy! expects 2 arguments, got 0", + "memset! expects 2 arguments, got 1", + } + found: [len(expected)]bool + for diagnostic in diagnostics.items { + for message, index in expected { + found[index] = found[index] || strings.contains(diagnostic.message, message) + } + } + for present in found { + testing.expect(t, present) + } +} + +@(test) +memory_intrinsic_runtime_guards_trap :: proc(t: ^testing.T) { + cases := [?]struct { + directory, output, text: string, + }{ + { + "/tmp/brolang-test-memcopy-length-trap", + "/tmp/brolang-test-memcopy-length-trap-output", + `copy func(destination []mut u8, source []u8) void { memcopy!(destination, source) } +main func() void { + destination [2]mut u8 = undefined + source [3]mut u8 = [1, 2, 3] + copy(destination[..], source[..]) +} +`, + }, + { + "/tmp/brolang-test-memcopy-overlap-trap", + "/tmp/brolang-test-memcopy-overlap-trap-output", + `copy func(destination []mut u8, source []u8) void { memcopy!(destination, source) } +main func() void { + items [4]mut u8 = [1, 2, 3, 4] + copy(items[1..], items[..3]) +} +`, + }, + } + for test_case in cases { + main_path := fmt.tprintf("%s/main.bro", test_case.directory) + _ = os2.remove_all(test_case.directory) + defer _ = os2.remove_all(test_case.directory) + defer _ = os.remove(test_case.output) + testing.expect(t, os.make_directory(test_case.directory) == nil) + testing.expect(t, os.write_entire_file(main_path, transmute([]byte)test_case.text)) + testing.expect_value(t, compiler_core.compile_package(test_case.directory, test_case.output), 0) + state := run_executable(test_case.output) + testing.expect(t, !state.success) + } +} + @(test) malformed_intrinsic_calls_have_targeted_parse_diagnostics :: proc(t: ^testing.T) { cases := [?]struct { diff --git a/std/mem/mem.bro b/std/mem/mem.bro index 6f97d01..f65cc24 100644 --- a/std/mem/mem.bro +++ b/std/mem/mem.bro @@ -174,10 +174,7 @@ hide c_realloc func(_ ?@mut anyopaque, memory ?*mut u8, old_size usize, new_size if new_size < copy_size { copy_size = new_size } - i usize = 0 - while i < copy_size : i += 1 { - new_bytes[i] = old_memory[i] - } + memcopy!(new_bytes[..copy_size], old_memory[..copy_size]) c.free(old_memory) } return new_memory