memcopy! and memset! intrinsics

This commit is contained in:
2026-07-20 22:53:04 +02:00
parent 297f2e3078
commit dd00af7731
11 changed files with 729 additions and 11 deletions
+19
View File
@@ -180,6 +180,25 @@ infinity/NaN behavior.
Only these six division bang calls select integer-division behavior. Bare calls such as 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. `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 ### functions, C interop, and linking
- demand-monomorphized Brolang and C-ABI functions - demand-monomorphized Brolang and C-ABI functions
+1
View File
@@ -244,6 +244,7 @@ Current prototype features:
- Ordered linking of additional C sources, objects, archives, and libraries - Ordered linking of additional C sources, objects, archives, and libraries
- Checked signed addition and unary negation - Checked signed addition and unary negation
- Float-only `/` plus explicit `divtrunc!`, `divfloor!`, `divexact!`, `divceil!`, `rem!`, and `mod!` scalar intrinsics - 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 - Static, eager runtime, mutable runtime, and deferred problematic globals
- Runtime diagnostics followed by `llvm.trap` - Runtime diagnostics followed by `llvm.trap`
+14
View File
@@ -934,6 +934,20 @@
- fields without defaults remain required; C-layout records, unions, tuples, and anonymous - fields without defaults remain required; C-layout records, unions, tuples, and anonymous
generated structs do not accept defaults 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 ## 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:
+137
View File
@@ -811,6 +811,68 @@ Division_Builtin :: enum u8 {
Mod, 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 { 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) { if !expr.intrinsic || expr.kind != .Call || expr.left != ast.INVALID_EXPR || symbol.is_valid(expr.qualifier) {
return .None return .None
@@ -5036,6 +5098,11 @@ infer_expr :: proc(
_ = pop(&stack) _ = pop(&stack)
continue 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 is_intrinsic_call(checker, expr, "some") {
if len(expr.args) == 1 && types.is_optional(frame.expected, &checker.module.types) { if len(expr.args) == 1 && types.is_optional(frame.expected, &checker.module.types) {
child := types.child_type(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( fallible_aggregate :: proc(
checker: ^Checker, checker: ^Checker,
span: source.Span, span: source.Span,
@@ -8793,6 +8925,11 @@ build_expr :: proc(
_ = pop(&stack) _ = pop(&stack)
continue 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 is_intrinsic_call(checker, expr, "some") {
if len(expr.args) != 1 { if len(expr.args) != 1 {
id := source.addf(checker.diagnostics, expr.span, "some! expects 1 argument, got %d", len(expr.args)) id := source.addf(checker.diagnostics, expr.span, "some! expects 1 argument, got %d", len(expr.args))
+163
View File
@@ -2421,6 +2421,166 @@ ct_eval_division_call :: proc(
return ct_eval_division_builtin(state, kind, left, right, expr.span) 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..<source_count {
if source.kind == .String {
values[index] = ct_add_value(state, Ct_Value{
kind=.Integer, type=types.U8, integer=i128(state.checker.ast_module.strings[source.index][index]),
})
continue
}
source_places[index] = ct_memory_element_place(state, source, index)
if source_places[index] == INVALID_CT_PLACE {
return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "comptime memory source no longer points to live storage")
}
// ponytail: O(n^2) is simplest here; use canonical intervals if large comptime copies become common.
for destination_place in destination_places {
if ct_places_equal(state, source_places[index], destination_place) {
return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "memcopy! source and destination overlap")
}
}
value, value_ok := ct_place_get(state, source_places[index])
if !value_ok {
return INVALID_CT_VALUE, ct_flow(.Normal), false
}
values[index] = value
}
for place, index in destination_places {
if !ct_place_set(state, place, values[index]) {
return INVALID_CT_VALUE, ct_flow(.Normal), false
}
}
return ct_add_value(state, Ct_Value{kind=.Void, type=types.VOID}), ct_flow(.Normal), true
}
ct_scalar_cast :: proc(state: ^Ct_State, id: Ct_Value_Id, target: types.Type, span: source.Span) -> (Ct_Value_Id, Ct_Flow, bool) { ct_scalar_cast :: proc(state: ^Ct_State, id: Ct_Value_Id, target: types.Type, span: source.Span) -> (Ct_Value_Id, Ct_Flow, bool) {
if id == INVALID_CT_VALUE || int(id) >= len(state.values) { if id == INVALID_CT_VALUE || int(id) >= len(state.values) {
return INVALID_CT_VALUE, ct_flow(.Normal), false 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 { if builtin := division_builtin_call(checker, expr); builtin != .None {
return ct_eval_division_call(state, expr, builtin, expected, depth+1) 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 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")
+3
View File
@@ -79,6 +79,7 @@ Expr_Kind :: enum u8 {
Float, Float,
String, String,
Bool, Bool,
Undefined,
Array, Array,
Struct, Struct,
None, None,
@@ -137,6 +138,8 @@ Expr_Kind :: enum u8 {
And, And,
Or, Or,
Range, Range,
Mem_Copy,
Mem_Set,
Call, Call,
} }
+3
View File
@@ -68,6 +68,7 @@ Linkage :: enum u8 {
Opcode :: enum u8 { Opcode :: enum u8 {
Param, Param,
Const, Const,
Poison,
String, String,
Aggregate, Aggregate,
None, None,
@@ -125,6 +126,8 @@ Opcode :: enum u8 {
Shift_Right, Shift_Right,
Shift_Left_Saturating, Shift_Left_Saturating,
Compare, Compare,
Mem_Copy,
Mem_Set,
Label, Label,
Br, Br,
Cond_Br, Cond_Br,
+131 -3
View File
@@ -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) 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( valid_value :: proc(
instructions: []ir.Instruction, instructions: []ir.Instruction,
value_id: ir.Instruction_Id, value_id: ir.Instruction_Id,
@@ -252,7 +275,7 @@ valid_value :: proc(
return false return false
} }
switch instructions[value_id].op { 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, .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,
@@ -264,7 +287,7 @@ valid_value :: proc(
.Shift_Left, .Shift_Right, .Shift_Left_Saturating, .Compare, .Call: .Shift_Left, .Shift_Right, .Shift_Left_Saturating, .Compare, .Call:
return true return true
case .Address_Global, .Alloca, .Index_Address, .Field_Address, .Orelse_Begin, 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
} }
return false return false
@@ -333,6 +356,8 @@ write_operand :: proc(
value := instructions[value_id] value := instructions[value_id]
if value.op == .Const { if value.op == .Const {
write_constant(builder, value.integer, expected, store) write_constant(builder, value.integer, expected, store)
} else if value.op == .Poison {
strings.write_string(builder, "poison")
} else { } else {
fmt.sbprintf(builder, "%%v%d", value_id) fmt.sbprintf(builder, "%%v%d", value_id)
} }
@@ -925,7 +950,7 @@ emit_instruction_stream :: proc(
after_terminator = false after_terminator = false
} }
switch instruction.op { switch instruction.op {
case .Param, .Const: case .Param, .Const, .Poison:
case .String: case .String:
string_id := int(instruction.integer) string_id := int(instruction.integer)
_, array, pointer_ok := types.array_pointer(instruction.type, &emitter.module.types) _, array, pointer_ok := types.array_pointer(instruction.type, &emitter.module.types)
@@ -1368,6 +1393,109 @@ emit_instruction_stream :: proc(
instruction.a, instruction.a,
types.size(instruction.type, &emitter.module.types, emitter.module.target), 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: case .Slice:
if !valid_instruction(instructions, instruction.a) { if !valid_instruction(instructions, instruction.a) {
emit_recovery_value(emitter, instruction_index, instruction, "invalid slice container") emit_recovery_value(emitter, instruction_index, instruction, "invalid slice container")
+34 -2
View File
@@ -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) 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 { lower_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instruction_Id {
stack := state.expr_stack stack := state.expr_stack
state.expr_stack = nil 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, target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC,
}) })
_ = pop(&stack) _ = 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, case .String, .Array, .Struct, .Range, .None, .Optional_Some, .Address, .Deref,
.Index, .Slice, .Field, .Union_Tag, .Length, .Slice_Ptr, .Unwrap, .Orelse, .Index, .Slice, .Field, .Union_Tag, .Length, .Slice_Ptr, .Unwrap, .Orelse,
.Try, .Catch, .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or: .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}) append(&stack, Lower_Expr_Frame{expr=expr.left})
case .Add, .Sub, .Mul, .Div, .Div_Trunc, .Div_Floor, .Div_Exact, .Div_Ceil, 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, .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 stack[frame_index].stage = 2
append(&stack, Lower_Expr_Frame{expr=expr.left}) append(&stack, Lower_Expr_Frame{expr=expr.left})
case .Call: case .Call:
@@ -823,6 +848,7 @@ lower_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instruction_Id {
} }
if frame.stage == 3 { if frame.stage == 3 {
op := ir.Opcode.Add_Checked op := ir.Opcode.Add_Checked
result_type := expr.type
#partial switch expr.kind { #partial switch expr.kind {
case .Sub: op = .Sub_Checked case .Sub: op = .Sub_Checked
case .Mul: op = .Mul_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_Left: op = .Shift_Left
case .Shift_Right: op = .Shift_Right case .Shift_Right: op = .Shift_Right
case .Shift_Left_Saturating: op = .Shift_Left_Saturating 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{ last = append_instruction(state, ir.Instruction{
op=op, 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, a=frame.left, b=last, diagnostic=source.INVALID_DIAGNOSTIC,
}) })
_ = pop(&stack) _ = pop(&stack)
+223 -2
View File
@@ -2230,7 +2230,9 @@ old_intrinsic_spellings_are_not_recognized :: proc(t: ^testing.T) {
@(test) @(test)
bare_intrinsic_names_are_available_to_user_functions :: proc(t: ^testing.T) { 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 } sizeof func() i32 { return 2 }
alignof func() i32 { return 3 } alignof func() i32 { return 3 }
minval func() i32 { return 4 } minval func() i32 { return 4 }
@@ -2241,19 +2243,29 @@ divexact func() i32 { return 8 }
divceil func() i32 { return 9 } divceil func() i32 { return 9 }
rem func() i32 { return 10 } rem func() i32 { return 10 }
mod func() i32 { return 11 } mod func() i32 { return 11 }
memcopy func() i32 { return 12 }
memset func() i32 { return 13 }
main func() i32 { main func() i32 {
return ptrcast() + sizeof() + alignof() + minval() + maxval() + 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" directory := "/tmp/brolang-test-user-intrinsic-names"
main_path := "/tmp/brolang-test-user-intrinsic-names/main.bro" 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" output := "/tmp/brolang-test-user-intrinsic-names-output"
_ = os2.remove_all(directory) _ = os2.remove_all(directory)
defer _ = os2.remove_all(directory) defer _ = os2.remove_all(directory)
defer _ = os.remove(output) defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil) 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)) 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) 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) 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) @(test)
malformed_intrinsic_calls_have_targeted_parse_diagnostics :: proc(t: ^testing.T) { malformed_intrinsic_calls_have_targeted_parse_diagnostics :: proc(t: ^testing.T) {
cases := [?]struct { cases := [?]struct {
+1 -4
View File
@@ -174,10 +174,7 @@ hide c_realloc func(_ ?@mut anyopaque, memory ?*mut u8, old_size usize, new_size
if new_size < copy_size { if new_size < copy_size {
copy_size = new_size copy_size = new_size
} }
i usize = 0 memcopy!(new_bytes[..copy_size], old_memory[..copy_size])
while i < copy_size : i += 1 {
new_bytes[i] = old_memory[i]
}
c.free(old_memory) c.free(old_memory)
} }
return new_memory return new_memory