| author | |
| committer | |
| log | aaf46187ab7bf23c87a275e2755a9bd01f3c93cf |
| tree | 7359cf8adeca3493e9e9865093ba68e0763d4ee1 |
| parent | fa08e49f839d47441af9a1bcf23dc1701849379e |
| parent | 703fe0f121df9df44721250528e508762e161780 |
| signature |
codegen/llvm: truncate padding bits when loading a non-byte-sized value5 files changed, 321 insertions(+), 12 deletions(-)
src/arch/wasm/CodeGen.zig+13-3| ... | ... | @@ -4353,9 +4353,21 @@ fn intcast(func: *CodeGen, operand: WValue, given: Type, wanted: Type) InnerErro |
| 4353 | 4353 | if (op_bits > 32 and op_bits <= 64 and wanted_bits == 32) { |
| 4354 | 4354 | try func.emitWValue(operand); |
| 4355 | 4355 | try func.addTag(.i32_wrap_i64); |
| 4356 | if (given.isSignedInt(mod) and wanted_bitsize < 32) | |
| 4357 | return func.wrapOperand(.{ .stack = {} }, wanted) | |
| 4358 | else | |
| 4359 | return WValue{ .stack = {} }; | |
| 4356 | 4360 | } else if (op_bits == 32 and wanted_bits > 32 and wanted_bits <= 64) { |
| 4357 | try func.emitWValue(operand); | |
| 4361 | const operand32 = if (given_bitsize < 32 and wanted.isSignedInt(mod)) | |
| 4362 | try func.signExtendInt(operand, given) | |
| 4363 | else | |
| 4364 | operand; | |
| 4365 | try func.emitWValue(operand32); | |
| 4358 | 4366 | try func.addTag(if (wanted.isSignedInt(mod)) .i64_extend_i32_s else .i64_extend_i32_u); |
| 4367 | if (given.isSignedInt(mod) and wanted_bitsize < 64) | |
| 4368 | return func.wrapOperand(.{ .stack = {} }, wanted) | |
| 4369 | else | |
| 4370 | return WValue{ .stack = {} }; | |
| 4359 | 4371 | } else if (wanted_bits == 128) { |
| 4360 | 4372 | // for 128bit integers we store the integer in the virtual stack, rather than a local |
| 4361 | 4373 | const stack_ptr = try func.allocStack(wanted); |
| ... | ... | @@ -4381,8 +4393,6 @@ fn intcast(func: *CodeGen, operand: WValue, given: Type, wanted: Type) InnerErro |
| 4381 | 4393 | } |
| 4382 | 4394 | return stack_ptr; |
| 4383 | 4395 | } else return func.load(operand, wanted, 0); |
| 4384 | ||
| 4385 | return WValue{ .stack = {} }; | |
| 4386 | 4396 | } |
| 4387 | 4397 | |
| 4388 | 4398 | fn airIsNull(func: *CodeGen, inst: Air.Inst.Index, opcode: wasm.Opcode, op_kind: enum { value, ptr }) InnerError!void { |
src/codegen/llvm.zig+55-8| ... | ... | @@ -6179,7 +6179,6 @@ pub const FuncGen = struct { |
| 6179 | 6179 | const elem_alignment = elem_ty.abiAlignment(mod).toLlvm(); |
| 6180 | 6180 | return self.loadByRef(elem_ptr, elem_ty, elem_alignment, .normal); |
| 6181 | 6181 | } else { |
| 6182 | const elem_llvm_ty = try o.lowerType(elem_ty); | |
| 6183 | 6182 | if (Air.refToIndex(bin_op.lhs)) |lhs_index| { |
| 6184 | 6183 | if (self.air.instructions.items(.tag)[lhs_index] == .load) { |
| 6185 | 6184 | const load_data = self.air.instructions.items(.data)[lhs_index]; |
| ... | ... | @@ -6201,7 +6200,7 @@ pub const FuncGen = struct { |
| 6201 | 6200 | &indices, |
| 6202 | 6201 | "", |
| 6203 | 6202 | ); |
| 6204 | return self.wip.load(.normal, elem_llvm_ty, gep, .default, ""); | |
| 6203 | return self.loadTruncate(.normal, elem_ty, gep, .default); | |
| 6205 | 6204 | }, |
| 6206 | 6205 | else => {}, |
| 6207 | 6206 | } |
| ... | ... | @@ -6210,7 +6209,7 @@ pub const FuncGen = struct { |
| 6210 | 6209 | } |
| 6211 | 6210 | const elem_ptr = |
| 6212 | 6211 | try self.wip.gep(.inbounds, array_llvm_ty, array_llvm_val, &indices, ""); |
| 6213 | return self.wip.load(.normal, elem_llvm_ty, elem_ptr, .default, ""); | |
| 6212 | return self.loadTruncate(.normal, elem_ty, elem_ptr, .default); | |
| 6214 | 6213 | } |
| 6215 | 6214 | } |
| 6216 | 6215 | |
| ... | ... | @@ -6378,13 +6377,12 @@ pub const FuncGen = struct { |
| 6378 | 6377 | const payload_index = @intFromBool(layout.tag_align.compare(.gte, layout.payload_align)); |
| 6379 | 6378 | const field_ptr = |
| 6380 | 6379 | try self.wip.gepStruct(union_llvm_ty, struct_llvm_val, payload_index, ""); |
| 6381 | const llvm_field_ty = try o.lowerType(field_ty); | |
| 6382 | 6380 | const payload_alignment = layout.payload_align.toLlvm(); |
| 6383 | 6381 | if (isByRef(field_ty, mod)) { |
| 6384 | 6382 | if (canElideLoad(self, body_tail)) return field_ptr; |
| 6385 | 6383 | return self.loadByRef(field_ptr, field_ty, payload_alignment, .normal); |
| 6386 | 6384 | } else { |
| 6387 | return self.wip.load(.normal, llvm_field_ty, field_ptr, payload_alignment, ""); | |
| 6385 | return self.loadTruncate(.normal, field_ty, field_ptr, payload_alignment); | |
| 6388 | 6386 | } |
| 6389 | 6387 | }, |
| 6390 | 6388 | else => unreachable, |
| ... | ... | @@ -10219,8 +10217,7 @@ pub const FuncGen = struct { |
| 10219 | 10217 | |
| 10220 | 10218 | return fg.loadByRef(payload_ptr, payload_ty, payload_alignment, .normal); |
| 10221 | 10219 | } |
| 10222 | const payload_llvm_ty = try o.lowerType(payload_ty); | |
| 10223 | return fg.wip.load(.normal, payload_llvm_ty, payload_ptr, payload_alignment, ""); | |
| 10220 | return fg.loadTruncate(.normal, payload_ty, payload_ptr, payload_alignment); | |
| 10224 | 10221 | } |
| 10225 | 10222 | |
| 10226 | 10223 | assert(!isByRef(payload_ty, mod)); |
| ... | ... | @@ -10321,6 +10318,56 @@ pub const FuncGen = struct { |
| 10321 | 10318 | } |
| 10322 | 10319 | } |
| 10323 | 10320 | |
| 10321 | /// Load a value and, if needed, mask out padding bits for non byte-sized integer values. | |
| 10322 | fn loadTruncate( | |
| 10323 | fg: *FuncGen, | |
| 10324 | access_kind: Builder.MemoryAccessKind, | |
| 10325 | payload_ty: Type, | |
| 10326 | payload_ptr: Builder.Value, | |
| 10327 | payload_alignment: Builder.Alignment, | |
| 10328 | ) !Builder.Value { | |
| 10329 | // from https://llvm.org/docs/LangRef.html#load-instruction : | |
| 10330 | // "When loading a value of a type like i20 with a size that is not an integral number of bytes, the result is undefined if the value was not originally written using a store of the same type. " | |
| 10331 | // => so load the byte aligned value and trunc the unwanted bits. | |
| 10332 | ||
| 10333 | const o = fg.dg.object; | |
| 10334 | const mod = o.module; | |
| 10335 | const payload_llvm_ty = try o.lowerType(payload_ty); | |
| 10336 | const abi_size = payload_ty.abiSize(mod); | |
| 10337 | ||
| 10338 | // llvm bug workarounds: | |
| 10339 | const workaround_explicit_mask = o.target.cpu.arch == .powerpc and abi_size >= 4; | |
| 10340 | const workaround_disable_truncate = o.target.cpu.arch == .wasm32 and abi_size >= 4; | |
| 10341 | ||
| 10342 | if (workaround_disable_truncate) { | |
| 10343 | // see https://github.com/llvm/llvm-project/issues/64222 | |
| 10344 | // disable the truncation codepath for larger that 32bits value - with this heuristic, the backend passes the test suite. | |
| 10345 | return try fg.wip.load(access_kind, payload_llvm_ty, payload_ptr, payload_alignment, ""); | |
| 10346 | } | |
| 10347 | ||
| 10348 | const load_llvm_ty = if (payload_ty.isAbiInt(mod)) | |
| 10349 | try o.builder.intType(@intCast(abi_size * 8)) | |
| 10350 | else | |
| 10351 | payload_llvm_ty; | |
| 10352 | const loaded = try fg.wip.load(access_kind, load_llvm_ty, payload_ptr, payload_alignment, ""); | |
| 10353 | const shifted = if (payload_llvm_ty != load_llvm_ty and o.target.cpu.arch.endian() == .Big) | |
| 10354 | try fg.wip.bin(.lshr, loaded, try o.builder.intValue( | |
| 10355 | load_llvm_ty, | |
| 10356 | (payload_ty.abiSize(mod) - (std.math.divCeil(u64, payload_ty.bitSize(mod), 8) catch unreachable)) * 8, | |
| 10357 | ), "") | |
| 10358 | else | |
| 10359 | loaded; | |
| 10360 | ||
| 10361 | const anded = if (workaround_explicit_mask and payload_llvm_ty != load_llvm_ty) blk: { | |
| 10362 | // this is rendundant with llvm.trunc. But without it, llvm17 emits invalid code for powerpc. | |
| 10363 | var mask_val = try o.builder.intConst(payload_llvm_ty, -1); | |
| 10364 | mask_val = try o.builder.castConst(.zext, mask_val, load_llvm_ty); | |
| 10365 | break :blk try fg.wip.bin(.@"and", shifted, mask_val.toValue(), ""); | |
| 10366 | } else shifted; | |
| 10367 | ||
| 10368 | return fg.wip.conv(.unneeded, anded, payload_llvm_ty, ""); | |
| 10369 | } | |
| 10370 | ||
| 10324 | 10371 | /// Load a by-ref type by constructing a new alloca and performing a memcpy. |
| 10325 | 10372 | fn loadByRef( |
| 10326 | 10373 | fg: *FuncGen, |
| ... | ... | @@ -10378,7 +10425,7 @@ pub const FuncGen = struct { |
| 10378 | 10425 | if (isByRef(elem_ty, mod)) { |
| 10379 | 10426 | return self.loadByRef(ptr, elem_ty, ptr_alignment, access_kind); |
| 10380 | 10427 | } |
| 10381 | return self.wip.load(access_kind, try o.lowerType(elem_ty), ptr, ptr_alignment, ""); | |
| 10428 | return self.loadTruncate(access_kind, elem_ty, ptr, ptr_alignment); | |
| 10382 | 10429 | } |
| 10383 | 10430 | |
| 10384 | 10431 | const containing_int_ty = try o.builder.intType(@intCast(info.packed_offset.host_size * 8)); |
test/behavior/cast_int.zig+195| ... | ... | @@ -1,6 +1,7 @@ |
| 1 | 1 | const builtin = @import("builtin"); |
| 2 | 2 | const std = @import("std"); |
| 3 | 3 | const expect = std.testing.expect; |
| 4 | const expectEqual = std.testing.expectEqual; | |
| 4 | 5 | const maxInt = std.math.maxInt; |
| 5 | 6 | |
| 6 | 7 | test "@intCast i32 to u7" { |
| ... | ... | @@ -28,3 +29,197 @@ test "coerce i8 to i32 and @intCast back" { |
| 28 | 29 | var y2: i8 = -5; |
| 29 | 30 | try expect(y2 == @as(i8, @intCast(x2))); |
| 30 | 31 | } |
| 32 | ||
| 33 | test "coerce non byte-sized integers accross 32bits boundary" { | |
| 34 | { | |
| 35 | var v: u21 = 6417; | |
| 36 | const a: u32 = v; | |
| 37 | const b: u64 = v; | |
| 38 | const c: u64 = a; | |
| 39 | var w: u64 = 0x1234567812345678; | |
| 40 | const d: u21 = @truncate(w); | |
| 41 | const e: u60 = d; | |
| 42 | try expectEqual(@as(u32, 6417), a); | |
| 43 | try expectEqual(@as(u64, 6417), b); | |
| 44 | try expectEqual(@as(u64, 6417), c); | |
| 45 | try expectEqual(@as(u21, 0x145678), d); | |
| 46 | try expectEqual(@as(u60, 0x145678), e); | |
| 47 | } | |
| 48 | ||
| 49 | { | |
| 50 | var v: u10 = 234; | |
| 51 | const a: u32 = v; | |
| 52 | const b: u64 = v; | |
| 53 | const c: u64 = a; | |
| 54 | var w: u64 = 0x1234567812345678; | |
| 55 | const d: u10 = @truncate(w); | |
| 56 | const e: u60 = d; | |
| 57 | try expectEqual(@as(u32, 234), a); | |
| 58 | try expectEqual(@as(u64, 234), b); | |
| 59 | try expectEqual(@as(u64, 234), c); | |
| 60 | try expectEqual(@as(u21, 0x278), d); | |
| 61 | try expectEqual(@as(u60, 0x278), e); | |
| 62 | } | |
| 63 | { | |
| 64 | var v: u7 = 11; | |
| 65 | const a: u32 = v; | |
| 66 | const b: u64 = v; | |
| 67 | const c: u64 = a; | |
| 68 | var w: u64 = 0x1234567812345678; | |
| 69 | const d: u7 = @truncate(w); | |
| 70 | const e: u60 = d; | |
| 71 | try expectEqual(@as(u32, 11), a); | |
| 72 | try expectEqual(@as(u64, 11), b); | |
| 73 | try expectEqual(@as(u64, 11), c); | |
| 74 | try expectEqual(@as(u21, 0x78), d); | |
| 75 | try expectEqual(@as(u60, 0x78), e); | |
| 76 | } | |
| 77 | ||
| 78 | { | |
| 79 | var v: i21 = -6417; | |
| 80 | const a: i32 = v; | |
| 81 | const b: i64 = v; | |
| 82 | const c: i64 = a; | |
| 83 | var w: i64 = -12345; | |
| 84 | const d: i21 = @intCast(w); | |
| 85 | const e: i60 = d; | |
| 86 | try expectEqual(@as(i32, -6417), a); | |
| 87 | try expectEqual(@as(i64, -6417), b); | |
| 88 | try expectEqual(@as(i64, -6417), c); | |
| 89 | try expectEqual(@as(i21, -12345), d); | |
| 90 | try expectEqual(@as(i60, -12345), e); | |
| 91 | } | |
| 92 | ||
| 93 | { | |
| 94 | var v: i10 = -234; | |
| 95 | const a: i32 = v; | |
| 96 | const b: i64 = v; | |
| 97 | const c: i64 = a; | |
| 98 | var w: i64 = -456; | |
| 99 | const d: i10 = @intCast(w); | |
| 100 | const e: i60 = d; | |
| 101 | try expectEqual(@as(i32, -234), a); | |
| 102 | try expectEqual(@as(i64, -234), b); | |
| 103 | try expectEqual(@as(i64, -234), c); | |
| 104 | try expectEqual(@as(i10, -456), d); | |
| 105 | try expectEqual(@as(i60, -456), e); | |
| 106 | } | |
| 107 | { | |
| 108 | var v: i7 = -11; | |
| 109 | const a: i32 = v; | |
| 110 | const b: i64 = v; | |
| 111 | const c: i64 = a; | |
| 112 | var w: i64 = -42; | |
| 113 | const d: i7 = @intCast(w); | |
| 114 | const e: i60 = d; | |
| 115 | try expectEqual(@as(i32, -11), a); | |
| 116 | try expectEqual(@as(i64, -11), b); | |
| 117 | try expectEqual(@as(i64, -11), c); | |
| 118 | try expectEqual(@as(i7, -42), d); | |
| 119 | try expectEqual(@as(i60, -42), e); | |
| 120 | } | |
| 121 | } | |
| 122 | ||
| 123 | const Piece = packed struct { | |
| 124 | color: Color, | |
| 125 | type: Type, | |
| 126 | ||
| 127 | const Type = enum { KING, QUEEN, BISHOP, KNIGHT, ROOK, PAWN }; | |
| 128 | const Color = enum { WHITE, BLACK }; | |
| 129 | ||
| 130 | fn charToPiece(c: u8) !@This() { | |
| 131 | return .{ | |
| 132 | .type = try charToPieceType(c), | |
| 133 | .color = if (std.ascii.isUpper(c)) Color.WHITE else Color.BLACK, | |
| 134 | }; | |
| 135 | } | |
| 136 | ||
| 137 | fn charToPieceType(c: u8) !Type { | |
| 138 | return switch (std.ascii.toLower(c)) { | |
| 139 | 'p' => .PAWN, | |
| 140 | 'k' => .KING, | |
| 141 | 'q' => .QUEEN, | |
| 142 | 'b' => .BISHOP, | |
| 143 | 'n' => .KNIGHT, | |
| 144 | 'r' => .ROOK, | |
| 145 | else => error.UnexpectedCharError, | |
| 146 | }; | |
| 147 | } | |
| 148 | }; | |
| 149 | ||
| 150 | test "load non byte-sized optional value" { | |
| 151 | // Originally reported at https://github.com/ziglang/zig/issues/14200 | |
| 152 | // note: this bug is triggered by the == operator, expectEqual will hide it | |
| 153 | var opt: ?Piece = try Piece.charToPiece('p'); | |
| 154 | try expect(opt.?.type == .PAWN); | |
| 155 | try expect(opt.?.color == .BLACK); | |
| 156 | ||
| 157 | var p: Piece = undefined; | |
| 158 | @as(*u8, @ptrCast(&p)).* = 0b11111011; | |
| 159 | try expect(p.type == .PAWN); | |
| 160 | try expect(p.color == .BLACK); | |
| 161 | } | |
| 162 | ||
| 163 | test "load non byte-sized value in struct" { | |
| 164 | if (builtin.cpu.arch.endian() != .Little) return error.SkipZigTest; // packed struct TODO | |
| 165 | ||
| 166 | // note: this bug is triggered by the == operator, expectEqual will hide it | |
| 167 | // using ptrCast not to depend on unitialised memory state | |
| 168 | ||
| 169 | var struct0: struct { | |
| 170 | p: Piece, | |
| 171 | int: u8, | |
| 172 | } = undefined; | |
| 173 | @as(*u8, @ptrCast(&struct0.p)).* = 0b11111011; | |
| 174 | try expect(struct0.p.type == .PAWN); | |
| 175 | try expect(struct0.p.color == .BLACK); | |
| 176 | ||
| 177 | var struct1: packed struct { | |
| 178 | p0: Piece, | |
| 179 | p1: Piece, | |
| 180 | pad: u1, | |
| 181 | p2: Piece, | |
| 182 | } = undefined; | |
| 183 | @as(*u8, @ptrCast(&struct1.p0)).* = 0b11111011; | |
| 184 | struct1.p1 = try Piece.charToPiece('p'); | |
| 185 | struct1.p2 = try Piece.charToPiece('p'); | |
| 186 | try expect(struct1.p0.type == .PAWN); | |
| 187 | try expect(struct1.p0.color == .BLACK); | |
| 188 | try expect(struct1.p1.type == .PAWN); | |
| 189 | try expect(struct1.p1.color == .BLACK); | |
| 190 | try expect(struct1.p2.type == .PAWN); | |
| 191 | try expect(struct1.p2.color == .BLACK); | |
| 192 | } | |
| 193 | ||
| 194 | test "load non byte-sized value in union" { | |
| 195 | if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; | |
| 196 | if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; | |
| 197 | if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; | |
| 198 | if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest; | |
| 199 | if (builtin.zig_backend == .stage2_x86_64) return error.SkipZigTest; | |
| 200 | if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; | |
| 201 | ||
| 202 | // note: this bug is triggered by the == operator, expectEqual will hide it | |
| 203 | // using ptrCast not to depend on unitialised memory state | |
| 204 | ||
| 205 | var union0: packed union { | |
| 206 | p: Piece, | |
| 207 | int: u8, | |
| 208 | } = .{ .int = 0 }; | |
| 209 | union0.int = 0b11111011; | |
| 210 | try expect(union0.p.type == .PAWN); | |
| 211 | try expect(union0.p.color == .BLACK); | |
| 212 | ||
| 213 | var union1: union { | |
| 214 | p: Piece, | |
| 215 | int: u8, | |
| 216 | } = .{ .p = .{ .color = .WHITE, .type = .KING } }; | |
| 217 | @as(*u8, @ptrCast(&union1.p)).* = 0b11111011; | |
| 218 | try expect(union1.p.type == .PAWN); | |
| 219 | try expect(union1.p.color == .BLACK); | |
| 220 | ||
| 221 | var pieces: [3]Piece = undefined; | |
| 222 | @as(*u8, @ptrCast(&pieces[1])).* = 0b11111011; | |
| 223 | try expect(pieces[1].type == .PAWN); | |
| 224 | try expect(pieces[1].color == .BLACK); | |
| 225 | } |
test/behavior/packed-struct.zig+30-1| ... | ... | @@ -991,7 +991,7 @@ test "bitcast back and forth" { |
| 991 | 991 | |
| 992 | 992 | test "field access of packed struct smaller than its abi size inside struct initialized with rls" { |
| 993 | 993 | // Originally reported at https://github.com/ziglang/zig/issues/14200 |
| 994 | if (builtin.zig_backend == .stage2_llvm and builtin.cpu.arch == .arm) return error.SkipZigTest; | |
| 994 | ||
| 995 | 995 | const S = struct { |
| 996 | 996 | ps: packed struct { x: i2, y: i2 }, |
| 997 | 997 | |
| ... | ... | @@ -1036,3 +1036,32 @@ test "modify nested packed struct aligned field" { |
| 1036 | 1036 | try std.testing.expectEqual(@as(u8, 1), opts.pretty_print.indent); |
| 1037 | 1037 | try std.testing.expect(!opts.baz); |
| 1038 | 1038 | } |
| 1039 | ||
| 1040 | test "assigning packed struct inside another packed struct" { | |
| 1041 | // Originally reported at https://github.com/ziglang/zig/issues/9674 | |
| 1042 | ||
| 1043 | const S = struct { | |
| 1044 | const Inner = packed struct { | |
| 1045 | bits: u3, | |
| 1046 | more_bits: u6, | |
| 1047 | }; | |
| 1048 | ||
| 1049 | const Outer = packed struct { | |
| 1050 | padding: u5, | |
| 1051 | inner: Inner, | |
| 1052 | }; | |
| 1053 | fn t(inner: Inner) void { | |
| 1054 | r.inner = inner; | |
| 1055 | } | |
| 1056 | ||
| 1057 | var mem: Outer = undefined; | |
| 1058 | var r: *volatile Outer = &mem; | |
| 1059 | }; | |
| 1060 | ||
| 1061 | const val = S.Inner{ .bits = 1, .more_bits = 11 }; | |
| 1062 | S.mem.padding = 0; | |
| 1063 | S.t(val); | |
| 1064 | ||
| 1065 | try expectEqual(val, S.mem.inner); | |
| 1066 | try expect(S.mem.padding == 0); | |
| 1067 | } |
test/behavior/packed-union.zig+28| ... | ... | @@ -85,3 +85,31 @@ test "flags in packed union at offset" { |
| 85 | 85 | try expectEqual(false, test_bits.adv_flags.adv.flags.enable_1); |
| 86 | 86 | try expectEqual(false, test_bits.adv_flags.adv.flags.enable_2); |
| 87 | 87 | } |
| 88 | ||
| 89 | test "packed union in packed struct" { | |
| 90 | // Originally reported at https://github.com/ziglang/zig/issues/16581 | |
| 91 | ||
| 92 | const ReadRequest = packed struct { key: i32 }; | |
| 93 | const RequestType = enum { | |
| 94 | read, | |
| 95 | insert, | |
| 96 | }; | |
| 97 | const RequestUnion = packed union { | |
| 98 | read: ReadRequest, | |
| 99 | }; | |
| 100 | ||
| 101 | const Request = packed struct { | |
| 102 | active_type: RequestType, | |
| 103 | request: RequestUnion, | |
| 104 | const Self = @This(); | |
| 105 | ||
| 106 | fn init(read: ReadRequest) Self { | |
| 107 | return .{ | |
| 108 | .active_type = .read, | |
| 109 | .request = RequestUnion{ .read = read }, | |
| 110 | }; | |
| 111 | } | |
| 112 | }; | |
| 113 | ||
| 114 | try std.testing.expectEqual(RequestType.read, Request.init(.{ .key = 3 }).active_type); | |
| 115 | } |