authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2026-05-06 11:01:16+01:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2026-05-07 06:21:59+02:00
logfc1c83a363d32e749b9061044db63852ab7255d0
tree6879df0fe06eab250156898dbf8928cbd8159ff1
parent0bcf29aff6e4ed536406634f03f7bf61eda1a2b8

Air: fix legalization of packed struct init with OPV field

I have verified that this fixes *both* of the reproductions given in https://codeberg.org/ziglang/zig/issues/31837 (they were the same bug). Resolves: https://codeberg.org/ziglang/zig/issues/31837

2 files changed, 38 insertions(+), 10 deletions(-)

src/Air/Legalize.zig+22-10
...@@ -853,14 +853,24 @@ fn legalizeBody(l: *Legalize, body_start: usize, body_len: usize) Error!void {...@@ -853,14 +853,24 @@ fn legalizeBody(l: *Legalize, body_start: usize, body_len: usize) Error!void {
853 .@"union" => unreachable,853 .@"union" => unreachable,
854 .@"struct" => switch (agg_ty.containerLayout(zcu)) {854 .@"struct" => switch (agg_ty.containerLayout(zcu)) {
855 .auto, .@"extern" => {},855 .auto, .@"extern" => {},
856 .@"packed" => switch (agg_ty.structFieldCount(zcu)) {856 .@"packed" => {
857 0 => unreachable,857 // If any field accounts for the full bit size of the struct, this init
858 // An `aggregate_init` of a packed struct with 1 field is just a fancy bitcast.858 // is just equivalent to a bitcast of that field. This usually means the
859 1 => continue :inst l.replaceInst(inst, .bitcast, .{ .ty_op = .{859 // field count is 1, but not always, as there could be zero-bit fields.
860 .ty = .fromType(agg_ty),860 const struct_bits = agg_ty.bitSize(zcu);
861 .operand = @enumFromInt(l.air_extra.items[ty_pl.payload]),861 for (0..agg_ty.structFieldCount(zcu)) |field_index| {
862 } }),862 const field_bits = agg_ty.fieldType(field_index, zcu).bitSize(zcu);
863 else => continue :inst l.replaceInst(inst, .block, try l.packedAggregateInitBlockPayload(inst)),863 if (field_bits == struct_bits) {
864 // Just bitcast this field.
865 continue :inst l.replaceInst(inst, .bitcast, .{ .ty_op = .{
866 .ty = .fromType(agg_ty),
867 .operand = @enumFromInt(l.air_extra.items[ty_pl.payload + field_index]),
868 } });
869 }
870 }
871 // Otherwise, we will need to use a sequence of bitcasts and shifts to
872 // combine multiple values' bits.
873 continue :inst l.replaceInst(inst, .block, try l.packedAggregateInitBlockPayload(inst));
864 },874 },
865 },875 },
866 }876 }
...@@ -2513,8 +2523,10 @@ fn packedAggregateInitBlockPayload(l: *Legalize, orig_inst: Air.Inst.Index) Erro...@@ -2513,8 +2523,10 @@ fn packedAggregateInitBlockPayload(l: *Legalize, orig_inst: Air.Inst.Index) Erro
2513 while (field_idx > 0) {2523 while (field_idx > 0) {
2514 field_idx -= 1;2524 field_idx -= 1;
2515 const field_ty = agg_ty.fieldType(field_idx, zcu);2525 const field_ty = agg_ty.fieldType(field_idx, zcu);
2516 const field_uint_ty = try pt.intType(.unsigned, @intCast(field_ty.bitSize(zcu)));2526 const field_bits: u16 = @intCast(field_ty.bitSize(zcu));
2517 const field_bit_size_ref: Air.Inst.Ref = .fromValue(try pt.intValue(shift_ty, field_ty.bitSize(zcu)));2527 assert(field_bits < num_bits);
2528 const field_uint_ty = try pt.intType(.unsigned, field_bits);
2529 const field_bit_size_ref: Air.Inst.Ref = .fromValue(try pt.intValue(shift_ty, field_bits));
2518 const field_val: Air.Inst.Ref = @enumFromInt(l.air_extra.items[orig_ty_pl.payload + field_idx]);2530 const field_val: Air.Inst.Ref = @enumFromInt(l.air_extra.items[orig_ty_pl.payload + field_idx]);
25192531
2520 const shifted = main_block.addBinOp(l, .shl_exact, cur_uint, field_bit_size_ref).toRef();2532 const shifted = main_block.addBinOp(l, .shl_exact, cur_uint, field_bit_size_ref).toRef();
test/behavior/bitcast.zig+16
...@@ -591,3 +591,19 @@ test "@bitCast of float to extern struct" {...@@ -591,3 +591,19 @@ test "@bitCast of float to extern struct" {
591 try S.doTheTest();591 try S.doTheTest();
592 try comptime S.doTheTest();592 try comptime S.doTheTest();
593}593}
594
595test "@bitCast of packed struct with void field to integer" {
596 const S = packed struct(u8) {
597 v: void,
598 x: u8,
599
600 fn doTheTest(x: u8) !void {
601 // Intentionally using `@as` to avoid RLS which masks the bug
602 const foo = @as(@This(), .{ .v = {}, .x = x });
603 const as_int: u8 = @bitCast(foo);
604 try expect(as_int == x);
605 }
606 };
607 try S.doTheTest(123);
608 try comptime S.doTheTest(123);
609}