authorgravatar for justus@klausecker.deJustus Klausecker <justus@klausecker.de> 2026-07-16 19:05:15+02:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2026-07-17 09:43:53+01:00
log4be6e74b5aeed95f4ed9f2a1c3377149c37c781d
treec295b3695f1fd595e7607ac73f2e6bfc7adf08f4
parentcb4c344e19a269ac227489a96e2ef53dd077ece0
signaturelock-open Commit is signed but in an unrecognized format.

Legalize: improve `expand_bit_cast_safe`/`expand_int_cast_safe` codegen if no checks are necessary

Legalize would previously emit something like this for expansions of `bit_cast_safe`/`int_cast_safe` if it determined that the requested safety checks were superfluous (e.g. `bit_cast_safe` to anything but an enum): ``` %1 = block({ %2 = bit_cast(%dest_ty, %operand) %3 = %br(%1, %2) }) ``` The `block` does absolutely nothing here, ideally we'd just want a plain `bit_cast` (or `int_cast`) instead: ``` %1 = bit_cast(%dest_ty, %operand) ``` which is exactly what this commit implements by checking whether a safety check is even necessary before emitting anything and replacing the `_safe` variant of each inst with the corresponding 'unsafe' variant directly if it isn't.

1 files changed, 47 insertions(+), 51 deletions(-)

src/Air/Legalize.zig+47-51
...@@ -605,7 +605,11 @@ fn legalizeBody(l: *Legalize, body_start: usize, body_len: usize) Error!void {...@@ -605,7 +605,11 @@ fn legalizeBody(l: *Legalize, body_start: usize, body_len: usize) Error!void {
605 }605 }
606 },606 },
607 .bit_cast_safe => if (l.features.has(.expand_bit_cast_safe)) {607 .bit_cast_safe => if (l.features.has(.expand_bit_cast_safe)) {
608 continue :inst l.replaceInst(inst, .block, try l.safeBitcastBlockPayload(inst));608 if (try l.safeBitcastBlockPayload(inst)) |payload| {
609 continue :inst l.replaceInst(inst, .block, payload);
610 }
611 const ty_op = l.air_instructions.items(.data)[@intFromEnum(inst)].ty_op;
612 continue :inst l.replaceInst(inst, .bit_cast, .{ .ty_op = ty_op });
609 } else if (l.features.hasAny(&.{613 } else if (l.features.hasAny(&.{
610 .scalarize_bit_cast_array,614 .scalarize_bit_cast_array,
611 .scalarize_bit_cast_vector_non_elementwise,615 .scalarize_bit_cast_vector_non_elementwise,
...@@ -617,7 +621,11 @@ fn legalizeBody(l: *Legalize, body_start: usize, body_len: usize) Error!void {...@@ -617,7 +621,11 @@ fn legalizeBody(l: *Legalize, body_start: usize, body_len: usize) Error!void {
617 },621 },
618 .int_cast_safe => if (l.features.has(.expand_int_cast_safe)) {622 .int_cast_safe => if (l.features.has(.expand_int_cast_safe)) {
619 assert(!l.features.has(.scalarize_int_cast_safe)); // it doesn't make sense to do both623 assert(!l.features.has(.scalarize_int_cast_safe)); // it doesn't make sense to do both
620 continue :inst l.replaceInst(inst, .block, try l.safeIntcastBlockPayload(inst));624 if (try l.safeIntcastBlockPayload(inst)) |payload| {
625 continue :inst l.replaceInst(inst, .block, payload);
626 }
627 const ty_op = l.air_instructions.items(.data)[@intFromEnum(inst)].ty_op;
628 continue :inst l.replaceInst(inst, .int_cast, .{ .ty_op = ty_op });
621 } else if (l.features.has(.scalarize_int_cast_safe)) {629 } else if (l.features.has(.scalarize_int_cast_safe)) {
622 const ty_op = l.air_instructions.items(.data)[@intFromEnum(inst)].ty_op;630 const ty_op = l.air_instructions.items(.data)[@intFromEnum(inst)].ty_op;
623 if (ty_op.ty.toType().isVector(zcu)) {631 if (ty_op.ty.toType().isVector(zcu)) {
...@@ -2125,7 +2133,7 @@ fn scalarizeReduceBlockPayload(l: *Legalize, orig_inst: Air.Inst.Index, optimize...@@ -2125,7 +2133,7 @@ fn scalarizeReduceBlockPayload(l: *Legalize, orig_inst: Air.Inst.Index, optimize
2125 } };2133 } };
2126}2134}
21272135
2128fn safeBitcastBlockPayload(l: *Legalize, orig_inst: Air.Inst.Index) Error!Air.Inst.Data {2136fn safeBitcastBlockPayload(l: *Legalize, orig_inst: Air.Inst.Index) Error!?Air.Inst.Data {
2129 const pt = l.pt;2137 const pt = l.pt;
2130 const zcu = pt.zcu;2138 const zcu = pt.zcu;
2131 const ty_op = l.air_instructions.items(.data)[@intFromEnum(orig_inst)].ty_op;2139 const ty_op = l.air_instructions.items(.data)[@intFromEnum(orig_inst)].ty_op;
...@@ -2133,7 +2141,14 @@ fn safeBitcastBlockPayload(l: *Legalize, orig_inst: Air.Inst.Index) Error!Air.In...@@ -2133,7 +2141,14 @@ fn safeBitcastBlockPayload(l: *Legalize, orig_inst: Air.Inst.Index) Error!Air.In
2133 const operand_ref = ty_op.operand;2141 const operand_ref = ty_op.operand;
2134 const dest_ty = ty_op.ty.toType();2142 const dest_ty = ty_op.ty.toType();
21352143
2136 // The worst case is a bitcast to an exhaustive enum and looks like this:2144 if (dest_ty.zigTypeTag(zcu) != .@"enum" or
2145 dest_ty.isNonexhaustiveEnum(zcu) or
2146 !zcu.backendSupportsFeature(.is_named_enum_value))
2147 {
2148 return null;
2149 }
2150
2151 // We are building this:
2137 //2152 //
2138 // %x = block({2153 // %x = block({
2139 // %1 = bit_cast(@res_ty, %y)2154 // %1 = bit_cast(@res_ty, %y)
...@@ -2148,54 +2163,32 @@ fn safeBitcastBlockPayload(l: *Legalize, orig_inst: Air.Inst.Index) Error!Air.In...@@ -2148,54 +2163,32 @@ fn safeBitcastBlockPayload(l: *Legalize, orig_inst: Air.Inst.Index) Error!Air.In
21482163
2149 var inst_buf: [6]Air.Inst.Index = undefined;2164 var inst_buf: [6]Air.Inst.Index = undefined;
2150 try l.air_instructions.ensureUnusedCapacity(zcu.gpa, inst_buf.len);2165 try l.air_instructions.ensureUnusedCapacity(zcu.gpa, inst_buf.len);
2151 var opt_condbr: ?CondBr = null;
2152
2153 var main_block: Block = .init(&inst_buf);
2154 var cur_block: *Block = &main_block;
21552166
2156 const cast_inst = cur_block.addBitCast(l, dest_ty, operand_ref);2167 var block: Block = .init(&inst_buf);
21572168
2158 if (dest_ty.zigTypeTag(zcu) == .@"enum" and2169 const cast_inst = block.addBitCast(l, dest_ty, operand_ref);
2159 !dest_ty.isNonexhaustiveEnum(zcu) and2170 const is_named_inst = block.add(l, .{
2160 zcu.backendSupportsFeature(.is_named_enum_value))2171 .tag = .is_named_enum_value,
2161 {2172 .data = .{ .un_op = cast_inst },
2162 // We are building this:
2163 // %1 = is_named_enum_value(%cast_inst)
2164 // %2 = cond_br(%1, {
2165 // <new cursor>
2166 // }, {
2167 // <panic>
2168 // })
2169 const is_named_inst = cur_block.add(l, .{
2170 .tag = .is_named_enum_value,
2171 .data = .{ .un_op = cast_inst },
2172 });
2173 opt_condbr = .init(l, is_named_inst.toRef(), cur_block, .{ .false = .cold });
2174 const condbr = &(opt_condbr.?);
2175 condbr.else_block = .init(cur_block.stealRemainingCapacity());
2176 try condbr.else_block.addPanic(l, .invalid_enum_value);
2177 condbr.then_block = .init(condbr.else_block.stealRemainingCapacity());
2178 cur_block = &condbr.then_block;
2179 }
2180 // Finally, just `br` to our outer `block`.
2181 _ = cur_block.add(l, .{
2182 .tag = .br,
2183 .data = .{ .br = .{
2184 .block_inst = orig_inst,
2185 .operand = cast_inst,
2186 } },
2187 });2173 });
2188 // We might not have used all of the instructions; that's intentional.
2189 _ = cur_block.stealRemainingCapacity();
21902174
2191 if (opt_condbr) |condbr| try condbr.finish(l);2175 var condbr: CondBr = .init(l, is_named_inst.toRef(), &block, .{ .false = .cold });
2176
2177 condbr.then_block = .init(block.stealRemainingCapacity());
2178 condbr.then_block.addBr(l, orig_inst, cast_inst);
2179
2180 condbr.else_block = .init(condbr.then_block.stealRemainingCapacity());
2181 try condbr.else_block.addPanic(l, .invalid_enum_value);
2182
2183 try condbr.finish(l);
2184
2192 return .{ .ty_pl = .{2185 return .{ .ty_pl = .{
2193 .ty = .fromType(dest_ty),2186 .ty = .fromType(dest_ty),
2194 .payload = try l.addBlockBody(main_block.body()),2187 .payload = try l.addBlockBody(block.body()),
2195 } };2188 } };
2196}2189}
21972190
2198fn safeIntcastBlockPayload(l: *Legalize, orig_inst: Air.Inst.Index) Error!Air.Inst.Data {2191fn safeIntcastBlockPayload(l: *Legalize, orig_inst: Air.Inst.Index) Error!?Air.Inst.Data {
2199 const pt = l.pt;2192 const pt = l.pt;
2200 const zcu = pt.zcu;2193 const zcu = pt.zcu;
2201 const ty_op = l.air_instructions.items(.data)[@intFromEnum(orig_inst)].ty_op;2194 const ty_op = l.air_instructions.items(.data)[@intFromEnum(orig_inst)].ty_op;
...@@ -2214,6 +2207,9 @@ fn safeIntcastBlockPayload(l: *Legalize, orig_inst: Air.Inst.Index) Error!Air.In...@@ -2214,6 +2207,9 @@ fn safeIntcastBlockPayload(l: *Legalize, orig_inst: Air.Inst.Index) Error!Air.In
2214 .@"enum" => true,2207 .@"enum" => true,
2215 else => unreachable,2208 else => unreachable,
2216 };2209 };
2210 const have_enum_value_check = dest_is_enum and
2211 !dest_ty.isNonexhaustiveEnum(zcu) and
2212 zcu.backendSupportsFeature(.is_named_enum_value);
22172213
2218 const operand_info = operand_scalar_ty.intInfo(zcu);2214 const operand_info = operand_scalar_ty.intInfo(zcu);
2219 const dest_info = dest_scalar_ty.intInfo(zcu);2215 const dest_info = dest_scalar_ty.intInfo(zcu);
...@@ -2229,6 +2225,10 @@ fn safeIntcastBlockPayload(l: *Legalize, orig_inst: Air.Inst.Index) Error!Air.In...@@ -2229,6 +2225,10 @@ fn safeIntcastBlockPayload(l: *Legalize, orig_inst: Air.Inst.Index) Error!Air.In
2229 };2225 };
2230 };2226 };
22312227
2228 if (!have_enum_value_check and !have_min_check and !have_max_check) {
2229 return null;
2230 }
2231
2232 // The worst-case scenario in terms of total instructions and total condbrs is the case where2232 // The worst-case scenario in terms of total instructions and total condbrs is the case where
2233 // the result type is an exhaustive enum whose tag type is smaller than the operand type:2233 // the result type is an exhaustive enum whose tag type is smaller than the operand type:
2234 //2234 //
...@@ -2324,7 +2324,7 @@ fn safeIntcastBlockPayload(l: *Legalize, orig_inst: Air.Inst.Index) Error!Air.In...@@ -2324,7 +2324,7 @@ fn safeIntcastBlockPayload(l: *Legalize, orig_inst: Air.Inst.Index) Error!Air.In
2324 } },2324 } },
2325 });2325 });
2326 // For ints we're already done, but for exhaustive enums we must check this is a valid tag.2326 // For ints we're already done, but for exhaustive enums we must check this is a valid tag.
2327 if (dest_is_enum and !dest_ty.isNonexhaustiveEnum(zcu) and zcu.backendSupportsFeature(.is_named_enum_value)) {2327 if (have_enum_value_check) {
2328 assert(!is_vector); // vectors of enums don't exist2328 assert(!is_vector); // vectors of enums don't exist
2329 // We are building this:2329 // We are building this:
2330 // %1 = is_named_enum_value(%cast_inst)2330 // %1 = is_named_enum_value(%cast_inst)
...@@ -2346,16 +2346,12 @@ fn safeIntcastBlockPayload(l: *Legalize, orig_inst: Air.Inst.Index) Error!Air.In...@@ -2346,16 +2346,12 @@ fn safeIntcastBlockPayload(l: *Legalize, orig_inst: Air.Inst.Index) Error!Air.In
2346 cur_block = &condbr.then_block;2346 cur_block = &condbr.then_block;
2347 }2347 }
2348 // Finally, just `br` to our outer `block`.2348 // Finally, just `br` to our outer `block`.
2349 _ = cur_block.add(l, .{2349 cur_block.addBr(l, orig_inst, cast_inst.toRef());
2350 .tag = .br,2350
2351 .data = .{ .br = .{
2352 .block_inst = orig_inst,
2353 .operand = cast_inst.toRef(),
2354 } },
2355 });
2356 // We might not have used all of the instructions; that's intentional.2351 // We might not have used all of the instructions; that's intentional.
2357 _ = cur_block.stealRemainingCapacity();2352 _ = cur_block.stealRemainingCapacity();
23582353
2354 assert(condbr_idx != 0); // should have already returned `null`
2359 for (condbr_buf[0..condbr_idx]) |*condbr| try condbr.finish(l);2355 for (condbr_buf[0..condbr_idx]) |*condbr| try condbr.finish(l);
2360 return .{ .ty_pl = .{2356 return .{ .ty_pl = .{
2361 .ty = Air.internedToRef(dest_ty.toIntern()),2357 .ty = Air.internedToRef(dest_ty.toIntern()),