Commit 5467582444

Andrew Kelley <andrew@ziglang.org>
2021-09-29 03:55:43
saturating arithmetic modifications
* Remove the builtins `@addWithSaturation`, `@subWithSaturation`, `@mulWithSaturation`, and `@shlWithSaturation` now that we have first-class syntax for saturating arithmetic. * langref: Clarify the behavior of `@shlExact`. * Ast: rename `bit_shift_left` to `shl` and `bit_shift_right` to `shr` for consistency. * Air: rename to include underscore separator with consistency with the rest of the ops. * Air: add shl_exact instruction * Use non-extended tags for saturating arithmetic, to keep it simple so that all the arithmetic operations can be done the same way. - Sema: unify analyzeArithmetic with analyzeSatArithmetic - implement comptime `+|`, `-|`, and `*|` - allow float operands to saturating arithmetic * `<<|` allows any integer type for the RHS. * C backend: fix rebase conflicts * LLVM backend: reduce the amount of branching for arithmetic ops * zig.h: fix magic number not matching actual size of C integer types
1 parent 71da169
doc/langref.html.in
@@ -1407,7 +1407,6 @@ a +|= b{#endsyntax#}</pre></th>
           <td>Saturating Addition.
             <ul>
               <li>Invokes {#link|Peer Type Resolution#} for the operands.</li>
-              <li>See also {#link|@addWithSaturation#}.</li>
             </ul>
           </td>
           <td>
@@ -1464,7 +1463,6 @@ a -|= b{#endsyntax#}</pre></th>
           <td>Saturating Subtraction.
             <ul>
               <li>Invokes {#link|Peer Type Resolution#} for the operands.</li>
-              <li>See also {#link|@subWithSaturation#}.</li>
             </ul>
           </td>
           <td>
@@ -1556,7 +1554,6 @@ a *|= b{#endsyntax#}</pre></th>
           <td>Saturating Multiplication.
             <ul>
               <li>Invokes {#link|Peer Type Resolution#} for the operands.</li>
-              <li>See also {#link|@mulWithSaturation#}.</li>
             </ul>
           </td>
           <td>
@@ -7235,15 +7232,6 @@ fn readFile(allocator: *Allocator, filename: []const u8) ![]u8 {
                   If no overflow or underflow occurs, returns {#syntax#}false{#endsyntax#}.
       </p>
       {#header_close#}
-      {#header_open|@addWithSaturation#}
-      <pre>{#syntax#}@addWithSaturation(a: T, b: T) T{#endsyntax#}</pre>
-      <p>
-      Returns {#syntax#}a + b{#endsyntax#}. The result will be clamped between the type maximum and minimum.
-      </p>
-      <p>
-      The syntax {#syntax#}a +| b{#endsyntax#} is equivalent to calling {#syntax#}@addWithSaturation(a, b){#endsyntax#}.
-      </p>
-      {#header_close#}
       {#header_open|@alignCast#}
       <pre>{#syntax#}@alignCast(comptime alignment: u29, ptr: anytype) anytype{#endsyntax#}</pre>
       <p>
@@ -8365,21 +8353,6 @@ test "@wasmMemoryGrow" {
       </p>
       {#header_close#}
       
-      {#header_open|@mulWithSaturation#}
-      <pre>{#syntax#}@mulWithSaturation(a: T, b: T) T{#endsyntax#}</pre>
-      <p>
-      Returns {#syntax#}a * b{#endsyntax#}. The result will be clamped between the type maximum and minimum.
-      </p>
-      <p>
-      The syntax {#syntax#}a *| b{#endsyntax#} is equivalent to calling {#syntax#}@mulWithSaturation(a, b){#endsyntax#}.
-      </p>
-      <p>
-      NOTE: Currently there is a bug in the llvm.smul.fix.sat intrinsic which affects {#syntax#}@mulWithSaturation{#endsyntax#} of signed integers.  
-      This may result in an incorrect sign bit when there is overflow.  This will be fixed in zig's 0.9.0 release.  
-      Check <a href="https://github.com/ziglang/zig/issues/9643">this issue</a> for more information.
-      </p>
-      {#header_close#}
-
       {#header_open|@panic#}
       <pre>{#syntax#}@panic(message: []const u8) noreturn{#endsyntax#}</pre>
       <p>
@@ -8597,14 +8570,16 @@ test "@setRuntimeSafety" {
       {#header_open|@shlExact#}
       <pre>{#syntax#}@shlExact(value: T, shift_amt: Log2T) T{#endsyntax#}</pre>
       <p>
-      Performs the left shift operation ({#syntax#}<<{#endsyntax#}). Caller guarantees
-      that the shift will not shift any 1 bits out.
+      Performs the left shift operation ({#syntax#}<<{#endsyntax#}).
+      For unsigned integers, the result is {#link|undefined#} if any 1 bits
+      are shifted out. For signed integers, the result is {#link|undefined#} if
+      any bits that disagree with the resultant sign bit are shifted out.
       </p>
       <p>
       The type of {#syntax#}shift_amt{#endsyntax#} is an unsigned integer with {#syntax#}log2(T.bit_count){#endsyntax#} bits.
               This is because {#syntax#}shift_amt >= T.bit_count{#endsyntax#} is undefined behavior.
       </p>
-      {#see_also|@shrExact|@shlWithOverflow|@shlWithSaturation#}
+      {#see_also|@shrExact|@shlWithOverflow#}
       {#header_close#}
 
       {#header_open|@shlWithOverflow#}
@@ -8618,23 +8593,9 @@ test "@setRuntimeSafety" {
       The type of {#syntax#}shift_amt{#endsyntax#} is an unsigned integer with {#syntax#}log2(T.bit_count){#endsyntax#} bits.
               This is because {#syntax#}shift_amt >= T.bit_count{#endsyntax#} is undefined behavior.
       </p>
-      {#see_also|@shlExact|@shrExact|@shlWithSaturation#}
+      {#see_also|@shlExact|@shrExact#}
       {#header_close#}
       
-      {#header_open|@shlWithSaturation#}
-      <pre>{#syntax#}@shlWithSaturation(a: T, shift_amt: T) T{#endsyntax#}</pre>
-      <p>
-      Returns {#syntax#}a << b{#endsyntax#}. The result will be clamped between type minimum and maximum.  
-      </p>
-      <p>
-      The syntax {#syntax#}a <<| b{#endsyntax#} is equivalent to calling {#syntax#}@shlWithSaturation(a, b){#endsyntax#}.
-      </p>
-      <p>
-      Unlike other @shl builtins, shift_amt doesn't need to be a Log2T as saturated overshifting is well defined.  
-      </p>
-      {#see_also|@shlExact|@shrExact|@shlWithOverflow#}
-      {#header_close#}
-
       {#header_open|@shrExact#}
       <pre>{#syntax#}@shrExact(value: T, shift_amt: Log2T) T{#endsyntax#}</pre>
       <p>
@@ -8645,7 +8606,7 @@ test "@setRuntimeSafety" {
       The type of {#syntax#}shift_amt{#endsyntax#} is an unsigned integer with {#syntax#}log2(T.bit_count){#endsyntax#} bits.
               This is because {#syntax#}shift_amt >= T.bit_count{#endsyntax#} is undefined behavior.
       </p>
-      {#see_also|@shlExact|@shlWithOverflow|@shlWithSaturation#}
+      {#see_also|@shlExact|@shlWithOverflow#}
       {#header_close#}
 
       {#header_open|@shuffle#}
@@ -8945,16 +8906,6 @@ fn doTheTest() !void {
       </p>
       {#header_close#}
       
-      {#header_open|@subWithSaturation#}
-      <pre>{#syntax#}@subWithSaturation(a: T, b: T) T{#endsyntax#}</pre>
-      <p>
-      Returns {#syntax#}a - b{#endsyntax#}. The result will be clamped between the type maximum and minimum.  
-      </p>
-      <p>
-      The syntax {#syntax#}a -| b{#endsyntax#} is equivalent to calling {#syntax#}@subWithSaturation(a, b){#endsyntax#}.
-      </p>
-      {#header_close#}
-
       {#header_open|@tagName#}
       <pre>{#syntax#}@tagName(value: anytype) [:0]const u8{#endsyntax#}</pre>
       <p>
lib/std/zig/Ast.zig
@@ -395,9 +395,9 @@ pub fn firstToken(tree: Tree, node: Node.Index) TokenIndex {
         .assign_mod,
         .assign_add,
         .assign_sub,
-        .assign_bit_shift_left,
-        .assign_bit_shift_left_sat,
-        .assign_bit_shift_right,
+        .assign_shl,
+        .assign_shl_sat,
+        .assign_shr,
         .assign_bit_and,
         .assign_bit_xor,
         .assign_bit_or,
@@ -422,9 +422,9 @@ pub fn firstToken(tree: Tree, node: Node.Index) TokenIndex {
         .sub_wrap,
         .add_sat,
         .sub_sat,
-        .bit_shift_left,
-        .bit_shift_left_sat,
-        .bit_shift_right,
+        .shl,
+        .shl_sat,
+        .shr,
         .bit_and,
         .bit_xor,
         .bit_or,
@@ -659,9 +659,9 @@ pub fn lastToken(tree: Tree, node: Node.Index) TokenIndex {
         .assign_mod,
         .assign_add,
         .assign_sub,
-        .assign_bit_shift_left,
-        .assign_bit_shift_left_sat,
-        .assign_bit_shift_right,
+        .assign_shl,
+        .assign_shl_sat,
+        .assign_shr,
         .assign_bit_and,
         .assign_bit_xor,
         .assign_bit_or,
@@ -686,9 +686,9 @@ pub fn lastToken(tree: Tree, node: Node.Index) TokenIndex {
         .sub_wrap,
         .add_sat,
         .sub_sat,
-        .bit_shift_left,
-        .bit_shift_left_sat,
-        .bit_shift_right,
+        .shl,
+        .shl_sat,
+        .shr,
         .bit_and,
         .bit_xor,
         .bit_or,
@@ -2540,11 +2540,11 @@ pub const Node = struct {
         /// `lhs -= rhs`. main_token is op.
         assign_sub,
         /// `lhs <<= rhs`. main_token is op.
-        assign_bit_shift_left,
+        assign_shl,
         /// `lhs <<|= rhs`. main_token is op.
-        assign_bit_shift_left_sat,
+        assign_shl_sat,
         /// `lhs >>= rhs`. main_token is op.
-        assign_bit_shift_right,
+        assign_shr,
         /// `lhs &= rhs`. main_token is op.
         assign_bit_and,
         /// `lhs ^= rhs`. main_token is op.
@@ -2594,11 +2594,11 @@ pub const Node = struct {
         /// `lhs -| rhs`. main_token is the `-|`.
         sub_sat,
         /// `lhs << rhs`. main_token is the `<<`.
-        bit_shift_left,
+        shl,
         /// `lhs <<| rhs`. main_token is the `<<|`.
-        bit_shift_left_sat,
+        shl_sat,
         /// `lhs >> rhs`. main_token is the `>>`.
-        bit_shift_right,
+        shr,
         /// `lhs & rhs`. main_token is the `&`.
         bit_and,
         /// `lhs ^ rhs`. main_token is the `^`.
lib/std/zig/parse.zig
@@ -1268,9 +1268,9 @@ const Parser = struct {
             .percent_equal => .assign_mod,
             .plus_equal => .assign_add,
             .minus_equal => .assign_sub,
-            .angle_bracket_angle_bracket_left_equal => .assign_bit_shift_left,
-            .angle_bracket_angle_bracket_left_pipe_equal => .assign_bit_shift_left_sat,
-            .angle_bracket_angle_bracket_right_equal => .assign_bit_shift_right,
+            .angle_bracket_angle_bracket_left_equal => .assign_shl,
+            .angle_bracket_angle_bracket_left_pipe_equal => .assign_shl_sat,
+            .angle_bracket_angle_bracket_right_equal => .assign_shr,
             .ampersand_equal => .assign_bit_and,
             .caret_equal => .assign_bit_xor,
             .pipe_equal => .assign_bit_or,
@@ -1346,9 +1346,9 @@ const Parser = struct {
         .keyword_orelse = .{ .prec = 40, .tag = .@"orelse" },
         .keyword_catch = .{ .prec = 40, .tag = .@"catch" },
 
-        .angle_bracket_angle_bracket_left = .{ .prec = 50, .tag = .bit_shift_left },
-        .angle_bracket_angle_bracket_left_pipe = .{ .prec = 50, .tag = .bit_shift_left_sat },
-        .angle_bracket_angle_bracket_right = .{ .prec = 50, .tag = .bit_shift_right },
+        .angle_bracket_angle_bracket_left = .{ .prec = 50, .tag = .shl },
+        .angle_bracket_angle_bracket_left_pipe = .{ .prec = 50, .tag = .shl_sat },
+        .angle_bracket_angle_bracket_right = .{ .prec = 50, .tag = .shr },
 
         .plus = .{ .prec = 60, .tag = .add },
         .minus = .{ .prec = 60, .tag = .sub },
lib/std/zig/render.zig
@@ -339,9 +339,9 @@ fn renderExpression(gpa: *Allocator, ais: *Ais, tree: Ast, node: Ast.Node.Index,
         .assign,
         .assign_bit_and,
         .assign_bit_or,
-        .assign_bit_shift_left,
-        .assign_bit_shift_left_sat,
-        .assign_bit_shift_right,
+        .assign_shl,
+        .assign_shl_sat,
+        .assign_shr,
         .assign_bit_xor,
         .assign_div,
         .assign_sub,
@@ -357,9 +357,9 @@ fn renderExpression(gpa: *Allocator, ais: *Ais, tree: Ast, node: Ast.Node.Index,
         .bang_equal,
         .bit_and,
         .bit_or,
-        .bit_shift_left,
-        .bit_shift_left_sat,
-        .bit_shift_right,
+        .shl,
+        .shl_sat,
+        .shr,
         .bit_xor,
         .bool_and,
         .bool_or,
@@ -2528,8 +2528,8 @@ fn nodeCausesSliceOpSpace(tag: Ast.Node.Tag) bool {
         .assign,
         .assign_bit_and,
         .assign_bit_or,
-        .assign_bit_shift_left,
-        .assign_bit_shift_right,
+        .assign_shl,
+        .assign_shr,
         .assign_bit_xor,
         .assign_div,
         .assign_sub,
@@ -2542,8 +2542,8 @@ fn nodeCausesSliceOpSpace(tag: Ast.Node.Tag) bool {
         .bang_equal,
         .bit_and,
         .bit_or,
-        .bit_shift_left,
-        .bit_shift_right,
+        .shl,
+        .shr,
         .bit_xor,
         .bool_and,
         .bool_or,
src/codegen/llvm/bindings.zig
@@ -469,6 +469,12 @@ pub const Builder = opaque {
     pub const buildShl = LLVMBuildShl;
     extern fn LLVMBuildShl(*const Builder, LHS: *const Value, RHS: *const Value, Name: [*:0]const u8) *const Value;
 
+    pub const buildNUWShl = ZigLLVMBuildNUWShl;
+    extern fn ZigLLVMBuildNUWShl(*const Builder, LHS: *const Value, RHS: *const Value, Name: [*:0]const u8) *const Value;
+
+    pub const buildNSWShl = ZigLLVMBuildNSWShl;
+    extern fn ZigLLVMBuildNSWShl(*const Builder, LHS: *const Value, RHS: *const Value, Name: [*:0]const u8) *const Value;
+
     pub const buildSShlSat = ZigLLVMBuildSShlSat;
     extern fn ZigLLVMBuildSShlSat(*const Builder, LHS: *const Value, RHS: *const Value, Name: [*:0]const u8) *const Value;
 
src/codegen/c.zig
@@ -883,25 +883,27 @@ fn genBody(f: *Function, body: []const Air.Inst.Index) error{ AnalysisFail, OutO
 
             // TODO use a different strategy for add that communicates to the optimizer
             // that wrapping is UB.
-            .add, .ptr_add => try airBinOp( f, inst, " + "),
-            .addwrap       => try airWrapOp(f, inst, " + ", "addw_"),
-            .addsat        => return f.fail("TODO: C backend: implement codegen for addsat", .{}),
+            .add, .ptr_add => try airBinOp (f, inst, " + "),
             // TODO use a different strategy for sub that communicates to the optimizer
             // that wrapping is UB.
-            .sub, .ptr_sub => try airBinOp( f, inst, " - "),
-            .subwrap       => try airWrapOp(f, inst, " - ", "subw_"),
-            .subsat        => return f.fail("TODO: C backend: implement codegen for subsat", .{}),
+            .sub, .ptr_sub => try airBinOp (f, inst, " - "),
             // TODO use a different strategy for mul that communicates to the optimizer
             // that wrapping is UB.
-            .mul           => try airBinOp( f, inst, " * "),
-            .mulwrap       => try airWrapOp(f, inst, " * ", "mulw_"),
-            .mulsat        => return f.fail("TODO: C backend: implement codegen for mulsat", .{}),
+            .mul           => try airBinOp (f, inst, " * "),
             // TODO use a different strategy for div that communicates to the optimizer
             // that wrapping is UB.
             .div           => try airBinOp( f, inst, " / "),
             .rem           => try airBinOp( f, inst, " % "),
-            // TODO implement modulus division
-            .mod           => try airBinOp( f, inst, " mod "),
+            .mod           => try airBinOp( f, inst, " mod "), // TODO implement modulus division
+
+            .addwrap => try airWrapOp(f, inst, " + ", "addw_"),
+            .subwrap => try airWrapOp(f, inst, " - ", "subw_"),
+            .mulwrap => try airWrapOp(f, inst, " * ", "mulw_"),
+
+            .add_sat => try airSatOp(f, inst, "adds_"),
+            .sub_sat => try airSatOp(f, inst, "subs_"),
+            .mul_sat => try airSatOp(f, inst, "muls_"),
+            .shl_sat => try airSatOp(f, inst, "shls_"),
 
             .cmp_eq  => try airBinOp(f, inst, " == "),
             .cmp_gt  => try airBinOp(f, inst, " > "),
@@ -911,18 +913,14 @@ fn genBody(f: *Function, body: []const Air.Inst.Index) error{ AnalysisFail, OutO
             .cmp_neq => try airBinOp(f, inst, " != "),
 
             // bool_and and bool_or are non-short-circuit operations
-            .bool_and   => try airBinOp(f, inst, " & "),
-            .bool_or    => try airBinOp(f, inst, " | "),
-            .bit_and    => try airBinOp(f, inst, " & "),
-            .bit_or     => try airBinOp(f, inst, " | "),
-            .xor        => try airBinOp(f, inst, " ^ "),
-
-            .shr        => try airBinOp(f, inst, " >> "),
-            .shl        => try airBinOp(f, inst, " << "),
-            .shl_sat    => return f.fail("TODO: C backend: implement codegen for mulsat", .{}),
-            
-
-            .not        => try airNot(  f, inst),
+            .bool_and        => try airBinOp(f, inst, " & "),
+            .bool_or         => try airBinOp(f, inst, " | "),
+            .bit_and         => try airBinOp(f, inst, " & "),
+            .bit_or          => try airBinOp(f, inst, " | "),
+            .xor             => try airBinOp(f, inst, " ^ "),
+            .shr             => try airBinOp(f, inst, " >> "),
+            .shl, .shl_exact => try airBinOp(f, inst, " << "),
+            .not             => try airNot  (f, inst),
 
             .optional_payload     => try airOptionalPayload(f, inst),
             .optional_payload_ptr => try airOptionalPayload(f, inst),
@@ -1314,27 +1312,23 @@ fn airWrapOp(
     return ret;
 }
 
-fn airSatOp(
-    o: *Object,
-    inst: Air.Inst.Index,
-    fn_op: [*:0]const u8,
-) !CValue {
-    if (o.liveness.isUnused(inst))
+fn airSatOp(f: *Function, inst: Air.Inst.Index, fn_op: [*:0]const u8) !CValue {
+    if (f.liveness.isUnused(inst))
         return CValue.none;
 
-    const bin_op = o.air.instructions.items(.data)[inst].bin_op;
-    const inst_ty = o.air.typeOfIndex(inst);
-    const int_info = inst_ty.intInfo(o.dg.module.getTarget());
+    const bin_op = f.air.instructions.items(.data)[inst].bin_op;
+    const inst_ty = f.air.typeOfIndex(inst);
+    const int_info = inst_ty.intInfo(f.object.dg.module.getTarget());
     const bits = int_info.bits;
 
     switch (bits) {
         8, 16, 32, 64, 128 => {},
-        else => return o.dg.fail("TODO: C backend: airSatOp for non power of 2 integers", .{}),
+        else => return f.object.dg.fail("TODO: C backend: airSatOp for non power of 2 integers", .{}),
     }
 
     // if it's an unsigned int with non-arbitrary bit size then we can just add
     if (bits > 64) {
-        return o.dg.fail("TODO: C backend: airSatOp for large integers", .{});
+        return f.object.dg.fail("TODO: C backend: airSatOp for large integers", .{});
     }
 
     var min_buf: [80]u8 = undefined;
@@ -1382,11 +1376,11 @@ fn airSatOp(
         },
     };
 
-    const lhs = try o.resolveInst(bin_op.lhs);
-    const rhs = try o.resolveInst(bin_op.rhs);
-    const w = o.writer();
+    const lhs = try f.resolveInst(bin_op.lhs);
+    const rhs = try f.resolveInst(bin_op.rhs);
+    const w = f.object.writer();
 
-    const ret = try o.allocLocal(inst_ty, .Mut);
+    const ret = try f.allocLocal(inst_ty, .Mut);
     try w.print(" = zig_{s}", .{fn_op});
 
     switch (inst_ty.tag()) {
@@ -1412,16 +1406,16 @@ fn airSatOp(
     }
 
     try w.writeByte('(');
-    try o.writeCValue(w, lhs);
+    try f.writeCValue(w, lhs);
     try w.writeAll(", ");
-    try o.writeCValue(w, rhs);
+    try f.writeCValue(w, rhs);
 
     if (int_info.signedness == .signed) {
         try w.print(", {s}", .{min});
     }
 
     try w.print(", {s});", .{max});
-    try o.indent_writer.insertNewline();
+    try f.object.indent_writer.insertNewline();
 
     return ret;
 }
src/codegen/llvm.zig
@@ -1236,27 +1236,27 @@ pub const FuncGen = struct {
         for (body) |inst| {
             const opt_value: ?*const llvm.Value = switch (air_tags[inst]) {
                 // zig fmt: off
-                .add     => try self.airAdd(inst, .standard),
-                .addwrap => try self.airAdd(inst, .wrapping),
-                .addsat  => try self.airAdd(inst, .saturated),
-                .sub     => try self.airSub(inst, .standard),
-                .subwrap => try self.airSub(inst, .wrapping),
-                .subsat  => try self.airSub(inst, .saturated),
-                .mul     => try self.airMul(inst, .standard),
-                .mulwrap => try self.airMul(inst, .wrapping),
-                .mulsat  => try self.airMul(inst, .saturated),
-                .div     => try self.airDiv(inst),
-                .rem     => try self.airRem(inst),
-                .mod     => try self.airMod(inst),
-                .ptr_add => try self.airPtrAdd(inst),
-                .ptr_sub => try self.airPtrSub(inst),
+                .add       => try self.airAdd(inst),
+                .addwrap   => try self.airAddWrap(inst),
+                .add_sat   => try self.airAddSat(inst),
+                .sub       => try self.airSub(inst),
+                .subwrap   => try self.airSubWrap(inst),
+                .sub_sat   => try self.airSubSat(inst),
+                .mul       => try self.airMul(inst),
+                .mulwrap   => try self.airMulWrap(inst),
+                .mul_sat   => try self.airMulSat(inst),
+                .div       => try self.airDiv(inst),
+                .rem       => try self.airRem(inst),
+                .mod       => try self.airMod(inst),
+                .ptr_add   => try self.airPtrAdd(inst),
+                .ptr_sub   => try self.airPtrSub(inst),
+                .shl       => try self.airShl(inst),
+                .shl_sat   => try self.airShlSat(inst),
+                .shl_exact => try self.airShlExact(inst),
 
                 .bit_and, .bool_and => try self.airAnd(inst),
                 .bit_or, .bool_or   => try self.airOr(inst),
                 .xor                => try self.airXor(inst),
-
-                .shl                => try self.airShl(inst, false),
-                .shl_sat            => try self.airShl(inst, true),
                 .shr                => try self.airShr(inst),
 
                 .cmp_eq  => try self.airCmp(inst, .eq),
@@ -2028,10 +2028,8 @@ pub const FuncGen = struct {
         return self.todo("implement llvm codegen for 'airWrapErrUnionErr'", .{});
     }
 
-    const ArithmeticType = enum { standard, wrapping, saturated };
-    fn airAdd(self: *FuncGen, inst: Air.Inst.Index, ty: ArithmeticType) !?*const llvm.Value {
-        if (self.liveness.isUnused(inst))
-            return null;
+    fn airAdd(self: *FuncGen, inst: Air.Inst.Index) !?*const llvm.Value {
+        if (self.liveness.isUnused(inst)) return null;
 
         const bin_op = self.air.instructions.items(.data)[inst].bin_op;
         const lhs = try self.resolveInst(bin_op.lhs);
@@ -2039,21 +2037,36 @@ pub const FuncGen = struct {
         const inst_ty = self.air.typeOfIndex(inst);
 
         if (inst_ty.isAnyFloat()) return self.builder.buildFAdd(lhs, rhs, "");
-        if (ty == .wrapping)
-            return self.builder.buildAdd(lhs, rhs, "")
-        else if (ty == .saturated) {
-            if (inst_ty.isSignedInt())
-                return self.builder.buildSAddSat(lhs, rhs, "")
-            else
-                return self.builder.buildUAddSat(lhs, rhs, "");
-        }
         if (inst_ty.isSignedInt()) return self.builder.buildNSWAdd(lhs, rhs, "");
         return self.builder.buildNUWAdd(lhs, rhs, "");
     }
 
-    fn airSub(self: *FuncGen, inst: Air.Inst.Index, ty: ArithmeticType) !?*const llvm.Value {
-        if (self.liveness.isUnused(inst))
-            return null;
+    fn airAddWrap(self: *FuncGen, inst: Air.Inst.Index) !?*const llvm.Value {
+        if (self.liveness.isUnused(inst)) return null;
+
+        const bin_op = self.air.instructions.items(.data)[inst].bin_op;
+        const lhs = try self.resolveInst(bin_op.lhs);
+        const rhs = try self.resolveInst(bin_op.rhs);
+
+        return self.builder.buildAdd(lhs, rhs, "");
+    }
+
+    fn airAddSat(self: *FuncGen, inst: Air.Inst.Index) !?*const llvm.Value {
+        if (self.liveness.isUnused(inst)) return null;
+
+        const bin_op = self.air.instructions.items(.data)[inst].bin_op;
+        const lhs = try self.resolveInst(bin_op.lhs);
+        const rhs = try self.resolveInst(bin_op.rhs);
+        const inst_ty = self.air.typeOfIndex(inst);
+
+        if (inst_ty.isAnyFloat()) return self.todo("saturating float add", .{});
+        if (inst_ty.isSignedInt()) return self.builder.buildSAddSat(lhs, rhs, "");
+
+        return self.builder.buildUAddSat(lhs, rhs, "");
+    }
+
+    fn airSub(self: *FuncGen, inst: Air.Inst.Index) !?*const llvm.Value {
+        if (self.liveness.isUnused(inst)) return null;
 
         const bin_op = self.air.instructions.items(.data)[inst].bin_op;
         const lhs = try self.resolveInst(bin_op.lhs);
@@ -2061,21 +2074,35 @@ pub const FuncGen = struct {
         const inst_ty = self.air.typeOfIndex(inst);
 
         if (inst_ty.isAnyFloat()) return self.builder.buildFSub(lhs, rhs, "");
-        if (ty == .wrapping)
-            return self.builder.buildSub(lhs, rhs, "")
-        else if (ty == .saturated) {
-            if (inst_ty.isSignedInt())
-                return self.builder.buildSSubSat(lhs, rhs, "")
-            else
-                return self.builder.buildUSubSat(lhs, rhs, "");
-        }
         if (inst_ty.isSignedInt()) return self.builder.buildNSWSub(lhs, rhs, "");
         return self.builder.buildNUWSub(lhs, rhs, "");
     }
 
-    fn airMul(self: *FuncGen, inst: Air.Inst.Index, ty: ArithmeticType) !?*const llvm.Value {
-        if (self.liveness.isUnused(inst))
-            return null;
+    fn airSubWrap(self: *FuncGen, inst: Air.Inst.Index) !?*const llvm.Value {
+        if (self.liveness.isUnused(inst)) return null;
+
+        const bin_op = self.air.instructions.items(.data)[inst].bin_op;
+        const lhs = try self.resolveInst(bin_op.lhs);
+        const rhs = try self.resolveInst(bin_op.rhs);
+
+        return self.builder.buildSub(lhs, rhs, "");
+    }
+
+    fn airSubSat(self: *FuncGen, inst: Air.Inst.Index) !?*const llvm.Value {
+        if (self.liveness.isUnused(inst)) return null;
+
+        const bin_op = self.air.instructions.items(.data)[inst].bin_op;
+        const lhs = try self.resolveInst(bin_op.lhs);
+        const rhs = try self.resolveInst(bin_op.rhs);
+        const inst_ty = self.air.typeOfIndex(inst);
+
+        if (inst_ty.isAnyFloat()) return self.todo("saturating float sub", .{});
+        if (inst_ty.isSignedInt()) return self.builder.buildSSubSat(lhs, rhs, "");
+        return self.builder.buildUSubSat(lhs, rhs, "");
+    }
+
+    fn airMul(self: *FuncGen, inst: Air.Inst.Index) !?*const llvm.Value {
+        if (self.liveness.isUnused(inst)) return null;
 
         const bin_op = self.air.instructions.items(.data)[inst].bin_op;
         const lhs = try self.resolveInst(bin_op.lhs);
@@ -2083,18 +2110,33 @@ pub const FuncGen = struct {
         const inst_ty = self.air.typeOfIndex(inst);
 
         if (inst_ty.isAnyFloat()) return self.builder.buildFMul(lhs, rhs, "");
-        if (ty == .wrapping)
-            return self.builder.buildMul(lhs, rhs, "")
-        else if (ty == .saturated) {
-            if (inst_ty.isSignedInt())
-                return self.builder.buildSMulFixSat(lhs, rhs, "")
-            else
-                return self.builder.buildUMulFixSat(lhs, rhs, "");
-        }
         if (inst_ty.isSignedInt()) return self.builder.buildNSWMul(lhs, rhs, "");
         return self.builder.buildNUWMul(lhs, rhs, "");
     }
 
+    fn airMulWrap(self: *FuncGen, inst: Air.Inst.Index) !?*const llvm.Value {
+        if (self.liveness.isUnused(inst)) return null;
+
+        const bin_op = self.air.instructions.items(.data)[inst].bin_op;
+        const lhs = try self.resolveInst(bin_op.lhs);
+        const rhs = try self.resolveInst(bin_op.rhs);
+
+        return self.builder.buildMul(lhs, rhs, "");
+    }
+
+    fn airMulSat(self: *FuncGen, inst: Air.Inst.Index) !?*const llvm.Value {
+        if (self.liveness.isUnused(inst)) return null;
+
+        const bin_op = self.air.instructions.items(.data)[inst].bin_op;
+        const lhs = try self.resolveInst(bin_op.lhs);
+        const rhs = try self.resolveInst(bin_op.rhs);
+        const inst_ty = self.air.typeOfIndex(inst);
+
+        if (inst_ty.isAnyFloat()) return self.todo("saturating float mul", .{});
+        if (inst_ty.isSignedInt()) return self.builder.buildSMulFixSat(lhs, rhs, "");
+        return self.builder.buildUMulFixSat(lhs, rhs, "");
+    }
+
     fn airDiv(self: *FuncGen, inst: Air.Inst.Index) !?*const llvm.Value {
         if (self.liveness.isUnused(inst))
             return null;
@@ -2200,9 +2242,25 @@ pub const FuncGen = struct {
         return self.builder.buildXor(lhs, rhs, "");
     }
 
-    fn airShl(self: *FuncGen, inst: Air.Inst.Index, sat: bool) !?*const llvm.Value {
-        if (self.liveness.isUnused(inst))
-            return null;
+    fn airShlExact(self: *FuncGen, inst: Air.Inst.Index) !?*const llvm.Value {
+        if (self.liveness.isUnused(inst)) return null;
+
+        const bin_op = self.air.instructions.items(.data)[inst].bin_op;
+        const lhs = try self.resolveInst(bin_op.lhs);
+        const rhs = try self.resolveInst(bin_op.rhs);
+        const lhs_type = self.air.typeOf(bin_op.lhs);
+        const tg = self.dg.module.getTarget();
+        const casted_rhs = if (self.air.typeOf(bin_op.rhs).bitSize(tg) < lhs_type.bitSize(tg))
+            self.builder.buildZExt(rhs, try self.dg.llvmType(lhs_type), "")
+        else
+            rhs;
+        if (lhs_type.isSignedInt()) return self.builder.buildNSWShl(lhs, casted_rhs, "");
+        return self.builder.buildNUWShl(lhs, casted_rhs, "");
+    }
+
+    fn airShl(self: *FuncGen, inst: Air.Inst.Index) !?*const llvm.Value {
+        if (self.liveness.isUnused(inst)) return null;
+
         const bin_op = self.air.instructions.items(.data)[inst].bin_op;
         const lhs = try self.resolveInst(bin_op.lhs);
         const rhs = try self.resolveInst(bin_op.rhs);
@@ -2212,15 +2270,25 @@ pub const FuncGen = struct {
             self.builder.buildZExt(rhs, try self.dg.llvmType(lhs_type), "")
         else
             rhs;
-        if (sat) {
-            return if (lhs_type.isSignedInt())
-                self.builder.buildSShlSat(lhs, casted_rhs, "")
-            else
-                self.builder.buildUShlSat(lhs, casted_rhs, "");
-        }
         return self.builder.buildShl(lhs, casted_rhs, "");
     }
 
+    fn airShlSat(self: *FuncGen, inst: Air.Inst.Index) !?*const llvm.Value {
+        if (self.liveness.isUnused(inst)) return null;
+
+        const bin_op = self.air.instructions.items(.data)[inst].bin_op;
+        const lhs = try self.resolveInst(bin_op.lhs);
+        const rhs = try self.resolveInst(bin_op.rhs);
+        const lhs_type = self.air.typeOf(bin_op.lhs);
+        const tg = self.dg.module.getTarget();
+        const casted_rhs = if (self.air.typeOf(bin_op.rhs).bitSize(tg) < lhs_type.bitSize(tg))
+            self.builder.buildZExt(rhs, try self.dg.llvmType(lhs_type), "")
+        else
+            rhs;
+        if (lhs_type.isSignedInt()) return self.builder.buildSShlSat(lhs, casted_rhs, "");
+        return self.builder.buildUShlSat(lhs, casted_rhs, "");
+    }
+
     fn airShr(self: *FuncGen, inst: Air.Inst.Index) !?*const llvm.Value {
         if (self.liveness.isUnused(inst))
             return null;
src/link/C/zig.h
@@ -356,9 +356,6 @@ static inline long long zig_subw_longlong(long long lhs, long long rhs, long lon
     return (long long)(((unsigned long long)lhs) - ((unsigned long long)rhs));
 }
 
-/*
- * Saturating aritmetic operations: add, sub, mul, shl
- */
 #define zig_add_sat_u(ZT, T) static inline T zig_adds_##ZT(T x, T y, T max) { \
     return (x > max - y) ? max : x + y; \
 }
@@ -449,7 +446,7 @@ zig_shl_sat_u(u32,   uint32_t, 32)
 zig_shl_sat_s(i32,    int32_t, 31)
 zig_shl_sat_u(u64,   uint64_t, 64)
 zig_shl_sat_s(i64,    int64_t, 63)
-zig_shl_sat_s(isize, intptr_t, 63)
-zig_shl_sat_s(short,    short, 15)
-zig_shl_sat_s(int,        int, 31)
-zig_shl_sat_s(long,      long, 63)
+zig_shl_sat_s(isize, intptr_t, ((sizeof(intptr_t)) * CHAR_BIT - 1))
+zig_shl_sat_s(short,    short, ((sizeof(short   )) * CHAR_BIT - 1))
+zig_shl_sat_s(int,        int, ((sizeof(int     )) * CHAR_BIT - 1))
+zig_shl_sat_s(long,      long, ((sizeof(long    )) * CHAR_BIT - 1))
src/stage1/all_types.hpp
@@ -1818,10 +1818,6 @@ enum BuiltinFnId {
     BuiltinFnIdReduce,
     BuiltinFnIdMaximum,
     BuiltinFnIdMinimum,
-    BuiltinFnIdSatAdd,
-    BuiltinFnIdSatSub,
-    BuiltinFnIdSatMul,
-    BuiltinFnIdSatShl,
 };
 
 struct BuiltinFnEntry {
src/stage1/astgen.cpp
@@ -4720,66 +4720,6 @@ static Stage1ZirInst *astgen_builtin_fn_call(Stage1AstGen *ag, Scope *scope, Ast
                 Stage1ZirInst *bin_op = ir_build_bin_op(ag, scope, node, IrBinOpMaximum, arg0_value, arg1_value, true);
                 return ir_lval_wrap(ag, scope, bin_op, lval, result_loc);
             }
-        case BuiltinFnIdSatAdd:
-            {
-                AstNode *arg0_node = node->data.fn_call_expr.params.at(0);
-                Stage1ZirInst *arg0_value = astgen_node(ag, arg0_node, scope);
-                if (arg0_value == ag->codegen->invalid_inst_src)
-                    return arg0_value;
-
-                AstNode *arg1_node = node->data.fn_call_expr.params.at(1);
-                Stage1ZirInst *arg1_value = astgen_node(ag, arg1_node, scope);
-                if (arg1_value == ag->codegen->invalid_inst_src)
-                    return arg1_value;
-
-                Stage1ZirInst *bin_op = ir_build_bin_op(ag, scope, node, IrBinOpAddSat, arg0_value, arg1_value, true);
-                return ir_lval_wrap(ag, scope, bin_op, lval, result_loc);
-            }
-        case BuiltinFnIdSatSub:
-            {
-                AstNode *arg0_node = node->data.fn_call_expr.params.at(0);
-                Stage1ZirInst *arg0_value = astgen_node(ag, arg0_node, scope);
-                if (arg0_value == ag->codegen->invalid_inst_src)
-                    return arg0_value;
-
-                AstNode *arg1_node = node->data.fn_call_expr.params.at(1);
-                Stage1ZirInst *arg1_value = astgen_node(ag, arg1_node, scope);
-                if (arg1_value == ag->codegen->invalid_inst_src)
-                    return arg1_value;
-
-                Stage1ZirInst *bin_op = ir_build_bin_op(ag, scope, node, IrBinOpSubSat, arg0_value, arg1_value, true);
-                return ir_lval_wrap(ag, scope, bin_op, lval, result_loc);
-            }
-        case BuiltinFnIdSatMul:
-            {
-                AstNode *arg0_node = node->data.fn_call_expr.params.at(0);
-                Stage1ZirInst *arg0_value = astgen_node(ag, arg0_node, scope);
-                if (arg0_value == ag->codegen->invalid_inst_src)
-                    return arg0_value;
-
-                AstNode *arg1_node = node->data.fn_call_expr.params.at(1);
-                Stage1ZirInst *arg1_value = astgen_node(ag, arg1_node, scope);
-                if (arg1_value == ag->codegen->invalid_inst_src)
-                    return arg1_value;
-
-                Stage1ZirInst *bin_op = ir_build_bin_op(ag, scope, node, IrBinOpMultSat, arg0_value, arg1_value, true);
-                return ir_lval_wrap(ag, scope, bin_op, lval, result_loc);
-            }
-        case BuiltinFnIdSatShl:
-            {
-                AstNode *arg0_node = node->data.fn_call_expr.params.at(0);
-                Stage1ZirInst *arg0_value = astgen_node(ag, arg0_node, scope);
-                if (arg0_value == ag->codegen->invalid_inst_src)
-                    return arg0_value;
-
-                AstNode *arg1_node = node->data.fn_call_expr.params.at(1);
-                Stage1ZirInst *arg1_value = astgen_node(ag, arg1_node, scope);
-                if (arg1_value == ag->codegen->invalid_inst_src)
-                    return arg1_value;
-
-                Stage1ZirInst *bin_op = ir_build_bin_op(ag, scope, node, IrBinOpShlSat, arg0_value, arg1_value, true);
-                return ir_lval_wrap(ag, scope, bin_op, lval, result_loc);
-            }
         case BuiltinFnIdMemcpy:
             {
                 AstNode *arg0_node = node->data.fn_call_expr.params.at(0);
src/stage1/codegen.cpp
@@ -9134,10 +9134,6 @@ static void define_builtin_fns(CodeGen *g) {
     create_builtin_fn(g, BuiltinFnIdReduce, "reduce", 2);
     create_builtin_fn(g, BuiltinFnIdMaximum, "maximum", 2);
     create_builtin_fn(g, BuiltinFnIdMinimum, "minimum", 2);
-    create_builtin_fn(g, BuiltinFnIdSatAdd, "addWithSaturation", 2);
-    create_builtin_fn(g, BuiltinFnIdSatSub, "subWithSaturation", 2);
-    create_builtin_fn(g, BuiltinFnIdSatMul, "mulWithSaturation", 2);
-    create_builtin_fn(g, BuiltinFnIdSatShl, "shlWithSaturation", 2);
 }
 
 static const char *bool_to_str(bool b) {
src/translate_c/ast.zig
@@ -1462,10 +1462,10 @@ fn renderNode(c: *Context, node: Node) Allocator.Error!NodeIndex {
         .mul_wrap_assign => return renderBinOp(c, node, .assign_mul_wrap, .asterisk_percent_equal, "*%="),
         .div => return renderBinOpGrouped(c, node, .div, .slash, "/"),
         .div_assign => return renderBinOp(c, node, .assign_div, .slash_equal, "/="),
-        .shl => return renderBinOpGrouped(c, node, .bit_shift_left, .angle_bracket_angle_bracket_left, "<<"),
-        .shl_assign => return renderBinOp(c, node, .assign_bit_shift_left, .angle_bracket_angle_bracket_left_equal, "<<="),
-        .shr => return renderBinOpGrouped(c, node, .bit_shift_right, .angle_bracket_angle_bracket_right, ">>"),
-        .shr_assign => return renderBinOp(c, node, .assign_bit_shift_right, .angle_bracket_angle_bracket_right_equal, ">>="),
+        .shl => return renderBinOpGrouped(c, node, .shl, .angle_bracket_angle_bracket_left, "<<"),
+        .shl_assign => return renderBinOp(c, node, .assign_shl, .angle_bracket_angle_bracket_left_equal, "<<="),
+        .shr => return renderBinOpGrouped(c, node, .shr, .angle_bracket_angle_bracket_right, ">>"),
+        .shr_assign => return renderBinOp(c, node, .assign_shr, .angle_bracket_angle_bracket_right_equal, ">>="),
         .mod => return renderBinOpGrouped(c, node, .mod, .percent, "%"),
         .mod_assign => return renderBinOp(c, node, .assign_mod, .percent_equal, "%="),
         .@"and" => return renderBinOpGrouped(c, node, .bool_and, .keyword_and, "and"),
src/Air.zig
@@ -48,7 +48,7 @@ pub const Inst = struct {
         /// Both operands are guaranteed to be the same type, and the result type
         /// is the same as both operands.
         /// Uses the `bin_op` field.
-        addsat,
+        add_sat,
         /// Float or integer subtraction. For integers, wrapping is undefined behavior.
         /// Both operands are guaranteed to be the same type, and the result type
         /// is the same as both operands.
@@ -63,7 +63,7 @@ pub const Inst = struct {
         /// Both operands are guaranteed to be the same type, and the result type
         /// is the same as both operands.
         /// Uses the `bin_op` field.
-        subsat,
+        sub_sat,
         /// Float or integer multiplication. For integers, wrapping is undefined behavior.
         /// Both operands are guaranteed to be the same type, and the result type
         /// is the same as both operands.
@@ -78,7 +78,7 @@ pub const Inst = struct {
         /// Both operands are guaranteed to be the same type, and the result type
         /// is the same as both operands.
         /// Uses the `bin_op` field.
-        mulsat,
+        mul_sat,
         /// Integer or float division. For integers, wrapping is undefined behavior.
         /// Both operands are guaranteed to be the same type, and the result type
         /// is the same as both operands.
@@ -125,6 +125,11 @@ pub const Inst = struct {
         /// Shift left. `<<`
         /// Uses the `bin_op` field.
         shl,
+        /// Shift left; For unsigned integers, the shift produces a poison value if it shifts
+        /// out any non-zero bits. For signed integers, the shift produces a poison value if
+        /// it shifts out any bits that disagree with the resultant sign bit.
+        /// Uses the `bin_op` field.
+        shl_exact,
         /// Shift left saturating. `<<|`
         /// Uses the `bin_op` field.
         shl_sat,
@@ -586,13 +591,13 @@ pub fn typeOfIndex(air: Air, inst: Air.Inst.Index) Type {
 
         .add,
         .addwrap,
-        .addsat,
+        .add_sat,
         .sub,
         .subwrap,
-        .subsat,
+        .sub_sat,
         .mul,
         .mulwrap,
-        .mulsat,
+        .mul_sat,
         .div,
         .rem,
         .mod,
@@ -603,6 +608,7 @@ pub fn typeOfIndex(air: Air, inst: Air.Inst.Index) Type {
         .ptr_sub,
         .shr,
         .shl,
+        .shl_exact,
         .shl_sat,
         => return air.typeOf(datas[inst].bin_op.lhs),
 
src/AstGen.zig
@@ -317,9 +317,9 @@ fn lvalExpr(gz: *GenZir, scope: *Scope, node: Ast.Node.Index) InnerError!Zir.Ins
         .assign,
         .assign_bit_and,
         .assign_bit_or,
-        .assign_bit_shift_left,
-        .assign_bit_shift_left_sat,
-        .assign_bit_shift_right,
+        .assign_shl,
+        .assign_shl_sat,
+        .assign_shr,
         .assign_bit_xor,
         .assign_div,
         .assign_sub,
@@ -345,9 +345,9 @@ fn lvalExpr(gz: *GenZir, scope: *Scope, node: Ast.Node.Index) InnerError!Zir.Ins
         .mod,
         .bit_and,
         .bit_or,
-        .bit_shift_left,
-        .bit_shift_left_sat,
-        .bit_shift_right,
+        .shl,
+        .shl_sat,
+        .shr,
         .bit_xor,
         .bang_equal,
         .equal_equal,
@@ -530,15 +530,15 @@ fn expr(gz: *GenZir, scope: *Scope, rl: ResultLoc, node: Ast.Node.Index) InnerEr
             return rvalue(gz, rl, .void_value, node);
         },
 
-        .assign_bit_shift_left => {
+        .assign_shl => {
             try assignShift(gz, scope, node, .shl);
             return rvalue(gz, rl, .void_value, node);
         },
-        .assign_bit_shift_left_sat => {
-            try assignOpExt(gz, scope, node, .shl_with_saturation, Zir.Inst.SaturatingArithmetic);
+        .assign_shl_sat => {
+            try assignShiftSat(gz, scope, node);
             return rvalue(gz, rl, .void_value, node);
         },
-        .assign_bit_shift_right => {
+        .assign_shr => {
             try assignShift(gz, scope, node, .shr);
             return rvalue(gz, rl, .void_value, node);
         },
@@ -568,7 +568,7 @@ fn expr(gz: *GenZir, scope: *Scope, rl: ResultLoc, node: Ast.Node.Index) InnerEr
             return rvalue(gz, rl, .void_value, node);
         },
         .assign_sub_sat => {
-            try assignOpExt(gz, scope, node, .sub_with_saturation, Zir.Inst.SaturatingArithmetic);
+            try assignOp(gz, scope, node, .sub_sat);
             return rvalue(gz, rl, .void_value, node);
         },
         .assign_mod => {
@@ -584,7 +584,7 @@ fn expr(gz: *GenZir, scope: *Scope, rl: ResultLoc, node: Ast.Node.Index) InnerEr
             return rvalue(gz, rl, .void_value, node);
         },
         .assign_add_sat => {
-            try assignOpExt(gz, scope, node, .add_with_saturation, Zir.Inst.SaturatingArithmetic);
+            try assignOp(gz, scope, node, .add_sat);
             return rvalue(gz, rl, .void_value, node);
         },
         .assign_mul => {
@@ -596,28 +596,27 @@ fn expr(gz: *GenZir, scope: *Scope, rl: ResultLoc, node: Ast.Node.Index) InnerEr
             return rvalue(gz, rl, .void_value, node);
         },
         .assign_mul_sat => {
-            try assignOpExt(gz, scope, node, .mul_with_saturation, Zir.Inst.SaturatingArithmetic);
+            try assignOp(gz, scope, node, .mul_sat);
             return rvalue(gz, rl, .void_value, node);
         },
 
         // zig fmt: off
-        .bit_shift_left     => return shiftOp(gz, scope, rl, node, node_datas[node].lhs, node_datas[node].rhs, .shl),
-        .bit_shift_right    => return shiftOp(gz, scope, rl, node, node_datas[node].lhs, node_datas[node].rhs, .shr),
+        .shl => return shiftOp(gz, scope, rl, node, node_datas[node].lhs, node_datas[node].rhs, .shl),
+        .shr => return shiftOp(gz, scope, rl, node, node_datas[node].lhs, node_datas[node].rhs, .shr),
 
         .add      => return simpleBinOp(gz, scope, rl, node, .add),
         .add_wrap => return simpleBinOp(gz, scope, rl, node, .addwrap),
+        .add_sat  => return simpleBinOp(gz, scope, rl, node, .add_sat),
         .sub      => return simpleBinOp(gz, scope, rl, node, .sub),
         .sub_wrap => return simpleBinOp(gz, scope, rl, node, .subwrap),
+        .sub_sat  => return simpleBinOp(gz, scope, rl, node, .sub_sat),
         .mul      => return simpleBinOp(gz, scope, rl, node, .mul),
         .mul_wrap => return simpleBinOp(gz, scope, rl, node, .mulwrap),
+        .mul_sat  => return simpleBinOp(gz, scope, rl, node, .mul_sat),
         .div      => return simpleBinOp(gz, scope, rl, node, .div),
         .mod      => return simpleBinOp(gz, scope, rl, node, .mod_rem),
+        .shl_sat  => return simpleBinOp(gz, scope, rl, node, .shl_sat),
 
-        .add_sat            => return simpleBinOpExt(gz, scope, rl, node, node_datas[node].lhs, node_datas[node].rhs, .add_with_saturation, Zir.Inst.SaturatingArithmetic),
-        .sub_sat            => return simpleBinOpExt(gz, scope, rl, node, node_datas[node].lhs, node_datas[node].rhs, .sub_with_saturation, Zir.Inst.SaturatingArithmetic),
-        .mul_sat            => return simpleBinOpExt(gz, scope, rl, node, node_datas[node].lhs, node_datas[node].rhs, .mul_with_saturation, Zir.Inst.SaturatingArithmetic),
-        .bit_shift_left_sat => return simpleBinOpExt(gz, scope, rl, node, node_datas[node].lhs, node_datas[node].rhs, .shl_with_saturation, Zir.Inst.SaturatingArithmetic),
-        
         .bit_and  => {
             const current_ampersand_token = main_tokens[node];
             if (token_tags[current_ampersand_token + 1] == .ampersand) {
@@ -1928,8 +1927,8 @@ fn blockExprStmts(gz: *GenZir, parent_scope: *Scope, statements: []const Ast.Nod
 
             .assign => try assign(gz, scope, statement),
 
-            .assign_bit_shift_left  => try assignShift(gz, scope, statement, .shl),
-            .assign_bit_shift_right => try assignShift(gz, scope, statement, .shr),
+            .assign_shl => try assignShift(gz, scope, statement, .shl),
+            .assign_shr => try assignShift(gz, scope, statement, .shr),
 
             .assign_bit_and  => try assignOp(gz, scope, statement, .bit_and),
             .assign_bit_or   => try assignOp(gz, scope, statement, .bit_or),
@@ -1979,6 +1978,7 @@ fn unusedResultExpr(gz: *GenZir, scope: *Scope, statement: Ast.Node.Index) Inner
             // ZIR instructions that might be a type other than `noreturn` or `void`.
             .add,
             .addwrap,
+            .add_sat,
             .param,
             .param_comptime,
             .param_anytype,
@@ -2045,12 +2045,15 @@ fn unusedResultExpr(gz: *GenZir, scope: *Scope, statement: Ast.Node.Index) Inner
             .mod_rem,
             .mul,
             .mulwrap,
+            .mul_sat,
             .ref,
             .shl,
+            .shl_sat,
             .shr,
             .str,
             .sub,
             .subwrap,
+            .sub_sat,
             .negate,
             .negate_wrap,
             .typeof,
@@ -2715,55 +2718,30 @@ fn assignOp(
     _ = try gz.addBin(.store, lhs_ptr, result);
 }
 
-fn simpleBinOpExt(
-    gz: *GenZir,
-    scope: *Scope,
-    rl: ResultLoc,
-    infix_node: Ast.Node.Index,
-    lhs_node: Ast.Node.Index,
-    rhs_node: Ast.Node.Index,
-    tag: Zir.Inst.Extended,
-    comptime T: type,
-) InnerError!Zir.Inst.Ref {
-    const lhs = try expr(gz, scope, .none, lhs_node);
-    const rhs = try expr(gz, scope, .none, rhs_node);
-    const result = try gz.addExtendedPayload(tag, T{
-        .node = gz.nodeIndexToRelative(infix_node),
-        .lhs = lhs,
-        .rhs = rhs,
-    });
-    return rvalue(gz, rl, result, infix_node);
-}
-
-fn assignOpExt(
+fn assignShift(
     gz: *GenZir,
     scope: *Scope,
     infix_node: Ast.Node.Index,
-    op_inst_tag: Zir.Inst.Extended,
-    comptime T: type,
+    op_inst_tag: Zir.Inst.Tag,
 ) InnerError!void {
+    try emitDbgNode(gz, infix_node);
     const astgen = gz.astgen;
     const tree = astgen.tree;
     const node_datas = tree.nodes.items(.data);
 
     const lhs_ptr = try lvalExpr(gz, scope, node_datas[infix_node].lhs);
     const lhs = try gz.addUnNode(.load, lhs_ptr, infix_node);
-    const lhs_type = try gz.addUnNode(.typeof, lhs, infix_node);
-    const rhs = try expr(gz, scope, .{ .coerced_ty = lhs_type }, node_datas[infix_node].rhs);
-    const result = try gz.addExtendedPayload(op_inst_tag, T{
-        .node = gz.nodeIndexToRelative(infix_node),
+    const rhs_type = try gz.addUnNode(.typeof_log2_int_type, lhs, infix_node);
+    const rhs = try expr(gz, scope, .{ .ty = rhs_type }, node_datas[infix_node].rhs);
+
+    const result = try gz.addPlNode(op_inst_tag, infix_node, Zir.Inst.Bin{
         .lhs = lhs,
         .rhs = rhs,
     });
     _ = try gz.addBin(.store, lhs_ptr, result);
 }
 
-fn assignShift(
-    gz: *GenZir,
-    scope: *Scope,
-    infix_node: Ast.Node.Index,
-    op_inst_tag: Zir.Inst.Tag,
-) InnerError!void {
+fn assignShiftSat(gz: *GenZir, scope: *Scope, infix_node: Ast.Node.Index) InnerError!void {
     try emitDbgNode(gz, infix_node);
     const astgen = gz.astgen;
     const tree = astgen.tree;
@@ -2771,10 +2749,10 @@ fn assignShift(
 
     const lhs_ptr = try lvalExpr(gz, scope, node_datas[infix_node].lhs);
     const lhs = try gz.addUnNode(.load, lhs_ptr, infix_node);
-    const rhs_type = try gz.addUnNode(.typeof_log2_int_type, lhs, infix_node);
-    const rhs = try expr(gz, scope, .{ .ty = rhs_type }, node_datas[infix_node].rhs);
+    // Saturating shift-left allows any integer type for both the LHS and RHS.
+    const rhs = try expr(gz, scope, .none, node_datas[infix_node].rhs);
 
-    const result = try gz.addPlNode(op_inst_tag, infix_node, Zir.Inst.Bin{
+    const result = try gz.addPlNode(.shl_sat, infix_node, Zir.Inst.Bin{
         .lhs = lhs,
         .rhs = rhs,
     });
@@ -7556,11 +7534,6 @@ fn builtinCall(
             return rvalue(gz, rl, result, node);
         },
 
-        .add_with_saturation => return saturatingArithmetic(gz, scope, rl, node, params, .add_with_saturation),
-        .sub_with_saturation => return saturatingArithmetic(gz, scope, rl, node, params, .sub_with_saturation),
-        .mul_with_saturation => return saturatingArithmetic(gz, scope, rl, node, params, .mul_with_saturation),
-        .shl_with_saturation => return saturatingArithmetic(gz, scope, rl, node, params, .shl_with_saturation),
-        
         .atomic_load => {
             const int_type = try typeExpr(gz, scope, params[0]);
             // TODO allow this pointer type to be volatile
@@ -7955,24 +7928,6 @@ fn overflowArithmetic(
     return rvalue(gz, rl, result, node);
 }
 
-fn saturatingArithmetic(
-    gz: *GenZir,
-    scope: *Scope,
-    rl: ResultLoc,
-    node: Ast.Node.Index,
-    params: []const Ast.Node.Index,
-    tag: Zir.Inst.Extended,
-) InnerError!Zir.Inst.Ref {
-    const lhs = try expr(gz, scope, .none, params[0]);
-    const rhs = try expr(gz, scope, .none, params[1]);
-    const result = try gz.addExtendedPayload(tag, Zir.Inst.SaturatingArithmetic{
-        .node = gz.nodeIndexToRelative(node),
-        .lhs = lhs,
-        .rhs = rhs,
-    });
-    return rvalue(gz, rl, result, node);
-}
-
 fn callExpr(
     gz: *GenZir,
     scope: *Scope,
@@ -8198,9 +8153,9 @@ fn nodeMayNeedMemoryLocation(tree: *const Ast, start_node: Ast.Node.Index) bool
             .assign,
             .assign_bit_and,
             .assign_bit_or,
-            .assign_bit_shift_left,
-            .assign_bit_shift_left_sat,
-            .assign_bit_shift_right,
+            .assign_shl,
+            .assign_shl_sat,
+            .assign_shr,
             .assign_bit_xor,
             .assign_div,
             .assign_sub,
@@ -8216,9 +8171,9 @@ fn nodeMayNeedMemoryLocation(tree: *const Ast, start_node: Ast.Node.Index) bool
             .bang_equal,
             .bit_and,
             .bit_or,
-            .bit_shift_left,
-            .bit_shift_left_sat,
-            .bit_shift_right,
+            .shl,
+            .shl_sat,
+            .shr,
             .bit_xor,
             .bool_and,
             .bool_or,
@@ -8439,9 +8394,9 @@ fn nodeMayEvalToError(tree: *const Ast, start_node: Ast.Node.Index) enum { never
             .assign,
             .assign_bit_and,
             .assign_bit_or,
-            .assign_bit_shift_left,
-            .assign_bit_shift_left_sat,
-            .assign_bit_shift_right,
+            .assign_shl,
+            .assign_shl_sat,
+            .assign_shr,
             .assign_bit_xor,
             .assign_div,
             .assign_sub,
@@ -8457,9 +8412,9 @@ fn nodeMayEvalToError(tree: *const Ast, start_node: Ast.Node.Index) enum { never
             .bang_equal,
             .bit_and,
             .bit_or,
-            .bit_shift_left,
-            .bit_shift_left_sat,
-            .bit_shift_right,
+            .shl,
+            .shl_sat,
+            .shr,
             .bit_xor,
             .bool_and,
             .bool_or,
@@ -8619,9 +8574,9 @@ fn nodeImpliesRuntimeBits(tree: *const Ast, start_node: Ast.Node.Index) bool {
             .assign,
             .assign_bit_and,
             .assign_bit_or,
-            .assign_bit_shift_left,
-            .assign_bit_shift_left_sat,
-            .assign_bit_shift_right,
+            .assign_shl,
+            .assign_shl_sat,
+            .assign_shr,
             .assign_bit_xor,
             .assign_div,
             .assign_sub,
@@ -8637,9 +8592,9 @@ fn nodeImpliesRuntimeBits(tree: *const Ast, start_node: Ast.Node.Index) bool {
             .bang_equal,
             .bit_and,
             .bit_or,
-            .bit_shift_left,
-            .bit_shift_left_sat,
-            .bit_shift_right,
+            .shl,
+            .shl_sat,
+            .shr,
             .bit_xor,
             .bool_and,
             .bool_or,
src/BuiltinFn.zig
@@ -2,7 +2,6 @@ const std = @import("std");
 
 pub const Tag = enum {
     add_with_overflow,
-    add_with_saturation,
     align_cast,
     align_of,
     as,
@@ -66,7 +65,6 @@ pub const Tag = enum {
     wasm_memory_grow,
     mod,
     mul_with_overflow,
-    mul_with_saturation,
     panic,
     pop_count,
     ptr_cast,
@@ -81,12 +79,10 @@ pub const Tag = enum {
     set_runtime_safety,
     shl_exact,
     shl_with_overflow,
-    shl_with_saturation,
     shr_exact,
     shuffle,
     size_of,
     splat,
-    sub_with_saturation,
     reduce,
     src,
     sqrt,
@@ -531,34 +527,6 @@ pub const list = list: {
                 .param_count = 2,
             },
         },
-        .{
-            "@addWithSaturation",
-            .{
-                .tag = .add_with_saturation,
-                .param_count = 2,
-            },
-        },
-        .{
-            "@subWithSaturation",
-            .{
-                .tag = .sub_with_saturation,
-                .param_count = 2,
-            },
-        },
-        .{
-            "@mulWithSaturation",
-            .{
-                .tag = .mul_with_saturation,
-                .param_count = 2,
-            },
-        },
-        .{
-            "@shlWithSaturation",
-            .{
-                .tag = .shl_with_saturation,
-                .param_count = 2,
-            },
-        },
         .{
             "@memcpy",
             .{
src/codegen.zig
@@ -824,18 +824,20 @@ fn Function(comptime arch: std.Target.Cpu.Arch) type {
 
                 switch (air_tags[inst]) {
                     // zig fmt: off
-                    .add, .ptr_add => try self.airAdd(inst),
-                    .addwrap       => try self.airAddWrap(inst),
-                    .addsat        => try self.airArithmeticOpSat(inst, "addsat"),
-                    .sub, .ptr_sub => try self.airSub(inst),
-                    .subwrap       => try self.airSubWrap(inst),
-                    .subsat        => try self.airArithmeticOpSat(inst, "subsat"),
-                    .mul           => try self.airMul(inst),
-                    .mulwrap       => try self.airMulWrap(inst),
-                    .mulsat        => try self.airArithmeticOpSat(inst, "mulsat"),
-                    .div           => try self.airDiv(inst),
-                    .rem           => try self.airRem(inst),
-                    .mod           => try self.airMod(inst),
+                    .add, .ptr_add   => try self.airAdd(inst),
+                    .addwrap         => try self.airAddWrap(inst),
+                    .add_sat         => try self.airAddSat(inst),
+                    .sub, .ptr_sub   => try self.airSub(inst),
+                    .subwrap         => try self.airSubWrap(inst),
+                    .sub_sat         => try self.airSubSat(inst),
+                    .mul             => try self.airMul(inst),
+                    .mulwrap         => try self.airMulWrap(inst),
+                    .mul_sat         => try self.airMulSat(inst),
+                    .div             => try self.airDiv(inst),
+                    .rem             => try self.airRem(inst),
+                    .mod             => try self.airMod(inst),
+                    .shl, .shl_exact => try self.airShl(inst),
+                    .shl_sat         => try self.airShlSat(inst),
 
                     .cmp_lt  => try self.airCmp(inst, .lt),
                     .cmp_lte => try self.airCmp(inst, .lte),
@@ -850,8 +852,6 @@ fn Function(comptime arch: std.Target.Cpu.Arch) type {
                     .bit_or   => try self.airBitOr(inst),
                     .xor      => try self.airXor(inst),
                     .shr      => try self.airShr(inst),
-                    .shl      => try self.airShl(inst),
-                    .shl_sat  => try self.airArithmeticOpSat(inst, "shl_sat"),
 
                     .alloc           => try self.airAlloc(inst),
                     .arg             => try self.airArg(inst),
@@ -1306,6 +1306,14 @@ fn Function(comptime arch: std.Target.Cpu.Arch) type {
             return self.finishAir(inst, result, .{ bin_op.lhs, bin_op.rhs, .none });
         }
 
+        fn airAddSat(self: *Self, inst: Air.Inst.Index) !void {
+            const bin_op = self.air.instructions.items(.data)[inst].bin_op;
+            const result: MCValue = if (self.liveness.isUnused(inst)) .dead else switch (arch) {
+                else => return self.fail("TODO implement add_sat for {}", .{self.target.cpu.arch}),
+            };
+            return self.finishAir(inst, result, .{ bin_op.lhs, bin_op.rhs, .none });
+        }
+
         fn airSub(self: *Self, inst: Air.Inst.Index) !void {
             const bin_op = self.air.instructions.items(.data)[inst].bin_op;
             const result: MCValue = if (self.liveness.isUnused(inst)) .dead else switch (arch) {
@@ -1324,10 +1332,10 @@ fn Function(comptime arch: std.Target.Cpu.Arch) type {
             return self.finishAir(inst, result, .{ bin_op.lhs, bin_op.rhs, .none });
         }
 
-        fn airArithmeticOpSat(self: *Self, inst: Air.Inst.Index, comptime name: []const u8) !void {
+        fn airSubSat(self: *Self, inst: Air.Inst.Index) !void {
             const bin_op = self.air.instructions.items(.data)[inst].bin_op;
             const result: MCValue = if (self.liveness.isUnused(inst)) .dead else switch (arch) {
-                else => return self.fail("TODO implement " ++ name ++ " for {}", .{self.target.cpu.arch}),
+                else => return self.fail("TODO implement sub_sat for {}", .{self.target.cpu.arch}),
             };
             return self.finishAir(inst, result, .{ bin_op.lhs, bin_op.rhs, .none });
         }
@@ -1350,6 +1358,14 @@ fn Function(comptime arch: std.Target.Cpu.Arch) type {
             return self.finishAir(inst, result, .{ bin_op.lhs, bin_op.rhs, .none });
         }
 
+        fn airMulSat(self: *Self, inst: Air.Inst.Index) !void {
+            const bin_op = self.air.instructions.items(.data)[inst].bin_op;
+            const result: MCValue = if (self.liveness.isUnused(inst)) .dead else switch (arch) {
+                else => return self.fail("TODO implement mul_sat for {}", .{self.target.cpu.arch}),
+            };
+            return self.finishAir(inst, result, .{ bin_op.lhs, bin_op.rhs, .none });
+        }
+
         fn airDiv(self: *Self, inst: Air.Inst.Index) !void {
             const bin_op = self.air.instructions.items(.data)[inst].bin_op;
             const result: MCValue = if (self.liveness.isUnused(inst)) .dead else switch (arch) {
@@ -1412,6 +1428,14 @@ fn Function(comptime arch: std.Target.Cpu.Arch) type {
             return self.finishAir(inst, result, .{ bin_op.lhs, bin_op.rhs, .none });
         }
 
+        fn airShlSat(self: *Self, inst: Air.Inst.Index) !void {
+            const bin_op = self.air.instructions.items(.data)[inst].bin_op;
+            const result: MCValue = if (self.liveness.isUnused(inst)) .dead else switch (arch) {
+                else => return self.fail("TODO implement shl_sat for {}", .{self.target.cpu.arch}),
+            };
+            return self.finishAir(inst, result, .{ bin_op.lhs, bin_op.rhs, .none });
+        }
+
         fn airShr(self: *Self, inst: Air.Inst.Index) !void {
             const bin_op = self.air.instructions.items(.data)[inst].bin_op;
             const result: MCValue = if (self.liveness.isUnused(inst)) .dead else switch (arch) {
src/Liveness.zig
@@ -226,13 +226,13 @@ fn analyzeInst(
     switch (inst_tags[inst]) {
         .add,
         .addwrap,
-        .addsat,
+        .add_sat,
         .sub,
         .subwrap,
-        .subsat,
+        .sub_sat,
         .mul,
         .mulwrap,
-        .mulsat,
+        .mul_sat,
         .div,
         .rem,
         .mod,
@@ -255,6 +255,7 @@ fn analyzeInst(
         .ptr_elem_val,
         .ptr_ptr_elem_val,
         .shl,
+        .shl_exact,
         .shl_sat,
         .shr,
         .atomic_store_unordered,
src/print_air.zig
@@ -104,13 +104,13 @@ const Writer = struct {
 
             .add,
             .addwrap,
-            .addsat,
+            .add_sat,
             .sub,
             .subwrap,
-            .subsat,
+            .sub_sat,
             .mul,
             .mulwrap,
-            .mulsat,
+            .mul_sat,
             .div,
             .rem,
             .mod,
@@ -133,6 +133,7 @@ const Writer = struct {
             .ptr_elem_val,
             .ptr_ptr_elem_val,
             .shl,
+            .shl_exact,
             .shl_sat,
             .shr,
             .set_union_tag,
src/print_zir.zig
@@ -229,12 +229,15 @@ const Writer = struct {
 
             .add,
             .addwrap,
+            .add_sat,
             .array_cat,
             .array_mul,
             .mul,
             .mulwrap,
+            .mul_sat,
             .sub,
             .subwrap,
+            .sub_sat,
             .cmp_lt,
             .cmp_lte,
             .cmp_eq,
@@ -247,6 +250,7 @@ const Writer = struct {
             .mod_rem,
             .shl,
             .shl_exact,
+            .shl_sat,
             .shr,
             .shr_exact,
             .xor,
@@ -400,12 +404,6 @@ const Writer = struct {
             .shl_with_overflow,
             => try self.writeOverflowArithmetic(stream, extended),
 
-            .add_with_saturation,
-            .sub_with_saturation,
-            .mul_with_saturation,
-            .shl_with_saturation,
-            => try self.writeSaturatingArithmetic(stream, extended),
-
             .struct_decl => try self.writeStructDecl(stream, extended),
             .union_decl => try self.writeUnionDecl(stream, extended),
             .enum_decl => try self.writeEnumDecl(stream, extended),
@@ -854,18 +852,6 @@ const Writer = struct {
         try self.writeSrc(stream, src);
     }
 
-    fn writeSaturatingArithmetic(self: *Writer, stream: anytype, extended: Zir.Inst.Extended.InstData) !void {
-        const extra = self.code.extraData(Zir.Inst.SaturatingArithmetic, extended.operand).data;
-        const src: LazySrcLoc = .{ .node_offset = extra.node };
-
-        try self.writeInstRef(stream, extra.lhs);
-        try stream.writeAll(", ");
-        try self.writeInstRef(stream, extra.rhs);
-        try stream.writeAll(", ");
-        try stream.writeAll(") ");
-        try self.writeSrc(stream, src);
-    }
-
     fn writePlNodeCall(self: *Writer, stream: anytype, inst: Zir.Inst.Index) !void {
         const inst_data = self.code.instructions.items(.data)[inst].pl_node;
         const extra = self.code.extraData(Zir.Inst.Call, inst_data.payload_index);
src/Sema.zig
@@ -246,7 +246,6 @@ pub fn analyzeBody(
             .ptr_type_simple              => try sema.zirPtrTypeSimple(block, inst),
             .ref                          => try sema.zirRef(block, inst),
             .ret_err_value_code           => try sema.zirRetErrValueCode(block, inst),
-            .shl                          => try sema.zirShl(block, inst),
             .shr                          => try sema.zirShr(block, inst),
             .slice_end                    => try sema.zirSliceEnd(block, inst),
             .slice_sentinel               => try sema.zirSliceSentinel(block, inst),
@@ -319,7 +318,6 @@ pub fn analyzeBody(
             .div_exact                    => try sema.zirDivExact(block, inst),
             .div_floor                    => try sema.zirDivFloor(block, inst),
             .div_trunc                    => try sema.zirDivTrunc(block, inst),
-            .shl_exact                    => try sema.zirShlExact(block, inst),
             .shr_exact                    => try sema.zirShrExact(block, inst),
             .bit_offset_of                => try sema.zirBitOffsetOf(block, inst),
             .offset_of                    => try sema.zirOffsetOf(block, inst),
@@ -363,14 +361,21 @@ pub fn analyzeBody(
 
             .add     => try sema.zirArithmetic(block, inst, .add),
             .addwrap => try sema.zirArithmetic(block, inst, .addwrap),
+            .add_sat => try sema.zirArithmetic(block, inst, .add_sat),
             .div     => try sema.zirArithmetic(block, inst, .div),
             .mod_rem => try sema.zirArithmetic(block, inst, .mod_rem),
             .mod     => try sema.zirArithmetic(block, inst, .mod),
             .rem     => try sema.zirArithmetic(block, inst, .rem),
             .mul     => try sema.zirArithmetic(block, inst, .mul),
             .mulwrap => try sema.zirArithmetic(block, inst, .mulwrap),
+            .mul_sat => try sema.zirArithmetic(block, inst, .mul_sat),
             .sub     => try sema.zirArithmetic(block, inst, .sub),
             .subwrap => try sema.zirArithmetic(block, inst, .subwrap),
+            .sub_sat => try sema.zirArithmetic(block, inst, .sub_sat),
+
+            .shl       => try sema.zirShl(block, inst, .shl),
+            .shl_exact => try sema.zirShl(block, inst, .shl_exact),
+            .shl_sat   => try sema.zirShl(block, inst, .shl_sat),
 
             // Instructions that we know to *always* be noreturn based solely on their tag.
             // These functions match the return type of analyzeBody so that we can
@@ -694,11 +699,6 @@ fn zirExtended(sema: *Sema, block: *Scope.Block, inst: Zir.Inst.Index) CompileEr
         .c_define           => return sema.zirCDefine(           block, extended),
         .wasm_memory_size   => return sema.zirWasmMemorySize(    block, extended),
         .wasm_memory_grow   => return sema.zirWasmMemoryGrow(    block, extended),
-        .add_with_saturation, 
-        .sub_with_saturation, 
-        .mul_with_saturation, 
-        .shl_with_saturation, 
-                            => return sema.zirSatArithmetic(     block, extended),
         // zig fmt: on
     }
 }
@@ -5875,7 +5875,12 @@ fn zirRetErrValueCode(sema: *Sema, block: *Scope.Block, inst: Zir.Inst.Index) Co
     return sema.mod.fail(&block.base, sema.src, "TODO implement zirRetErrValueCode", .{});
 }
 
-fn zirShl(sema: *Sema, block: *Scope.Block, inst: Zir.Inst.Index) CompileError!Air.Inst.Ref {
+fn zirShl(
+    sema: *Sema,
+    block: *Scope.Block,
+    inst: Zir.Inst.Index,
+    air_tag: Air.Inst.Tag,
+) CompileError!Air.Inst.Ref {
     const tracy = trace(@src());
     defer tracy.end();
 
@@ -5886,6 +5891,8 @@ fn zirShl(sema: *Sema, block: *Scope.Block, inst: Zir.Inst.Index) CompileError!A
     const lhs = sema.resolveInst(extra.lhs);
     const rhs = sema.resolveInst(extra.rhs);
 
+    // TODO coerce rhs if air_tag is not shl_sat
+
     const maybe_lhs_val = try sema.resolveMaybeUndefVal(block, lhs_src, lhs);
     const maybe_rhs_val = try sema.resolveMaybeUndefVal(block, rhs_src, rhs);
 
@@ -5901,6 +5908,12 @@ fn zirShl(sema: *Sema, block: *Scope.Block, inst: Zir.Inst.Index) CompileError!A
             return sema.addConstant(lhs_ty, lhs_val);
         }
         const val = try lhs_val.shl(rhs_val, sema.arena);
+        switch (air_tag) {
+            .shl_exact => return sema.mod.fail(&block.base, lhs_src, "TODO implement Sema for comptime shl_exact", .{}),
+            .shl_sat => return sema.mod.fail(&block.base, lhs_src, "TODO implement Sema for comptime shl_sat", .{}),
+            .shl => {},
+            else => unreachable,
+        }
         return sema.addConstant(lhs_ty, val);
     } else rs: {
         if (maybe_rhs_val) |rhs_val| {
@@ -5909,8 +5922,10 @@ fn zirShl(sema: *Sema, block: *Scope.Block, inst: Zir.Inst.Index) CompileError!A
         break :rs lhs_src;
     };
 
+    // TODO: insert runtime safety check for shl_exact
+
     try sema.requireRuntimeBlock(block, runtime_src);
-    return block.addBinOp(.shl, lhs, rhs);
+    return block.addBinOp(air_tag, lhs, rhs);
 }
 
 fn zirShr(sema: *Sema, block: *Scope.Block, inst: Zir.Inst.Index) CompileError!Air.Inst.Ref {
@@ -6201,105 +6216,6 @@ fn zirOverflowArithmetic(
     return sema.mod.fail(&block.base, src, "TODO implement Sema.zirOverflowArithmetic", .{});
 }
 
-fn zirSatArithmetic(
-    sema: *Sema,
-    block: *Scope.Block,
-    extended: Zir.Inst.Extended.InstData,
-) CompileError!Air.Inst.Ref {
-    const tracy = trace(@src());
-    defer tracy.end();
-
-    const extra = sema.code.extraData(Zir.Inst.SaturatingArithmetic, extended.operand).data;
-    sema.src = .{ .node_offset_bin_op = extra.node };
-    const lhs_src: LazySrcLoc = .{ .node_offset_bin_lhs = extra.node };
-    const rhs_src: LazySrcLoc = .{ .node_offset_bin_rhs = extra.node };
-    const lhs = sema.resolveInst(extra.lhs);
-    const rhs = sema.resolveInst(extra.rhs);
-
-    return sema.analyzeSatArithmetic(block, lhs, rhs, sema.src, lhs_src, rhs_src, extended);
-}
-
-fn analyzeSatArithmetic(
-    sema: *Sema,
-    block: *Scope.Block,
-    lhs: Air.Inst.Ref,
-    rhs: Air.Inst.Ref,
-    src: LazySrcLoc,
-    lhs_src: LazySrcLoc,
-    rhs_src: LazySrcLoc,
-    extended: Zir.Inst.Extended.InstData,
-) CompileError!Air.Inst.Ref {
-    const lhs_ty = sema.typeOf(lhs);
-    const rhs_ty = sema.typeOf(rhs);
-    const lhs_zig_ty_tag = try lhs_ty.zigTypeTagOrPoison();
-    const rhs_zig_ty_tag = try rhs_ty.zigTypeTagOrPoison();
-    if (lhs_zig_ty_tag == .Vector and rhs_zig_ty_tag == .Vector) {
-        if (lhs_ty.arrayLen() != rhs_ty.arrayLen()) {
-            return sema.mod.fail(&block.base, src, "vector length mismatch: {d} and {d}", .{
-                lhs_ty.arrayLen(), rhs_ty.arrayLen(),
-            });
-        }
-        return sema.mod.fail(&block.base, src, "TODO implement support for vectors in zirBinOp", .{});
-    } else if (lhs_zig_ty_tag == .Vector or rhs_zig_ty_tag == .Vector) {
-        return sema.mod.fail(&block.base, src, "mixed scalar and vector operands to binary expression: '{}' and '{}'", .{
-            lhs_ty, rhs_ty,
-        });
-    }
-
-    if (lhs_zig_ty_tag == .Pointer or rhs_zig_ty_tag == .Pointer)
-        return sema.mod.fail(&block.base, src, "TODO implement support for pointers in zirSatArithmetic", .{});
-
-    const instructions = &[_]Air.Inst.Ref{ lhs, rhs };
-    const resolved_type = try sema.resolvePeerTypes(block, src, instructions, .{ .override = &[_]LazySrcLoc{ lhs_src, rhs_src } });
-    const casted_lhs = try sema.coerce(block, resolved_type, lhs, lhs_src);
-    const casted_rhs = try sema.coerce(block, resolved_type, rhs, rhs_src);
-
-    const scalar_type = if (resolved_type.zigTypeTag() == .Vector)
-        resolved_type.elemType()
-    else
-        resolved_type;
-
-    const scalar_tag = scalar_type.zigTypeTag();
-
-    const is_int = scalar_tag == .Int or scalar_tag == .ComptimeInt;
-
-    if (!is_int)
-        return sema.mod.fail(&block.base, src, "invalid operands to binary expression: '{s}' and '{s}'", .{
-            @tagName(lhs_zig_ty_tag), @tagName(rhs_zig_ty_tag),
-        });
-
-    if (try sema.resolveMaybeUndefVal(block, lhs_src, casted_lhs)) |lhs_val| {
-        if (try sema.resolveMaybeUndefVal(block, rhs_src, casted_rhs)) |rhs_val| {
-            if (lhs_val.isUndef() or rhs_val.isUndef()) {
-                return sema.addConstUndef(resolved_type);
-            }
-            // incase rhs is 0, simply return lhs without doing any calculations
-            if (rhs_val.compareWithZero(.eq)) {
-                switch (extended.opcode) {
-                    .add_with_saturation, .sub_with_saturation => return sema.addConstant(scalar_type, lhs_val),
-                    else => {},
-                }
-            }
-
-            return sema.mod.fail(&block.base, src, "TODO implement comptime saturating arithmetic for operand '{s}'", .{@tagName(extended.opcode)});
-        } else {
-            try sema.requireRuntimeBlock(block, rhs_src);
-        }
-    } else {
-        try sema.requireRuntimeBlock(block, lhs_src);
-    }
-
-    const air_tag: Air.Inst.Tag = switch (extended.opcode) {
-        .add_with_saturation => .addsat,
-        .sub_with_saturation => .subsat,
-        .mul_with_saturation => .mulsat,
-        .shl_with_saturation => .shl_sat,
-        else => return sema.mod.fail(&block.base, src, "TODO implement arithmetic for extended opcode '{s}'", .{@tagName(extended.opcode)}),
-    };
-
-    return block.addBinOp(air_tag, casted_lhs, casted_rhs);
-}
-
 fn analyzeArithmetic(
     sema: *Sema,
     block: *Scope.Block,
@@ -6441,8 +6357,7 @@ fn analyzeArithmetic(
             },
             .addwrap => {
                 // Integers only; floats are checked above.
-                // If either of the operands are zero, then the other operand is
-                // returned, even if it is undefined.
+                // If either of the operands are zero, the other operand is returned.
                 // If either of the operands are undefined, the result is undefined.
                 if (maybe_lhs_val) |lhs_val| {
                     if (!lhs_val.isUndef() and lhs_val.compareWithZero(.eq)) {
@@ -6464,6 +6379,30 @@ fn analyzeArithmetic(
                     } else break :rs .{ .src = lhs_src, .air_tag = .addwrap };
                 } else break :rs .{ .src = rhs_src, .air_tag = .addwrap };
             },
+            .add_sat => {
+                // For both integers and floats:
+                // If either of the operands are zero, then the other operand is returned.
+                // If either of the operands are undefined, the result is undefined.
+                if (maybe_lhs_val) |lhs_val| {
+                    if (!lhs_val.isUndef() and lhs_val.compareWithZero(.eq)) {
+                        return casted_rhs;
+                    }
+                }
+                if (maybe_rhs_val) |rhs_val| {
+                    if (rhs_val.isUndef()) {
+                        return sema.addConstUndef(scalar_type);
+                    }
+                    if (rhs_val.compareWithZero(.eq)) {
+                        return casted_lhs;
+                    }
+                    if (maybe_lhs_val) |lhs_val| {
+                        return sema.addConstant(
+                            scalar_type,
+                            try lhs_val.numberAddSat(rhs_val, scalar_type, sema.arena, target),
+                        );
+                    } else break :rs .{ .src = lhs_src, .air_tag = .add_sat };
+                } else break :rs .{ .src = rhs_src, .air_tag = .add_sat };
+            },
             .sub => {
                 // For integers:
                 // If the rhs is zero, then the other operand is
@@ -6531,6 +6470,30 @@ fn analyzeArithmetic(
                     } else break :rs .{ .src = rhs_src, .air_tag = .subwrap };
                 } else break :rs .{ .src = lhs_src, .air_tag = .subwrap };
             },
+            .sub_sat => {
+                // For both integers and floats:
+                // If the RHS is zero, result is LHS.
+                // If either of the operands are undefined, result is undefined.
+                if (maybe_rhs_val) |rhs_val| {
+                    if (rhs_val.isUndef()) {
+                        return sema.addConstUndef(scalar_type);
+                    }
+                    if (rhs_val.compareWithZero(.eq)) {
+                        return casted_lhs;
+                    }
+                }
+                if (maybe_lhs_val) |lhs_val| {
+                    if (lhs_val.isUndef()) {
+                        return sema.addConstUndef(scalar_type);
+                    }
+                    if (maybe_rhs_val) |rhs_val| {
+                        return sema.addConstant(
+                            scalar_type,
+                            try lhs_val.numberSubSat(rhs_val, scalar_type, sema.arena, target),
+                        );
+                    } else break :rs .{ .src = rhs_src, .air_tag = .sub_sat };
+                } else break :rs .{ .src = lhs_src, .air_tag = .sub_sat };
+            },
             .div => {
                 // For integers:
                 // If the lhs is zero, then zero is returned regardless of rhs.
@@ -6649,10 +6612,9 @@ fn analyzeArithmetic(
             },
             .mulwrap => {
                 // Integers only; floats are handled above.
-                // If either of the operands are zero, the result is zero.
-                // If either of the operands are one, the result is the other
-                // operand, even if it is undefined.
-                // If either of the operands are undefined, the result is undefined.
+                // If either of the operands are zero, result is zero.
+                // If either of the operands are one, result is the other operand.
+                // If either of the operands are undefined, result is undefined.
                 if (maybe_lhs_val) |lhs_val| {
                     if (!lhs_val.isUndef()) {
                         if (lhs_val.compareWithZero(.eq)) {
@@ -6684,6 +6646,42 @@ fn analyzeArithmetic(
                     } else break :rs .{ .src = lhs_src, .air_tag = .mulwrap };
                 } else break :rs .{ .src = rhs_src, .air_tag = .mulwrap };
             },
+            .mul_sat => {
+                // For both integers and floats:
+                // If either of the operands are zero, result is zero.
+                // If either of the operands are one, result is the other operand.
+                // If either of the operands are undefined, result is undefined.
+                if (maybe_lhs_val) |lhs_val| {
+                    if (!lhs_val.isUndef()) {
+                        if (lhs_val.compareWithZero(.eq)) {
+                            return sema.addConstant(scalar_type, Value.zero);
+                        }
+                        if (lhs_val.compare(.eq, Value.one, scalar_type)) {
+                            return casted_rhs;
+                        }
+                    }
+                }
+                if (maybe_rhs_val) |rhs_val| {
+                    if (rhs_val.isUndef()) {
+                        return sema.addConstUndef(scalar_type);
+                    }
+                    if (rhs_val.compareWithZero(.eq)) {
+                        return sema.addConstant(scalar_type, Value.zero);
+                    }
+                    if (rhs_val.compare(.eq, Value.one, scalar_type)) {
+                        return casted_lhs;
+                    }
+                    if (maybe_lhs_val) |lhs_val| {
+                        if (lhs_val.isUndef()) {
+                            return sema.addConstUndef(scalar_type);
+                        }
+                        return sema.addConstant(
+                            scalar_type,
+                            try lhs_val.numberMulSat(rhs_val, scalar_type, sema.arena, target),
+                        );
+                    } else break :rs .{ .src = lhs_src, .air_tag = .mul_sat };
+                } else break :rs .{ .src = rhs_src, .air_tag = .mul_sat };
+            },
             .mod_rem => {
                 // For integers:
                 // Either operand being undef is a compile error because there exists
@@ -7933,7 +7931,7 @@ fn analyzeRet(
 fn floatOpAllowed(tag: Zir.Inst.Tag) bool {
     // extend this swich as additional operators are implemented
     return switch (tag) {
-        .add, .sub, .mul, .div, .mod, .rem, .mod_rem => true,
+        .add, .add_sat, .sub, .sub_sat, .mul, .mul_sat, .div, .mod, .rem, .mod_rem => true,
         else => false,
     };
 }
@@ -8600,12 +8598,6 @@ fn zirDivTrunc(sema: *Sema, block: *Scope.Block, inst: Zir.Inst.Index) CompileEr
     return sema.mod.fail(&block.base, src, "TODO: Sema.zirDivTrunc", .{});
 }
 
-fn zirShlExact(sema: *Sema, block: *Scope.Block, inst: Zir.Inst.Index) CompileError!Air.Inst.Ref {
-    const inst_data = sema.code.instructions.items(.data)[inst].pl_node;
-    const src = inst_data.src();
-    return sema.mod.fail(&block.base, src, "TODO: Sema.zirShlExact", .{});
-}
-
 fn zirShrExact(sema: *Sema, block: *Scope.Block, inst: Zir.Inst.Index) CompileError!Air.Inst.Ref {
     const inst_data = sema.code.instructions.items(.data)[inst].pl_node;
     const src = inst_data.src();
src/value.zig
@@ -1588,6 +1588,35 @@ pub const Value = extern union {
         return result;
     }
 
+    /// Supports both floats and ints; handles undefined.
+    pub fn numberAddSat(
+        lhs: Value,
+        rhs: Value,
+        ty: Type,
+        arena: *Allocator,
+        target: Target,
+    ) !Value {
+        if (lhs.isUndef() or rhs.isUndef()) return Value.initTag(.undef);
+
+        if (ty.isAnyFloat()) {
+            // TODO: handle outside float range
+            return floatAdd(lhs, rhs, ty, arena);
+        }
+        const result = try intAdd(lhs, rhs, arena);
+
+        const max = try ty.maxInt(arena, target);
+        if (compare(result, .gt, max, ty)) {
+            return max;
+        }
+
+        const min = try ty.minInt(arena, target);
+        if (compare(result, .lt, min, ty)) {
+            return min;
+        }
+
+        return result;
+    }
+
     /// Supports both floats and ints; handles undefined.
     pub fn numberSubWrap(
         lhs: Value,
@@ -1616,6 +1645,35 @@ pub const Value = extern union {
         return result;
     }
 
+    /// Supports both floats and ints; handles undefined.
+    pub fn numberSubSat(
+        lhs: Value,
+        rhs: Value,
+        ty: Type,
+        arena: *Allocator,
+        target: Target,
+    ) !Value {
+        if (lhs.isUndef() or rhs.isUndef()) return Value.initTag(.undef);
+
+        if (ty.isAnyFloat()) {
+            // TODO: handle outside float range
+            return floatSub(lhs, rhs, ty, arena);
+        }
+        const result = try intSub(lhs, rhs, arena);
+
+        const max = try ty.maxInt(arena, target);
+        if (compare(result, .gt, max, ty)) {
+            return max;
+        }
+
+        const min = try ty.minInt(arena, target);
+        if (compare(result, .lt, min, ty)) {
+            return min;
+        }
+
+        return result;
+    }
+
     /// Supports both floats and ints; handles undefined.
     pub fn numberMulWrap(
         lhs: Value,
@@ -1644,6 +1702,35 @@ pub const Value = extern union {
         return result;
     }
 
+    /// Supports both floats and ints; handles undefined.
+    pub fn numberMulSat(
+        lhs: Value,
+        rhs: Value,
+        ty: Type,
+        arena: *Allocator,
+        target: Target,
+    ) !Value {
+        if (lhs.isUndef() or rhs.isUndef()) return Value.initTag(.undef);
+
+        if (ty.isAnyFloat()) {
+            // TODO: handle outside float range
+            return floatMul(lhs, rhs, ty, arena);
+        }
+        const result = try intMul(lhs, rhs, arena);
+
+        const max = try ty.maxInt(arena, target);
+        if (compare(result, .gt, max, ty)) {
+            return max;
+        }
+
+        const min = try ty.minInt(arena, target);
+        if (compare(result, .lt, min, ty)) {
+            return min;
+        }
+
+        return result;
+    }
+
     /// Supports both floats and ints; handles undefined.
     pub fn numberMax(lhs: Value, rhs: Value, arena: *Allocator) !Value {
         if (lhs.isUndef() or rhs.isUndef()) return Value.initTag(.undef);
src/Zir.zig
@@ -126,6 +126,64 @@ pub const Inst = struct {
         /// Twos complement wrapping integer addition.
         /// Uses the `pl_node` union field. Payload is `Bin`.
         addwrap,
+        /// Saturating addition.
+        /// Uses the `pl_node` union field. Payload is `Bin`.
+        add_sat,
+        /// Arithmetic subtraction. Asserts no integer overflow.
+        /// Uses the `pl_node` union field. Payload is `Bin`.
+        sub,
+        /// Twos complement wrapping integer subtraction.
+        /// Uses the `pl_node` union field. Payload is `Bin`.
+        subwrap,
+        /// Saturating subtraction.
+        /// Uses the `pl_node` union field. Payload is `Bin`.
+        sub_sat,
+        /// Arithmetic multiplication. Asserts no integer overflow.
+        /// Uses the `pl_node` union field. Payload is `Bin`.
+        mul,
+        /// Twos complement wrapping integer multiplication.
+        /// Uses the `pl_node` union field. Payload is `Bin`.
+        mulwrap,
+        /// Saturating multiplication.
+        /// Uses the `pl_node` union field. Payload is `Bin`.
+        mul_sat,
+        /// Implements the `@divExact` builtin.
+        /// Uses the `pl_node` union field with payload `Bin`.
+        div_exact,
+        /// Implements the `@divFloor` builtin.
+        /// Uses the `pl_node` union field with payload `Bin`.
+        div_floor,
+        /// Implements the `@divTrunc` builtin.
+        /// Uses the `pl_node` union field with payload `Bin`.
+        div_trunc,
+        /// Implements the `@mod` builtin.
+        /// Uses the `pl_node` union field with payload `Bin`.
+        mod,
+        /// Implements the `@rem` builtin.
+        /// Uses the `pl_node` union field with payload `Bin`.
+        rem,
+        /// Ambiguously remainder division or modulus. If the computation would possibly have
+        /// a different value depending on whether the operation is remainder division or modulus,
+        /// a compile error is emitted. Otherwise the computation is performed.
+        /// Uses the `pl_node` union field. Payload is `Bin`.
+        mod_rem,
+        /// Integer shift-left. Zeroes are shifted in from the right hand side.
+        /// Uses the `pl_node` union field. Payload is `Bin`.
+        shl,
+        /// Implements the `@shlExact` builtin.
+        /// Uses the `pl_node` union field with payload `Bin`.
+        shl_exact,
+        /// Saturating shift-left.
+        /// Uses the `pl_node` union field. Payload is `Bin`.
+        shl_sat,
+        /// Integer shift-right. Arithmetic or logical depending on the signedness of
+        /// the integer type.
+        /// Uses the `pl_node` union field. Payload is `Bin`.
+        shr,
+        /// Implements the `@shrExact` builtin.
+        /// Uses the `pl_node` union field with payload `Bin`.
+        shr_exact,
+
         /// Declares a parameter of the current function. Used for:
         /// * debug info
         /// * checking shadowing against declarations in the current namespace
@@ -471,12 +529,6 @@ pub const Inst = struct {
         /// String Literal. Makes an anonymous Decl and then takes a pointer to it.
         /// Uses the `str` union field.
         str,
-        /// Arithmetic subtraction. Asserts no integer overflow.
-        /// Uses the `pl_node` union field. Payload is `Bin`.
-        sub,
-        /// Twos complement wrapping integer subtraction.
-        /// Uses the `pl_node` union field. Payload is `Bin`.
-        subwrap,
         /// Arithmetic negation. Asserts no integer overflow.
         /// Same as sub with a lhs of 0, split into a separate instruction to save memory.
         /// Uses `un_node`.
@@ -802,46 +854,6 @@ pub const Inst = struct {
         /// Implements the `@bitReverse` builtin. Uses the `un_node` union field.
         bit_reverse,
 
-        /// Implements the `@divExact` builtin.
-        /// Uses the `pl_node` union field with payload `Bin`.
-        div_exact,
-        /// Implements the `@divFloor` builtin.
-        /// Uses the `pl_node` union field with payload `Bin`.
-        div_floor,
-        /// Implements the `@divTrunc` builtin.
-        /// Uses the `pl_node` union field with payload `Bin`.
-        div_trunc,
-        /// Implements the `@mod` builtin.
-        /// Uses the `pl_node` union field with payload `Bin`.
-        mod,
-        /// Implements the `@rem` builtin.
-        /// Uses the `pl_node` union field with payload `Bin`.
-        rem,
-        /// Ambiguously remainder division or modulus. If the computation would possibly have
-        /// a different value depending on whether the operation is remainder division or modulus,
-        /// a compile error is emitted. Otherwise the computation is performed.
-        /// Uses the `pl_node` union field. Payload is `Bin`.
-        mod_rem,
-        /// Arithmetic multiplication. Asserts no integer overflow.
-        /// Uses the `pl_node` union field. Payload is `Bin`.
-        mul,
-        /// Twos complement wrapping integer multiplication.
-        /// Uses the `pl_node` union field. Payload is `Bin`.
-        mulwrap,
-
-        /// Integer shift-left. Zeroes are shifted in from the right hand side.
-        /// Uses the `pl_node` union field. Payload is `Bin`.
-        shl,
-        /// Implements the `@shlExact` builtin.
-        /// Uses the `pl_node` union field with payload `Bin`.
-        shl_exact,
-        /// Integer shift-right. Arithmetic or logical depending on the signedness of the integer type.
-        /// Uses the `pl_node` union field. Payload is `Bin`.
-        shr,
-        /// Implements the `@shrExact` builtin.
-        /// Uses the `pl_node` union field with payload `Bin`.
-        shr_exact,
-
         /// Implements the `@bitOffsetOf` builtin.
         /// Uses the `pl_node` union field with payload `Bin`.
         bit_offset_of,
@@ -961,6 +973,7 @@ pub const Inst = struct {
                 .param_anytype_comptime,
                 .add,
                 .addwrap,
+                .add_sat,
                 .alloc,
                 .alloc_mut,
                 .alloc_comptime,
@@ -1035,8 +1048,10 @@ pub const Inst = struct {
                 .mod_rem,
                 .mul,
                 .mulwrap,
+                .mul_sat,
                 .ref,
                 .shl,
+                .shl_sat,
                 .shr,
                 .store,
                 .store_node,
@@ -1045,6 +1060,7 @@ pub const Inst = struct {
                 .str,
                 .sub,
                 .subwrap,
+                .sub_sat,
                 .negate,
                 .negate_wrap,
                 .typeof,
@@ -1218,6 +1234,14 @@ pub const Inst = struct {
             break :list std.enums.directEnumArray(Tag, Data.FieldEnum, 0, .{
                 .add = .pl_node,
                 .addwrap = .pl_node,
+                .add_sat = .pl_node,
+                .sub = .pl_node,
+                .subwrap = .pl_node,
+                .sub_sat = .pl_node,
+                .mul = .pl_node,
+                .mulwrap = .pl_node,
+                .mul_sat = .pl_node,
+
                 .param = .pl_tok,
                 .param_comptime = .pl_tok,
                 .param_anytype = .str_tok,
@@ -1297,8 +1321,6 @@ pub const Inst = struct {
                 .repeat_inline = .node,
                 .merge_error_sets = .pl_node,
                 .mod_rem = .pl_node,
-                .mul = .pl_node,
-                .mulwrap = .pl_node,
                 .ref = .un_tok,
                 .ret_node = .un_node,
                 .ret_load = .un_node,
@@ -1315,8 +1337,6 @@ pub const Inst = struct {
                 .store_to_block_ptr = .bin,
                 .store_to_inferred_ptr = .bin,
                 .str = .str,
-                .sub = .pl_node,
-                .subwrap = .pl_node,
                 .negate = .un_node,
                 .negate_wrap = .un_node,
                 .typeof = .un_node,
@@ -1437,6 +1457,7 @@ pub const Inst = struct {
 
                 .shl = .pl_node,
                 .shl_exact = .pl_node,
+                .shl_sat = .pl_node,
                 .shr = .pl_node,
                 .shr_exact = .pl_node,
 
@@ -1593,22 +1614,6 @@ pub const Inst = struct {
         wasm_memory_size,
         /// `operand` is payload index to `BinNode`.
         wasm_memory_grow,
-        /// Implements the `@addWithSaturation` builtin.
-        /// `operand` is payload index to `SaturatingArithmetic`.
-        /// `small` is unused.
-        add_with_saturation,
-        /// Implements the `@subWithSaturation` builtin.
-        /// `operand` is payload index to `SaturatingArithmetic`.
-        /// `small` is unused.
-        sub_with_saturation,
-        /// Implements the `@mulWithSaturation` builtin.
-        /// `operand` is payload index to `SaturatingArithmetic`.
-        /// `small` is unused.
-        mul_with_saturation,
-        /// Implements the `@shlWithSaturation` builtin.
-        /// `operand` is payload index to `SaturatingArithmetic`.
-        /// `small` is unused.
-        shl_with_saturation,
 
         pub const InstData = struct {
             opcode: Extended,
@@ -2788,12 +2793,6 @@ pub const Inst = struct {
         ptr: Ref,
     };
 
-    pub const SaturatingArithmetic = struct {
-        node: i32,
-        lhs: Ref,
-        rhs: Ref,
-    };
-
     pub const Cmpxchg = struct {
         ptr: Ref,
         expected_value: Ref,
test/behavior/saturating_arithmetic.zig
@@ -32,7 +32,7 @@ fn testSaturatingOp(comptime op: Op, comptime T: type, test_data: [3]T) !void {
     }
 }
 
-test "@addWithSaturation" {
+test "saturating add" {
     const S = struct {
         fn doTheTest() !void {
             //                             .{a, b, expected a+b}
@@ -50,22 +50,16 @@ test "@addWithSaturation" {
             try testSaturatingOp(.add, u128, .{ maxInt(u128), 1, maxInt(u128) });
 
             const u8x3 = std.meta.Vector(3, u8);
-            try expectEqual(u8x3{ 255, 255, 255 }, @addWithSaturation(
-                u8x3{ 255, 254, 1 },
-                u8x3{ 1, 2, 255 },
-            ));
+            try expectEqual(u8x3{ 255, 255, 255 }, (u8x3{ 255, 254, 1 } +| u8x3{ 1, 2, 255 }));
             const i8x3 = std.meta.Vector(3, i8);
-            try expectEqual(i8x3{ 127, 127, 127 }, @addWithSaturation(
-                i8x3{ 127, 126, 1 },
-                i8x3{ 1, 2, 127 },
-            ));
+            try expectEqual(i8x3{ 127, 127, 127 }, (i8x3{ 127, 126, 1 } +| i8x3{ 1, 2, 127 }));
         }
     };
     try S.doTheTest();
     comptime try S.doTheTest();
 }
 
-test "@subWithSaturation" {
+test "saturating subtraction" {
     const S = struct {
         fn doTheTest() !void {
             //                             .{a, b, expected a-b}
@@ -81,17 +75,14 @@ test "@subWithSaturation" {
             try testSaturatingOp(.sub, u128, .{ 0, maxInt(u128), 0 });
 
             const u8x3 = std.meta.Vector(3, u8);
-            try expectEqual(u8x3{ 0, 0, 0 }, @subWithSaturation(
-                u8x3{ 0, 0, 0 },
-                u8x3{ 255, 255, 255 },
-            ));
+            try expectEqual(u8x3{ 0, 0, 0 }, (u8x3{ 0, 0, 0 } -| u8x3{ 255, 255, 255 }));
         }
     };
     try S.doTheTest();
     comptime try S.doTheTest();
 }
 
-test "@mulWithSaturation" {
+test "saturating multiplication" {
     // TODO: once #9660 has been solved, remove this line
     if (std.builtin.target.cpu.arch == .wasm32) return error.SkipZigTest;
 
@@ -112,10 +103,7 @@ test "@mulWithSaturation" {
             try testSaturatingOp(.mul, u128, .{ maxInt(u128), maxInt(u128), maxInt(u128) });
 
             const u8x3 = std.meta.Vector(3, u8);
-            try expectEqual(u8x3{ 255, 255, 255 }, @mulWithSaturation(
-                u8x3{ 2, 2, 2 },
-                u8x3{ 255, 255, 255 },
-            ));
+            try expectEqual(u8x3{ 255, 255, 255 }, (u8x3{ 2, 2, 2 } *| u8x3{ 255, 255, 255 }));
         }
     };
 
@@ -123,7 +111,7 @@ test "@mulWithSaturation" {
     comptime try S.doTheTest();
 }
 
-test "@shlWithSaturation" {
+test "saturating shift-left" {
     const S = struct {
         fn doTheTest() !void {
             //                             .{a, b, expected a<<b}
@@ -140,10 +128,7 @@ test "@shlWithSaturation" {
             try testSaturatingOp(.shl, u8, .{ 255, 1, 255 });
 
             const u8x3 = std.meta.Vector(3, u8);
-            try expectEqual(u8x3{ 255, 255, 255 }, @shlWithSaturation(
-                u8x3{ 255, 255, 255 },
-                u8x3{ 1, 1, 1 },
-            ));
+            try expectEqual(u8x3{ 255, 255, 255 }, (u8x3{ 255, 255, 255 } <<| u8x3{ 1, 1, 1 }));
         }
     };
     try S.doTheTest();