aboutsummaryrefslogtreecommitdiff
path: root/src/Sema.zig
diff options
context:
space:
mode:
authorkcbanner <kcbanner@gmail.com>2023-10-01 00:07:21 -0400
committerkcbanner <kcbanner@gmail.com>2023-10-02 13:15:28 -0400
commitd657b6c0e2ab7c47f5416dc4df1abb2bfbecd4b6 (patch)
tree1c28a341c2b8094468bac5121a3dc955f16f8582 /src/Sema.zig
parent53775b0999527ced0550f3abb32c4a4a715af74e (diff)
downloadzig-d657b6c0e2ab7c47f5416dc4df1abb2bfbecd4b6.tar.gz
zig-d657b6c0e2ab7c47f5416dc4df1abb2bfbecd4b6.zip
sema: support reinterpreting extern/packed unions at comptime via field access
My previous change for reading / writing to unions at comptime did not handle union field read/writes correctly in all cases. Previously, if a field was written to a union, it would overwrite the entire value. This is problematic when a field of a larger size is subsequently read, because the value would not be long enough, causing a panic. Additionally, the writing behaviour itself was incorrect. Writing to a field of a packed or extern union should only overwrite the bits corresponding to that field, allowing for memory reintepretation via field writes / reads. I addressed these problems as follows: Add the concept of a "backing type" for extern / packed unions (`Type.unionBackingType`). For extern unions, this is a `u8` array, for packed unions it's an integer matching the `bitSize` of the union. Whenever union memory is read at comptime, it's read as this type. When union memory is written at comptime, the tag may still be known. If so, the memory is written using the tagged type. If the tag is unknown (because this union had previously been read from memory), it's simply written back out as the backing type. I added `write_packed` to the `reinterpret` field of `ComptimePtrMutationKit`. This causes writes of the operand to be packed - which is necessary when writing to a field of a packed union. Without this, writing a value to a `u1` field would overwrite the entire byte it occupied. The final case to address was reading a different (potentially larger) field from a union when it was written with a known tag. To handle this, a new kind of bitcast was introduced (`bitCastUnionFieldVal`) which supports reading a larger field by using a backing buffer that has the unwritten bits set to undefined. The reason to support this (vs always just writing the union as it's backing type), is that no reads to larger fields ever occur at comptime, it would be strictly worse to have spent time writing the full backing type.
Diffstat (limited to 'src/Sema.zig')
-rw-r--r--src/Sema.zig106
1 files changed, 85 insertions, 21 deletions
diff --git a/src/Sema.zig b/src/Sema.zig
index 096ebb0589..26247fbb2c 100644
--- a/src/Sema.zig
+++ b/src/Sema.zig
@@ -27260,7 +27260,7 @@ fn unionFieldVal(
else
union_ty.unionFieldType(un.tag.toValue(), mod).?;
- if (try sema.bitCastVal(block, src, un.val.toValue(), old_ty, field_ty, 0)) |new_val| {
+ if (try sema.bitCastUnionFieldVal(block, src, un.val.toValue(), old_ty, field_ty)) |new_val| {
return Air.internedToRef(new_val.toIntern());
}
}
@@ -29781,13 +29781,19 @@ fn storePtrVal(
error.IllDefinedMemoryLayout => unreachable, // Sema was supposed to emit a compile error already
error.Unimplemented => return sema.fail(block, src, "TODO: implement writeToMemory for type '{}'", .{mut_kit.ty.fmt(mod)}),
};
- operand_val.writeToMemory(operand_ty, mod, buffer[reinterpret.byte_offset..]) catch |err| switch (err) {
- error.OutOfMemory => return error.OutOfMemory,
- error.ReinterpretDeclRef => unreachable,
- error.IllDefinedMemoryLayout => unreachable, // Sema was supposed to emit a compile error already
- error.Unimplemented => return sema.fail(block, src, "TODO: implement writeToMemory for type '{}'", .{operand_ty.fmt(mod)}),
- };
-
+ if (reinterpret.write_packed) {
+ operand_val.writeToPackedMemory(operand_ty, mod, buffer[reinterpret.byte_offset..], 0) catch |err| switch (err) {
+ error.OutOfMemory => return error.OutOfMemory,
+ error.ReinterpretDeclRef => unreachable,
+ };
+ } else {
+ operand_val.writeToMemory(operand_ty, mod, buffer[reinterpret.byte_offset..]) catch |err| switch (err) {
+ error.OutOfMemory => return error.OutOfMemory,
+ error.ReinterpretDeclRef => unreachable,
+ error.IllDefinedMemoryLayout => unreachable, // Sema was supposed to emit a compile error already
+ error.Unimplemented => return sema.fail(block, src, "TODO: implement writeToMemory for type '{}'", .{operand_ty.fmt(mod)}),
+ };
+ }
const val = Value.readFromMemory(mut_kit.ty, mod, buffer, sema.arena) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
error.IllDefinedMemoryLayout => unreachable,
@@ -29819,6 +29825,8 @@ const ComptimePtrMutationKit = struct {
reinterpret: struct {
val_ptr: *Value,
byte_offset: usize,
+ /// If set, write the operand to packed memory
+ write_packed: bool = false,
},
/// If the root decl could not be used as parent, this means `ty` is the type that
/// caused that by not having a well-defined layout.
@@ -30182,21 +30190,43 @@ fn beginComptimePtrMutation(
);
},
.@"union" => {
- // We need to set the active field of the union.
- const union_tag_ty = base_child_ty.unionTagTypeHypothetical(mod);
-
const payload = &val_ptr.castTag(.@"union").?.data;
- payload.tag = try mod.enumValueFieldIndex(union_tag_ty, field_index);
+ const layout = base_child_ty.containerLayout(mod);
- return beginComptimePtrMutationInner(
- sema,
- block,
- src,
- parent.ty.structFieldType(field_index, mod),
- &payload.val,
- ptr_elem_ty,
- parent.mut_decl,
- );
+ const tag_type = base_child_ty.unionTagTypeHypothetical(mod);
+ const hypothetical_tag = try mod.enumValueFieldIndex(tag_type, field_index);
+ if (layout == .Auto or (payload.tag != null and hypothetical_tag.eql(payload.tag.?, tag_type, mod))) {
+ // We need to set the active field of the union.
+ payload.tag = hypothetical_tag;
+
+ const field_ty = parent.ty.structFieldType(field_index, mod);
+ return beginComptimePtrMutationInner(
+ sema,
+ block,
+ src,
+ field_ty,
+ &payload.val,
+ ptr_elem_ty,
+ parent.mut_decl,
+ );
+ } else {
+ // Writing to a different field (a different or unknown tag is active) requires reinterpreting
+ // memory of the entire union, which requires knowing its abiSize.
+ try sema.resolveTypeLayout(parent.ty);
+
+ // This union value no longer has a well-defined tag type.
+ // The reinterpretation will read it back out as .none.
+ payload.val = try payload.val.unintern(sema.arena, mod);
+ return ComptimePtrMutationKit{
+ .mut_decl = parent.mut_decl,
+ .pointee = .{ .reinterpret = .{
+ .val_ptr = val_ptr,
+ .byte_offset = 0,
+ .write_packed = layout == .Packed,
+ } },
+ .ty = parent.ty,
+ };
+ }
},
.slice => switch (field_index) {
Value.slice_ptr_index => return beginComptimePtrMutationInner(
@@ -30697,6 +30727,7 @@ fn bitCastVal(
// For types with well-defined memory layouts, we serialize them a byte buffer,
// then deserialize to the new type.
const abi_size = try sema.usizeCast(block, src, old_ty.abiSize(mod));
+
const buffer = try sema.gpa.alloc(u8, abi_size);
defer sema.gpa.free(buffer);
val.writeToMemory(old_ty, mod, buffer) catch |err| switch (err) {
@@ -30713,6 +30744,39 @@ fn bitCastVal(
};
}
+fn bitCastUnionFieldVal(
+ sema: *Sema,
+ block: *Block,
+ src: LazySrcLoc,
+ val: Value,
+ old_ty: Type,
+ field_ty: Type,
+) !?Value {
+ const mod = sema.mod;
+ if (old_ty.eql(field_ty, mod)) return val;
+
+ const old_size = try sema.usizeCast(block, src, old_ty.abiSize(mod));
+ const field_size = try sema.usizeCast(block, src, field_ty.abiSize(mod));
+
+ const buffer = try sema.gpa.alloc(u8, @max(old_size, field_size));
+ defer sema.gpa.free(buffer);
+ val.writeToMemory(old_ty, mod, buffer) catch |err| switch (err) {
+ error.OutOfMemory => return error.OutOfMemory,
+ error.ReinterpretDeclRef => return null,
+ error.IllDefinedMemoryLayout => unreachable, // Sema was supposed to emit a compile error already
+ error.Unimplemented => return sema.fail(block, src, "TODO: implement writeToMemory for type '{}'", .{old_ty.fmt(mod)}),
+ };
+
+ // Reading a larger value means we need to reinterpret from undefined bytes
+ if (field_size > old_size) @memset(buffer[old_size..], 0xaa);
+
+ return Value.readFromMemory(field_ty, mod, buffer[0..], sema.arena) catch |err| switch (err) {
+ error.OutOfMemory => return error.OutOfMemory,
+ error.IllDefinedMemoryLayout => unreachable,
+ error.Unimplemented => return sema.fail(block, src, "TODO: implement readFromMemory for type '{}'", .{field_ty.fmt(mod)}),
+ };
+}
+
fn coerceArrayPtrToSlice(
sema: *Sema,
block: *Block,