authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2026-06-14 11:21:11+01:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2026-06-24 10:20:55+01:00
log45833c031def1faa53f0715202b7eb3f23d326c1
treea09a7b124cab00812d3832f21d4e9ebea7cdf5da
parentfa3a9fcdfaee42ef304f662d65decbb685c705a0
signaturelock-open Commit is signed but in an unrecognized format.

Air.Legalize: introduce new bitcast scalarizations


4 files changed, 125 insertions(+), 39 deletions(-)

src/Air.zig+1-1
...@@ -955,7 +955,7 @@ pub const Inst = struct {...@@ -955,7 +955,7 @@ pub const Inst = struct {
955 /// here is runtime-known, which is usually not allowed for vectors. `Legalize` may emit955 /// here is runtime-known, which is usually not allowed for vectors. `Legalize` may emit
956 /// this instruction when scalarizing vector operations.956 /// this instruction when scalarizing vector operations.
957 ///957 ///
958 /// Uses the `bin_op` field. `lhs` is the vector pointer. `rhs` is the element index. Result958 /// Uses the `bin_op` field. `lhs` is the vector value. `rhs` is the element index. Result
959 /// type is the vector element type.959 /// type is the vector element type.
960 legalize_vec_elem_val,960 legalize_vec_elem_val,
961961
src/Air/Legalize.zig+120-36
...@@ -75,13 +75,6 @@ pub const Feature = enum {...@@ -75,13 +75,6 @@ pub const Feature = enum {
75 scalarize_shl_sat,75 scalarize_shl_sat,
76 scalarize_xor,76 scalarize_xor,
77 scalarize_not,77 scalarize_not,
78 /// Scalarize `bitcast` from or to an array or vector type to `bitcast`s of the elements.
79 /// This does not apply if `@bitSizeOf(Elem) == 8 * @sizeOf(Elem)`.
80 /// When this feature is enabled, all remaining `bitcast`s can be lowered using the old bitcast
81 /// semantics (reinterpret memory) instead of the new bitcast semantics (copy logical bits) and
82 /// the behavior will be equivalent. However, the behavior of `@bitSize` on arrays must be
83 /// changed in `Type.zig` before enabling this feature to conform to the new bitcast semantics.
84 scalarize_bitcast,
85 scalarize_clz,78 scalarize_clz,
86 scalarize_ctz,79 scalarize_ctz,
87 scalarize_popcount,80 scalarize_popcount,
...@@ -122,6 +115,35 @@ pub const Feature = enum {...@@ -122,6 +115,35 @@ pub const Feature = enum {
122 scalarize_select,115 scalarize_select,
123 scalarize_mul_add,116 scalarize_mul_add,
124117
118 // Below are several different features for scalarizing `bitcast` in different scenarios. It is
119 // valid to enable any combination of these features.
120
121 /// Scalarize `bitcast` where the operand or result type is an array.
122 scalarize_bitcast_array,
123 /// Scalarize `bitcast` where either:
124 ///
125 /// * operand type is `@Vector(n, A), but result type is not `@Vector(n, B)`; or
126 /// * result type is `@Vector(n, A), but operand type is not `@Vector(n, B)`
127 ///
128 /// This effectively scalarizes any `bitcast` to/from a vector, *unless* the operation can be
129 /// performed by bitcasting each vector element and returning a vector of the results.
130 ///
131 /// If this feature is enabled, the following AIR instruction tags may be emitted:
132 /// * `.legalize_vec_elem_val`
133 /// * `.legalize_vec_store_elem`
134 scalarize_bitcast_vector_non_elementwise,
135 /// Scalarize `bitcast` where the operand or result type is an array or vector whose element
136 /// type `E` has `@bitSizeOf(E) != 8 * @sizeOf(E)`. These are the cases where the backend may
137 /// need to sign- or zero-extend multiple elements to populate "padding" bits.
138 ///
139 /// Enabling this feature requires changing the behavior of `@bitSize` on arrays in `Type.zig`
140 /// to conform to the new bitcast semantics.
141 ///
142 /// If this feature is enabled, the following AIR instruction tags may be emitted:
143 /// * `.legalize_vec_elem_val`
144 /// * `.legalize_vec_store_elem`
145 scalarize_bitcast_padded_elems,
146
125 /// Legalize (shift lhs, (splat rhs)) -> (shift lhs, rhs)147 /// Legalize (shift lhs, (splat rhs)) -> (shift lhs, rhs)
126 unsplat_shift_rhs,148 unsplat_shift_rhs,
127 /// Legalize reduce of a one element vector to a bitcast.149 /// Legalize reduce of a one element vector to a bitcast.
...@@ -227,7 +249,6 @@ pub const Feature = enum {...@@ -227,7 +249,6 @@ pub const Feature = enum {
227 .shl_sat => .scalarize_shl_sat,249 .shl_sat => .scalarize_shl_sat,
228 .xor => .scalarize_xor,250 .xor => .scalarize_xor,
229 .not => .scalarize_not,251 .not => .scalarize_not,
230 .bitcast => .scalarize_bitcast,
231 .clz => .scalarize_clz,252 .clz => .scalarize_clz,
232 .ctz => .scalarize_ctz,253 .ctz => .scalarize_ctz,
233 .popcount => .scalarize_popcount,254 .popcount => .scalarize_popcount,
...@@ -548,7 +569,11 @@ fn legalizeBody(l: *Legalize, body_start: usize, body_len: usize) Error!void {...@@ -548,7 +569,11 @@ fn legalizeBody(l: *Legalize, body_start: usize, body_len: usize) Error!void {
548 },569 },
549 }570 }
550 },571 },
551 .bitcast => if (l.features.has(.scalarize_bitcast)) {572 .bitcast => if (l.features.hasAny(&.{
573 .scalarize_bitcast_array,
574 .scalarize_bitcast_vector_non_elementwise,
575 .scalarize_bitcast_padded_elems,
576 })) {
552 if (try l.scalarizeBitcastBlockPayload(inst)) |payload| {577 if (try l.scalarizeBitcastBlockPayload(inst)) |payload| {
553 continue :inst l.replaceInst(inst, .block, payload);578 continue :inst l.replaceInst(inst, .block, payload);
554 }579 }
...@@ -1423,35 +1448,94 @@ fn scalarizeBitcastBlockPayload(l: *Legalize, orig_inst: Air.Inst.Index) Error!?...@@ -1423,35 +1448,94 @@ fn scalarizeBitcastBlockPayload(l: *Legalize, orig_inst: Air.Inst.Index) Error!?
1423 const ty_op = l.air_instructions.items(.data)[@intFromEnum(orig_inst)].ty_op;1448 const ty_op = l.air_instructions.items(.data)[@intFromEnum(orig_inst)].ty_op;
14241449
1425 const dest_ty = ty_op.ty.toType();1450 const dest_ty = ty_op.ty.toType();
1426 const dest_legal = switch (dest_ty.zigTypeTag(zcu)) {
1427 else => true,
1428 .array, .vector => legal: {
1429 if (dest_ty.arrayLen(zcu) == 1) break :legal true;
1430 const dest_elem_ty = dest_ty.childType(zcu);
1431 break :legal dest_elem_ty.bitSize(zcu) == 8 * dest_elem_ty.abiSize(zcu);
1432 },
1433 };
1434
1435 const operand_ty = l.typeOf(ty_op.operand);1451 const operand_ty = l.typeOf(ty_op.operand);
1436 const operand_legal = switch (operand_ty.zigTypeTag(zcu)) {
1437 else => true,
1438 .array, .vector => legal: {
1439 if (operand_ty.arrayLen(zcu) == 1) break :legal true;
1440 const operand_elem_ty = operand_ty.childType(zcu);
1441 break :legal operand_elem_ty.bitSize(zcu) == 8 * operand_elem_ty.abiSize(zcu);
1442 },
1443 };
14441452
1445 if (dest_legal and operand_legal) return null;1453 // We exit this block only if the scalarization is actually necessary. Otherwise we will return
1454 // `null` from within the block.
1455 const operand_to_int_ok: bool, const int_to_dest_ok: bool = int_ok: {
1456 const operand_tag = operand_ty.zigTypeTag(zcu);
1457 const dest_tag = dest_ty.zigTypeTag(zcu);
1458
1459 if (operand_tag != .array and
1460 operand_tag != .vector and
1461 dest_tag != .array and
1462 dest_tag != .vector)
1463 {
1464 return null;
1465 }
14461466
1447 if (!operand_legal and !dest_legal and operand_ty.arrayLen(zcu) == dest_ty.arrayLen(zcu)) {1467 // We track the validity of 3 different bitcast operations:
1448 // from_ty and to_ty are both arrays or vectors of types with the same bit size,1468 // * operand -> dest
1449 // so we can do an elementwise bitcast.1469 // * operand -> uint
1450 return try l.scalarizeBlockPayload(orig_inst, .ty_op);1470 // * uint -> dest
1451 }1471 // If operand->dest turns out to be valid, we don't need to scalarize. Otherwise, knowing
1472 // the validity of the other operations helps us lower the scalarization efficiently.
1473 var operand_to_dest: bool = true;
1474 var operand_to_int: bool = true;
1475 var int_to_dest: bool = true;
1476
1477 if (l.features.has(.scalarize_bitcast_array)) {
1478 if (operand_tag == .array) {
1479 operand_to_dest = false;
1480 operand_to_int = false;
1481 }
1482 if (dest_tag == .array) {
1483 operand_to_dest = false;
1484 int_to_dest = false;
1485 }
1486 }
1487
1488 if (l.features.has(.scalarize_bitcast_vector_non_elementwise)) {
1489 if (operand_tag == .vector) operand_to_int = false;
1490 if (dest_tag == .vector) int_to_dest = false;
1491
1492 if (operand_tag == .vector or dest_tag == .vector) {
1493 if (operand_tag != .vector or
1494 dest_tag != .vector or
1495 operand_ty.vectorLen(zcu) != dest_ty.vectorLen(zcu))
1496 {
1497 operand_to_dest = false;
1498 }
1499 }
1500 }
1501
1502 if (l.features.has(.scalarize_bitcast_padded_elems)) {
1503 if (operand_tag == .array or operand_tag == .vector) {
1504 const elem_ty = operand_ty.childType(zcu);
1505 if (elem_ty.bitSize(zcu) != 8 * elem_ty.abiSize(zcu)) {
1506 operand_to_int = false;
1507 operand_to_dest = false;
1508 }
1509 }
1510 if (dest_tag == .array or dest_tag == .vector) {
1511 const elem_ty = dest_ty.childType(zcu);
1512 if (elem_ty.bitSize(zcu) != 8 * elem_ty.abiSize(zcu)) {
1513 int_to_dest = false;
1514 operand_to_dest = false;
1515 }
1516 }
1517 }
1518
1519 if (operand_to_dest) {
1520 return null; // no scalarization needed!
1521 }
1522
1523 // We need a scalarization, but before breaking from the block, check if we can do it
1524 // elementwise---if we can, that's preferable to the generic lowering.
1525 if ((operand_tag == .array or operand_tag == .vector) and
1526 (dest_tag == .array or dest_tag == .vector) and
1527 operand_ty.arrayLenIncludingSentinel(zcu) == dest_ty.arrayLenIncludingSentinel(zcu))
1528 {
1529 // Operand and result types are both arrays/vectors whose element types have the same
1530 // bit size, so we can do an elementwise bitcast.
1531 return try l.scalarizeBlockPayload(orig_inst, .ty_op);
1532 }
1533
1534 break :int_ok .{ operand_to_int, int_to_dest };
1535 };
14521536
1453 // Fallback path. Our strategy is to use an unsigned integer type as an intermediate1537 // Generic scalarization implementation. Our strategy is to use an unsigned integer type as an
1454 // "bag of bits" representation which can be manipulated by bitwise operations.1538 // intermediate "bag of bits" representation which can be manipulated by bitwise operations.
14551539
1456 const num_bits: u16 = @intCast(dest_ty.bitSize(zcu));1540 const num_bits: u16 = @intCast(dest_ty.bitSize(zcu));
1457 assert(operand_ty.bitSize(zcu) == num_bits);1541 assert(operand_ty.bitSize(zcu) == num_bits);
...@@ -1465,7 +1549,7 @@ fn scalarizeBitcastBlockPayload(l: *Legalize, orig_inst: Air.Inst.Index) Error!?...@@ -1465,7 +1549,7 @@ fn scalarizeBitcastBlockPayload(l: *Legalize, orig_inst: Air.Inst.Index) Error!?
1465 // First, convert `operand_ty` to `uint_ty` (`uN`).1549 // First, convert `operand_ty` to `uint_ty` (`uN`).
14661550
1467 const uint_val: Air.Inst.Ref = uint_val: {1551 const uint_val: Air.Inst.Ref = uint_val: {
1468 if (operand_legal) {1552 if (operand_to_int_ok) {
1469 _ = main_block.stealCapacity(19);1553 _ = main_block.stealCapacity(19);
1470 break :uint_val main_block.addBitCast(l, uint_ty, ty_op.operand);1554 break :uint_val main_block.addBitCast(l, uint_ty, ty_op.operand);
1471 }1555 }
...@@ -1560,7 +1644,7 @@ fn scalarizeBitcastBlockPayload(l: *Legalize, orig_inst: Air.Inst.Index) Error!?...@@ -1560,7 +1644,7 @@ fn scalarizeBitcastBlockPayload(l: *Legalize, orig_inst: Air.Inst.Index) Error!?
15601644
1561 // Now convert `uint_ty` (`uN`) to `dest_ty`.1645 // Now convert `uint_ty` (`uN`) to `dest_ty`.
15621646
1563 if (dest_legal) {1647 if (int_to_dest_ok) {
1564 _ = main_block.stealCapacity(17);1648 _ = main_block.stealCapacity(17);
1565 const result = main_block.addBitCast(l, dest_ty, uint_val);1649 const result = main_block.addBitCast(l, dest_ty, uint_val);
1566 main_block.addBr(l, orig_inst, result);1650 main_block.addBr(l, orig_inst, result);
src/codegen/wasm/CodeGen.zig+2-1
...@@ -83,7 +83,6 @@ pub fn legalizeFeatures(_: *const std.Target) *const Air.Legalize.Features {...@@ -83,7 +83,6 @@ pub fn legalizeFeatures(_: *const std.Target) *const Air.Legalize.Features {
83 .scalarize_shl_sat,83 .scalarize_shl_sat,
84 .scalarize_xor,84 .scalarize_xor,
85 .scalarize_not,85 .scalarize_not,
86 .scalarize_bitcast,
87 .scalarize_clz,86 .scalarize_clz,
88 .scalarize_ctz,87 .scalarize_ctz,
89 .scalarize_popcount,88 .scalarize_popcount,
...@@ -120,6 +119,8 @@ pub fn legalizeFeatures(_: *const std.Target) *const Air.Legalize.Features {...@@ -120,6 +119,8 @@ pub fn legalizeFeatures(_: *const std.Target) *const Air.Legalize.Features {
120 .scalarize_shuffle_two,119 .scalarize_shuffle_two,
121 .scalarize_select,120 .scalarize_select,
122 .scalarize_mul_add,121 .scalarize_mul_add,
122
123 .scalarize_bitcast_padded_elems,
123 });124 });
124}125}
125126
src/codegen/x86_64/CodeGen.zig+2-1
...@@ -47,7 +47,6 @@ pub fn legalizeFeatures(_: *const std.Target) *const Air.Legalize.Features {...@@ -47,7 +47,6 @@ pub fn legalizeFeatures(_: *const std.Target) *const Air.Legalize.Features {
47 .scalarize_shl,47 .scalarize_shl,
48 .scalarize_shl_exact,48 .scalarize_shl_exact,
49 .scalarize_shl_sat,49 .scalarize_shl_sat,
50 .scalarize_bitcast,
51 .scalarize_ctz,50 .scalarize_ctz,
52 .scalarize_popcount,51 .scalarize_popcount,
53 .scalarize_byte_swap,52 .scalarize_byte_swap,
...@@ -58,6 +57,8 @@ pub fn legalizeFeatures(_: *const std.Target) *const Air.Legalize.Features {...@@ -58,6 +57,8 @@ pub fn legalizeFeatures(_: *const std.Target) *const Air.Legalize.Features {
58 .scalarize_shuffle_two,57 .scalarize_shuffle_two,
59 .scalarize_select,58 .scalarize_select,
6059
60 .scalarize_bitcast_padded_elems,
61
61 //.unsplat_shift_rhs,62 //.unsplat_shift_rhs,
62 .reduce_one_elem_to_bitcast,63 .reduce_one_elem_to_bitcast,
63 .splat_one_elem_to_bitcast,64 .splat_one_elem_to_bitcast,