Commit 29f41896ed

Travis Staloch <twostepted@gmail.com>
2021-09-02 22:50:24
sat-arithmetic: add operator support
- adds initial support for the operators +|, -|, *|, <<|, +|=, -|=, *|=, <<|= - uses operators in addition to builtins in behavior test - adds binOpExt() and assignBinOpExt() to AstGen.zig. these need to be audited
1 parent 79bc589
lib/std/zig/Ast.zig
@@ -396,6 +396,7 @@ pub fn firstToken(tree: Tree, node: Node.Index) TokenIndex {
         .assign_add,
         .assign_sub,
         .assign_bit_shift_left,
+        .assign_bit_shift_left_sat,
         .assign_bit_shift_right,
         .assign_bit_and,
         .assign_bit_xor,
@@ -403,6 +404,9 @@ pub fn firstToken(tree: Tree, node: Node.Index) TokenIndex {
         .assign_mul_wrap,
         .assign_add_wrap,
         .assign_sub_wrap,
+        .assign_mul_sat,
+        .assign_add_sat,
+        .assign_sub_sat,
         .assign,
         .merge_error_sets,
         .mul,
@@ -410,12 +414,16 @@ pub fn firstToken(tree: Tree, node: Node.Index) TokenIndex {
         .mod,
         .array_mult,
         .mul_wrap,
+        .mul_sat,
         .add,
         .sub,
         .array_cat,
         .add_wrap,
         .sub_wrap,
+        .add_sat,
+        .sub_sat,
         .bit_shift_left,
+        .bit_shift_left_sat,
         .bit_shift_right,
         .bit_and,
         .bit_xor,
@@ -652,6 +660,7 @@ pub fn lastToken(tree: Tree, node: Node.Index) TokenIndex {
         .assign_add,
         .assign_sub,
         .assign_bit_shift_left,
+        .assign_bit_shift_left_sat,
         .assign_bit_shift_right,
         .assign_bit_and,
         .assign_bit_xor,
@@ -659,6 +668,9 @@ pub fn lastToken(tree: Tree, node: Node.Index) TokenIndex {
         .assign_mul_wrap,
         .assign_add_wrap,
         .assign_sub_wrap,
+        .assign_mul_sat,
+        .assign_add_sat,
+        .assign_sub_sat,
         .assign,
         .merge_error_sets,
         .mul,
@@ -666,12 +678,16 @@ pub fn lastToken(tree: Tree, node: Node.Index) TokenIndex {
         .mod,
         .array_mult,
         .mul_wrap,
+        .mul_sat,
         .add,
         .sub,
         .array_cat,
         .add_wrap,
         .sub_wrap,
+        .add_sat,
+        .sub_sat,
         .bit_shift_left,
+        .bit_shift_left_sat,
         .bit_shift_right,
         .bit_and,
         .bit_xor,
@@ -2525,6 +2541,8 @@ pub const Node = struct {
         assign_sub,
         /// `lhs <<= rhs`. main_token is op.
         assign_bit_shift_left,
+        /// `lhs <<|= rhs`. main_token is op.
+        assign_bit_shift_left_sat,
         /// `lhs >>= rhs`. main_token is op.
         assign_bit_shift_right,
         /// `lhs &= rhs`. main_token is op.
@@ -2539,6 +2557,12 @@ pub const Node = struct {
         assign_add_wrap,
         /// `lhs -%= rhs`. main_token is op.
         assign_sub_wrap,
+        /// `lhs *|= rhs`. main_token is op.
+        assign_mul_sat,
+        /// `lhs +|= rhs`. main_token is op.
+        assign_add_sat,
+        /// `lhs -|= rhs`. main_token is op.
+        assign_sub_sat,
         /// `lhs = rhs`. main_token is op.
         assign,
         /// `lhs || rhs`. main_token is the `||`.
@@ -2553,6 +2577,8 @@ pub const Node = struct {
         array_mult,
         /// `lhs *% rhs`. main_token is the `*%`.
         mul_wrap,
+        /// `lhs *| rhs`. main_token is the `*%`.
+        mul_sat,
         /// `lhs + rhs`. main_token is the `+`.
         add,
         /// `lhs - rhs`. main_token is the `-`.
@@ -2563,8 +2589,14 @@ pub const Node = struct {
         add_wrap,
         /// `lhs -% rhs`. main_token is the `-%`.
         sub_wrap,
+        /// `lhs +| rhs`. main_token is the `+|`.
+        add_sat,
+        /// `lhs -| rhs`. main_token is the `-|`.
+        sub_sat,
         /// `lhs << rhs`. main_token is the `<<`.
         bit_shift_left,
+        /// `lhs <<| rhs`. main_token is the `<<|`.
+        bit_shift_left_sat,
         /// `lhs >> rhs`. main_token is the `>>`.
         bit_shift_right,
         /// `lhs & rhs`. main_token is the `&`.
lib/std/zig/parse.zig
@@ -1269,6 +1269,7 @@ const Parser = struct {
             .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,
             .ampersand_equal => .assign_bit_and,
             .caret_equal => .assign_bit_xor,
@@ -1276,6 +1277,9 @@ const Parser = struct {
             .asterisk_percent_equal => .assign_mul_wrap,
             .plus_percent_equal => .assign_add_wrap,
             .minus_percent_equal => .assign_sub_wrap,
+            .asterisk_pipe_equal => .assign_mul_sat,
+            .plus_pipe_equal => .assign_add_sat,
+            .minus_pipe_equal => .assign_sub_sat,
             .equal => .assign,
             else => return expr,
         };
@@ -1343,6 +1347,7 @@ const Parser = struct {
         .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 },
 
         .plus = .{ .prec = 60, .tag = .add },
@@ -1350,6 +1355,8 @@ const Parser = struct {
         .plus_plus = .{ .prec = 60, .tag = .array_cat },
         .plus_percent = .{ .prec = 60, .tag = .add_wrap },
         .minus_percent = .{ .prec = 60, .tag = .sub_wrap },
+        .plus_pipe = .{ .prec = 60, .tag = .add_sat },
+        .minus_pipe = .{ .prec = 60, .tag = .sub_sat },
 
         .pipe_pipe = .{ .prec = 70, .tag = .merge_error_sets },
         .asterisk = .{ .prec = 70, .tag = .mul },
@@ -1357,6 +1364,7 @@ const Parser = struct {
         .percent = .{ .prec = 70, .tag = .mod },
         .asterisk_asterisk = .{ .prec = 70, .tag = .array_mult },
         .asterisk_percent = .{ .prec = 70, .tag = .mul_wrap },
+        .asterisk_pipe = .{ .prec = 70, .tag = .mul_sat },
     });
 
     fn parseExprPrecedence(p: *Parser, min_prec: i32) Error!Node.Index {
lib/std/zig/render.zig
@@ -333,26 +333,32 @@ fn renderExpression(gpa: *Allocator, ais: *Ais, tree: Ast, node: Ast.Node.Index,
 
         .add,
         .add_wrap,
+        .add_sat,
         .array_cat,
         .array_mult,
         .assign,
         .assign_bit_and,
         .assign_bit_or,
         .assign_bit_shift_left,
+        .assign_bit_shift_left_sat,
         .assign_bit_shift_right,
         .assign_bit_xor,
         .assign_div,
         .assign_sub,
         .assign_sub_wrap,
+        .assign_sub_sat,
         .assign_mod,
         .assign_add,
         .assign_add_wrap,
+        .assign_add_sat,
         .assign_mul,
         .assign_mul_wrap,
+        .assign_mul_sat,
         .bang_equal,
         .bit_and,
         .bit_or,
         .bit_shift_left,
+        .bit_shift_left_sat,
         .bit_shift_right,
         .bit_xor,
         .bool_and,
@@ -367,8 +373,10 @@ fn renderExpression(gpa: *Allocator, ais: *Ais, tree: Ast, node: Ast.Node.Index,
         .mod,
         .mul,
         .mul_wrap,
+        .mul_sat,
         .sub,
         .sub_wrap,
+        .sub_sat,
         .@"orelse",
         => {
             const infix = datas[node];
lib/std/zig/tokenizer.zig
@@ -103,15 +103,21 @@ pub const Token = struct {
         plus_equal,
         plus_percent,
         plus_percent_equal,
+        plus_pipe,
+        plus_pipe_equal,
         minus,
         minus_equal,
         minus_percent,
         minus_percent_equal,
+        minus_pipe,
+        minus_pipe_equal,
         asterisk,
         asterisk_equal,
         asterisk_asterisk,
         asterisk_percent,
         asterisk_percent_equal,
+        asterisk_pipe,
+        asterisk_pipe_equal,
         arrow,
         colon,
         slash,
@@ -124,6 +130,8 @@ pub const Token = struct {
         angle_bracket_left_equal,
         angle_bracket_angle_bracket_left,
         angle_bracket_angle_bracket_left_equal,
+        angle_bracket_angle_bracket_left_pipe,
+        angle_bracket_angle_bracket_left_pipe_equal,
         angle_bracket_right,
         angle_bracket_right_equal,
         angle_bracket_angle_bracket_right,
@@ -227,15 +235,21 @@ pub const Token = struct {
                 .plus_equal => "+=",
                 .plus_percent => "+%",
                 .plus_percent_equal => "+%=",
+                .plus_pipe => "+|",
+                .plus_pipe_equal => "+|=",
                 .minus => "-",
                 .minus_equal => "-=",
                 .minus_percent => "-%",
                 .minus_percent_equal => "-%=",
+                .minus_pipe => "-|",
+                .minus_pipe_equal => "-|=",
                 .asterisk => "*",
                 .asterisk_equal => "*=",
                 .asterisk_asterisk => "**",
                 .asterisk_percent => "*%",
                 .asterisk_percent_equal => "*%=",
+                .asterisk_pipe => "*|",
+                .asterisk_pipe_equal => "*|=",
                 .arrow => "->",
                 .colon => ":",
                 .slash => "/",
@@ -248,6 +262,8 @@ pub const Token = struct {
                 .angle_bracket_left_equal => "<=",
                 .angle_bracket_angle_bracket_left => "<<",
                 .angle_bracket_angle_bracket_left_equal => "<<=",
+                .angle_bracket_angle_bracket_left_pipe => "<<|",
+                .angle_bracket_angle_bracket_left_pipe_equal => "<<|=",
                 .angle_bracket_right => ">",
                 .angle_bracket_right_equal => ">=",
                 .angle_bracket_angle_bracket_right => ">>",
@@ -352,8 +368,10 @@ pub const Tokenizer = struct {
         pipe,
         minus,
         minus_percent,
+        minus_pipe,
         asterisk,
         asterisk_percent,
+        asterisk_pipe,
         slash,
         line_comment_start,
         line_comment,
@@ -382,8 +400,10 @@ pub const Tokenizer = struct {
         percent,
         plus,
         plus_percent,
+        plus_pipe,
         angle_bracket_left,
         angle_bracket_angle_bracket_left,
+        angle_bracket_angle_bracket_left_pipe,
         angle_bracket_right,
         angle_bracket_angle_bracket_right,
         period,
@@ -584,6 +604,9 @@ pub const Tokenizer = struct {
                     '%' => {
                         state = .asterisk_percent;
                     },
+                    '|' => {
+                        state = .asterisk_pipe;
+                    },
                     else => {
                         result.tag = .asterisk;
                         break;
@@ -602,6 +625,18 @@ pub const Tokenizer = struct {
                     },
                 },
 
+                .asterisk_pipe => switch (c) {
+                    '=' => {
+                        result.tag = .asterisk_pipe_equal;
+                        self.index += 1;
+                        break;
+                    },
+                    else => {
+                        result.tag = .asterisk_pipe;
+                        break;
+                    },
+                },
+
                 .percent => switch (c) {
                     '=' => {
                         result.tag = .percent_equal;
@@ -628,6 +663,9 @@ pub const Tokenizer = struct {
                     '%' => {
                         state = .plus_percent;
                     },
+                    '|' => {
+                        state = .plus_pipe;
+                    },
                     else => {
                         result.tag = .plus;
                         break;
@@ -646,6 +684,18 @@ pub const Tokenizer = struct {
                     },
                 },
 
+                .plus_pipe => switch (c) {
+                    '=' => {
+                        result.tag = .plus_pipe_equal;
+                        self.index += 1;
+                        break;
+                    },
+                    else => {
+                        result.tag = .plus_pipe;
+                        break;
+                    },
+                },
+
                 .caret => switch (c) {
                     '=' => {
                         result.tag = .caret_equal;
@@ -903,6 +953,9 @@ pub const Tokenizer = struct {
                     '%' => {
                         state = .minus_percent;
                     },
+                    '|' => {
+                        state = .minus_pipe;
+                    },
                     else => {
                         result.tag = .minus;
                         break;
@@ -920,6 +973,17 @@ pub const Tokenizer = struct {
                         break;
                     },
                 },
+                .minus_pipe => switch (c) {
+                    '=' => {
+                        result.tag = .minus_pipe_equal;
+                        self.index += 1;
+                        break;
+                    },
+                    else => {
+                        result.tag = .minus_pipe;
+                        break;
+                    },
+                },
 
                 .angle_bracket_left => switch (c) {
                     '<' => {
@@ -942,12 +1006,27 @@ pub const Tokenizer = struct {
                         self.index += 1;
                         break;
                     },
+                    '|' => {
+                        result.tag = .angle_bracket_angle_bracket_left_pipe;
+                    },
                     else => {
                         result.tag = .angle_bracket_angle_bracket_left;
                         break;
                     },
                 },
 
+                .angle_bracket_angle_bracket_left_pipe => switch (c) {
+                    '=' => {
+                        result.tag = .angle_bracket_angle_bracket_left_pipe_equal;
+                        self.index += 1;
+                        break;
+                    },
+                    else => {
+                        result.tag = .angle_bracket_angle_bracket_left_pipe;
+                        break;
+                    },
+                },
+
                 .angle_bracket_right => switch (c) {
                     '>' => {
                         state = .angle_bracket_angle_bracket_right;
src/codegen/llvm/bindings.zig
@@ -397,6 +397,12 @@ pub const Builder = opaque {
     pub const buildNUWAdd = LLVMBuildNUWAdd;
     extern fn LLVMBuildNUWAdd(*const Builder, LHS: *const Value, RHS: *const Value, Name: [*:0]const u8) *const Value;
 
+    pub const buildSAddSat = ZigLLVMBuildSAddSat;
+    extern fn ZigLLVMBuildSAddSat(*const Builder, LHS: *const Value, RHS: *const Value, Name: [*:0]const u8) *const Value;
+
+    pub const buildUAddSat = ZigLLVMBuildUAddSat;
+    extern fn ZigLLVMBuildUAddSat(*const Builder, LHS: *const Value, RHS: *const Value, Name: [*:0]const u8) *const Value;
+
     pub const buildFSub = LLVMBuildFSub;
     extern fn LLVMBuildFSub(*const Builder, LHS: *const Value, RHS: *const Value, Name: [*:0]const u8) *const Value;
 
@@ -409,6 +415,12 @@ pub const Builder = opaque {
     pub const buildNUWSub = LLVMBuildNUWSub;
     extern fn LLVMBuildNUWSub(*const Builder, LHS: *const Value, RHS: *const Value, Name: [*:0]const u8) *const Value;
 
+    pub const buildSSubSat = ZigLLVMBuildSSubSat;
+    extern fn ZigLLVMBuildSSubSat(*const Builder, LHS: *const Value, RHS: *const Value, Name: [*:0]const u8) *const Value;
+
+    pub const buildUSubSat = ZigLLVMBuildUSubSat;
+    extern fn ZigLLVMBuildUSubSat(*const Builder, LHS: *const Value, RHS: *const Value, Name: [*:0]const u8) *const Value;
+
     pub const buildFMul = LLVMBuildFMul;
     extern fn LLVMBuildFMul(*const Builder, LHS: *const Value, RHS: *const Value, Name: [*:0]const u8) *const Value;
 
@@ -421,6 +433,12 @@ pub const Builder = opaque {
     pub const buildNUWMul = LLVMBuildNUWMul;
     extern fn LLVMBuildNUWMul(*const Builder, LHS: *const Value, RHS: *const Value, Name: [*:0]const u8) *const Value;
 
+    pub const buildSMulFixSat = ZigLLVMBuildSMulFixSat;
+    extern fn ZigLLVMBuildSMulFixSat(*const Builder, LHS: *const Value, RHS: *const Value, Name: [*:0]const u8) *const Value;
+
+    pub const buildUMulFixSat = ZigLLVMBuildUMulFixSat;
+    extern fn ZigLLVMBuildUMulFixSat(*const Builder, LHS: *const Value, RHS: *const Value, Name: [*:0]const u8) *const Value;
+
     pub const buildUDiv = LLVMBuildUDiv;
     extern fn LLVMBuildUDiv(*const Builder, LHS: *const Value, RHS: *const Value, Name: [*:0]const u8) *const Value;
 
@@ -451,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 buildSShlSat = ZigLLVMBuildSShlSat;
+    extern fn ZigLLVMBuildSShlSat(*const Builder, LHS: *const Value, RHS: *const Value, Name: [*:0]const u8) *const Value;
+
+    pub const buildUShlSat = ZigLLVMBuildUShlSat;
+    extern fn ZigLLVMBuildUShlSat(*const Builder, LHS: *const Value, RHS: *const Value, Name: [*:0]const u8) *const Value;
+
     pub const buildOr = LLVMBuildOr;
     extern fn LLVMBuildOr(*const Builder, LHS: *const Value, RHS: *const Value, Name: [*:0]const u8) *const Value;
 
src/codegen/c.zig
@@ -885,14 +885,17 @@ fn genBody(f: *Function, body: []const Air.Inst.Index) error{ AnalysisFail, OutO
             // that wrapping is UB.
             .add, .ptr_add => try airBinOp( f, inst, " + "),
             .addwrap       => try airWrapOp(f, inst, " + ", "addw_"),
+            .addsat        => return o.dg.fail("TODO: C backend: implement codegen for addsat", .{}),
             // 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 o.dg.fail("TODO: C backend: implement codegen for subsat", .{}),
             // 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 o.dg.fail("TODO: C backend: implement codegen for mulsat", .{}),
             // TODO use a different strategy for div that communicates to the optimizer
             // that wrapping is UB.
             .div           => try airBinOp( f, inst, " / "),
src/codegen/llvm.zig
@@ -1236,12 +1236,15 @@ 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, false),
-                .addwrap => try self.airAdd(inst, true),
-                .sub     => try self.airSub(inst, false),
-                .subwrap => try self.airSub(inst, true),
-                .mul     => try self.airMul(inst, false),
-                .mulwrap => try self.airMul(inst, true),
+                .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),
@@ -1252,7 +1255,8 @@ pub const FuncGen = struct {
                 .bit_or, .bool_or   => try self.airOr(inst),
                 .xor                => try self.airXor(inst),
 
-                .shl                => try self.airShl(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),
@@ -2024,7 +2028,8 @@ pub const FuncGen = struct {
         return self.todo("implement llvm codegen for 'airWrapErrUnionErr'", .{});
     }
 
-    fn airAdd(self: *FuncGen, inst: Air.Inst.Index, wrap: bool) !?*const llvm.Value {
+    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;
 
@@ -2033,13 +2038,20 @@ pub const FuncGen = struct {
         const rhs = try self.resolveInst(bin_op.rhs);
         const inst_ty = self.air.typeOfIndex(inst);
 
-        if (inst_ty.isRuntimeFloat()) return self.builder.buildFAdd(lhs, rhs, "");
-        if (wrap) return self.builder.buildAdd(lhs, rhs, "");
+        if (inst_ty.isFloat()) 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, wrap: bool) !?*const llvm.Value {
+    fn airSub(self: *FuncGen, inst: Air.Inst.Index, ty: ArithmeticType) !?*const llvm.Value {
         if (self.liveness.isUnused(inst))
             return null;
 
@@ -2048,13 +2060,20 @@ pub const FuncGen = struct {
         const rhs = try self.resolveInst(bin_op.rhs);
         const inst_ty = self.air.typeOfIndex(inst);
 
-        if (inst_ty.isRuntimeFloat()) return self.builder.buildFSub(lhs, rhs, "");
-        if (wrap) return self.builder.buildSub(lhs, rhs, "");
+        if (inst_ty.isFloat()) 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, wrap: bool) !?*const llvm.Value {
+    fn airMul(self: *FuncGen, inst: Air.Inst.Index, ty: ArithmeticType) !?*const llvm.Value {
         if (self.liveness.isUnused(inst))
             return null;
 
@@ -2063,8 +2082,15 @@ pub const FuncGen = struct {
         const rhs = try self.resolveInst(bin_op.rhs);
         const inst_ty = self.air.typeOfIndex(inst);
 
-        if (inst_ty.isRuntimeFloat()) return self.builder.buildFMul(lhs, rhs, "");
-        if (wrap) return self.builder.buildMul(lhs, rhs, "");
+        if (inst_ty.isFloat()) 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, "");
     }
@@ -2174,7 +2200,7 @@ pub const FuncGen = struct {
         return self.builder.buildXor(lhs, rhs, "");
     }
 
-    fn airShl(self: *FuncGen, inst: Air.Inst.Index) !?*const llvm.Value {
+    fn airShl(self: *FuncGen, inst: Air.Inst.Index, sat: bool) !?*const llvm.Value {
         if (self.liveness.isUnused(inst))
             return null;
         const bin_op = self.air.instructions.items(.data)[inst].bin_op;
@@ -2186,6 +2212,12 @@ 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, "");
     }
 
src/stage1/all_types.hpp
@@ -812,14 +812,18 @@ enum BinOpType {
     BinOpTypeInvalid,
     BinOpTypeAssign,
     BinOpTypeAssignTimes,
+    BinOpTypeAssignTimesSat,
     BinOpTypeAssignTimesWrap,
     BinOpTypeAssignDiv,
     BinOpTypeAssignMod,
     BinOpTypeAssignPlus,
+    BinOpTypeAssignPlusSat,
     BinOpTypeAssignPlusWrap,
     BinOpTypeAssignMinus,
+    BinOpTypeAssignMinusSat,
     BinOpTypeAssignMinusWrap,
     BinOpTypeAssignBitShiftLeft,
+    BinOpTypeAssignBitShiftLeftSat,
     BinOpTypeAssignBitShiftRight,
     BinOpTypeAssignBitAnd,
     BinOpTypeAssignBitXor,
@@ -836,12 +840,16 @@ enum BinOpType {
     BinOpTypeBinXor,
     BinOpTypeBinAnd,
     BinOpTypeBitShiftLeft,
+    BinOpTypeBitShiftLeftSat,
     BinOpTypeBitShiftRight,
     BinOpTypeAdd,
+    BinOpTypeAddSat,
     BinOpTypeAddWrap,
     BinOpTypeSub,
+    BinOpTypeSubSat,
     BinOpTypeSubWrap,
     BinOpTypeMult,
+    BinOpTypeMultSat,
     BinOpTypeMultWrap,
     BinOpTypeDiv,
     BinOpTypeMod,
@@ -2958,10 +2966,10 @@ enum IrBinOp {
     IrBinOpArrayMult,
     IrBinOpMaximum,
     IrBinOpMinimum,
-    IrBinOpSatAdd,
-    IrBinOpSatSub,
-    IrBinOpSatMul,
-    IrBinOpSatShl,
+    IrBinOpAddSat,
+    IrBinOpSubSat,
+    IrBinOpMultSat,
+    IrBinOpShlSat,
 };
 
 struct Stage1ZirInstBinOp {
src/stage1/astgen.cpp
@@ -3672,6 +3672,8 @@ static Stage1ZirInst *astgen_bin_op(Stage1AstGen *ag, Scope *scope, AstNode *nod
             return ir_lval_wrap(ag, scope, astgen_assign_op(ag, scope, node, IrBinOpMult), lval, result_loc);
         case BinOpTypeAssignTimesWrap:
             return ir_lval_wrap(ag, scope, astgen_assign_op(ag, scope, node, IrBinOpMultWrap), lval, result_loc);
+        case BinOpTypeAssignTimesSat:
+            return ir_lval_wrap(ag, scope, astgen_assign_op(ag, scope, node, IrBinOpMultSat), lval, result_loc);
         case BinOpTypeAssignDiv:
             return ir_lval_wrap(ag, scope, astgen_assign_op(ag, scope, node, IrBinOpDivUnspecified), lval, result_loc);
         case BinOpTypeAssignMod:
@@ -3680,12 +3682,18 @@ static Stage1ZirInst *astgen_bin_op(Stage1AstGen *ag, Scope *scope, AstNode *nod
             return ir_lval_wrap(ag, scope, astgen_assign_op(ag, scope, node, IrBinOpAdd), lval, result_loc);
         case BinOpTypeAssignPlusWrap:
             return ir_lval_wrap(ag, scope, astgen_assign_op(ag, scope, node, IrBinOpAddWrap), lval, result_loc);
+        case BinOpTypeAssignPlusSat:
+            return ir_lval_wrap(ag, scope, astgen_assign_op(ag, scope, node, IrBinOpAddSat), lval, result_loc);
         case BinOpTypeAssignMinus:
             return ir_lval_wrap(ag, scope, astgen_assign_op(ag, scope, node, IrBinOpSub), lval, result_loc);
         case BinOpTypeAssignMinusWrap:
             return ir_lval_wrap(ag, scope, astgen_assign_op(ag, scope, node, IrBinOpSubWrap), lval, result_loc);
+        case BinOpTypeAssignMinusSat:
+            return ir_lval_wrap(ag, scope, astgen_assign_op(ag, scope, node, IrBinOpSubSat), lval, result_loc);
         case BinOpTypeAssignBitShiftLeft:
             return ir_lval_wrap(ag, scope, astgen_assign_op(ag, scope, node, IrBinOpBitShiftLeftLossy), lval, result_loc);
+        case BinOpTypeAssignBitShiftLeftSat:
+            return ir_lval_wrap(ag, scope, astgen_assign_op(ag, scope, node, IrBinOpShlSat), lval, result_loc);
         case BinOpTypeAssignBitShiftRight:
             return ir_lval_wrap(ag, scope, astgen_assign_op(ag, scope, node, IrBinOpBitShiftRightLossy), lval, result_loc);
         case BinOpTypeAssignBitAnd:
@@ -3718,20 +3726,28 @@ static Stage1ZirInst *astgen_bin_op(Stage1AstGen *ag, Scope *scope, AstNode *nod
             return ir_lval_wrap(ag, scope, astgen_bin_op_id(ag, scope, node, IrBinOpBinAnd), lval, result_loc);
         case BinOpTypeBitShiftLeft:
             return ir_lval_wrap(ag, scope, astgen_bin_op_id(ag, scope, node, IrBinOpBitShiftLeftLossy), lval, result_loc);
+        case BinOpTypeBitShiftLeftSat:
+            return ir_lval_wrap(ag, scope, astgen_bin_op_id(ag, scope, node, IrBinOpShlSat), lval, result_loc);
         case BinOpTypeBitShiftRight:
             return ir_lval_wrap(ag, scope, astgen_bin_op_id(ag, scope, node, IrBinOpBitShiftRightLossy), lval, result_loc);
         case BinOpTypeAdd:
             return ir_lval_wrap(ag, scope, astgen_bin_op_id(ag, scope, node, IrBinOpAdd), lval, result_loc);
         case BinOpTypeAddWrap:
             return ir_lval_wrap(ag, scope, astgen_bin_op_id(ag, scope, node, IrBinOpAddWrap), lval, result_loc);
+        case BinOpTypeAddSat:
+            return ir_lval_wrap(ag, scope, astgen_bin_op_id(ag, scope, node, IrBinOpAddSat), lval, result_loc);
         case BinOpTypeSub:
             return ir_lval_wrap(ag, scope, astgen_bin_op_id(ag, scope, node, IrBinOpSub), lval, result_loc);
         case BinOpTypeSubWrap:
             return ir_lval_wrap(ag, scope, astgen_bin_op_id(ag, scope, node, IrBinOpSubWrap), lval, result_loc);
+        case BinOpTypeSubSat:
+            return ir_lval_wrap(ag, scope, astgen_bin_op_id(ag, scope, node, IrBinOpSubSat), lval, result_loc);
         case BinOpTypeMult:
             return ir_lval_wrap(ag, scope, astgen_bin_op_id(ag, scope, node, IrBinOpMult), lval, result_loc);
         case BinOpTypeMultWrap:
             return ir_lval_wrap(ag, scope, astgen_bin_op_id(ag, scope, node, IrBinOpMultWrap), lval, result_loc);
+        case BinOpTypeMultSat:
+            return ir_lval_wrap(ag, scope, astgen_bin_op_id(ag, scope, node, IrBinOpMultSat), lval, result_loc);
         case BinOpTypeDiv:
             return ir_lval_wrap(ag, scope, astgen_bin_op_id(ag, scope, node, IrBinOpDivUnspecified), lval, result_loc);
         case BinOpTypeMod:
@@ -4716,7 +4732,7 @@ static Stage1ZirInst *astgen_builtin_fn_call(Stage1AstGen *ag, Scope *scope, Ast
                 if (arg1_value == ag->codegen->invalid_inst_src)
                     return arg1_value;
 
-                Stage1ZirInst *bin_op = ir_build_bin_op(ag, scope, node, IrBinOpSatAdd, arg0_value, arg1_value, true);
+                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:
@@ -4731,7 +4747,7 @@ static Stage1ZirInst *astgen_builtin_fn_call(Stage1AstGen *ag, Scope *scope, Ast
                 if (arg1_value == ag->codegen->invalid_inst_src)
                     return arg1_value;
 
-                Stage1ZirInst *bin_op = ir_build_bin_op(ag, scope, node, IrBinOpSatSub, arg0_value, arg1_value, true);
+                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:
@@ -4746,7 +4762,7 @@ static Stage1ZirInst *astgen_builtin_fn_call(Stage1AstGen *ag, Scope *scope, Ast
                 if (arg1_value == ag->codegen->invalid_inst_src)
                     return arg1_value;
 
-                Stage1ZirInst *bin_op = ir_build_bin_op(ag, scope, node, IrBinOpSatMul, arg0_value, arg1_value, true);
+                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:
@@ -4761,7 +4777,7 @@ static Stage1ZirInst *astgen_builtin_fn_call(Stage1AstGen *ag, Scope *scope, Ast
                 if (arg1_value == ag->codegen->invalid_inst_src)
                     return arg1_value;
 
-                Stage1ZirInst *bin_op = ir_build_bin_op(ag, scope, node, IrBinOpSatShl, arg0_value, arg1_value, true);
+                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:
src/stage1/codegen.cpp
@@ -3333,7 +3333,7 @@ static LLVMValueRef ir_render_bin_op(CodeGen *g, Stage1Air *executable,
             } else {
                 zig_unreachable();
             }
-        case IrBinOpSatAdd:
+        case IrBinOpAddSat:
             if (scalar_type->id == ZigTypeIdInt) {
                 if (scalar_type->data.integral.is_signed) {
                     return ZigLLVMBuildSAddSat(g->builder, op1_value, op2_value, "");
@@ -3343,7 +3343,7 @@ static LLVMValueRef ir_render_bin_op(CodeGen *g, Stage1Air *executable,
             } else {
                 zig_unreachable();
             }
-        case IrBinOpSatSub:
+        case IrBinOpSubSat:
             if (scalar_type->id == ZigTypeIdInt) {
                 if (scalar_type->data.integral.is_signed) {
                     return ZigLLVMBuildSSubSat(g->builder, op1_value, op2_value, "");
@@ -3353,7 +3353,7 @@ static LLVMValueRef ir_render_bin_op(CodeGen *g, Stage1Air *executable,
             } else {
                 zig_unreachable();
             }
-        case IrBinOpSatMul:
+        case IrBinOpMultSat:
             if (scalar_type->id == ZigTypeIdInt) {
                 if (scalar_type->data.integral.is_signed) {
                     return ZigLLVMBuildSMulFixSat(g->builder, op1_value, op2_value, "");
@@ -3363,7 +3363,7 @@ static LLVMValueRef ir_render_bin_op(CodeGen *g, Stage1Air *executable,
             } else {
                 zig_unreachable();
             }
-        case IrBinOpSatShl:
+        case IrBinOpShlSat:
             if (scalar_type->id == ZigTypeIdInt) {
                 if (scalar_type->data.integral.is_signed) {
                     return ZigLLVMBuildSShlSat(g->builder, op1_value, op2_value, "");
src/stage1/ir.cpp
@@ -9820,28 +9820,28 @@ static ErrorMsg *ir_eval_math_op_scalar(IrAnalyze *ira, Scope *scope, AstNode *s
                 float_min(out_val, op1_val, op2_val);
             }
             break;
-        case IrBinOpSatAdd:
+        case IrBinOpAddSat:
             if (is_int) {
                 bigint_add_sat(&out_val->data.x_bigint, &op1_val->data.x_bigint, &op2_val->data.x_bigint, type_entry->data.integral.bit_count, type_entry->data.integral.is_signed);
             } else {
                 zig_unreachable();
             }
             break;
-        case IrBinOpSatSub:
+        case IrBinOpSubSat:
             if (is_int) {
                 bigint_sub_sat(&out_val->data.x_bigint, &op1_val->data.x_bigint, &op2_val->data.x_bigint, type_entry->data.integral.bit_count, type_entry->data.integral.is_signed);
             } else {
                 zig_unreachable();
             }
             break;
-        case IrBinOpSatMul:
+        case IrBinOpMultSat:
             if (is_int) {
                 bigint_mul_sat(&out_val->data.x_bigint, &op1_val->data.x_bigint, &op2_val->data.x_bigint, type_entry->data.integral.bit_count, type_entry->data.integral.is_signed);
             } else {
                 zig_unreachable();
             }
             break;
-        case IrBinOpSatShl:
+        case IrBinOpShlSat:
             if (is_int) {
                 bigint_shl_sat(&out_val->data.x_bigint, &op1_val->data.x_bigint, &op2_val->data.x_bigint, type_entry->data.integral.bit_count, type_entry->data.integral.is_signed);
             } else {
@@ -10069,10 +10069,10 @@ static bool ok_float_op(IrBinOp op) {
         case IrBinOpBitShiftRightExact:
         case IrBinOpAddWrap:
         case IrBinOpSubWrap:
-        case IrBinOpSatAdd:
-        case IrBinOpSatSub:
-        case IrBinOpSatMul:
-        case IrBinOpSatShl:
+        case IrBinOpAddSat:
+        case IrBinOpSubSat:
+        case IrBinOpMultSat:
+        case IrBinOpShlSat:
         case IrBinOpMultWrap:
         case IrBinOpArrayCat:
         case IrBinOpArrayMult:
@@ -11046,10 +11046,10 @@ static Stage1AirInst *ir_analyze_instruction_bin_op(IrAnalyze *ira, Stage1ZirIns
         case IrBinOpRemMod:
         case IrBinOpMaximum:
         case IrBinOpMinimum:
-        case IrBinOpSatAdd:
-        case IrBinOpSatSub:
-        case IrBinOpSatMul:
-        case IrBinOpSatShl:
+        case IrBinOpAddSat:
+        case IrBinOpSubSat:
+        case IrBinOpMultSat:
+        case IrBinOpShlSat:
             return ir_analyze_bin_op_math(ira, bin_op_instruction);
         case IrBinOpArrayCat:
             return ir_analyze_array_cat(ira, bin_op_instruction);
src/stage1/ir_print.cpp
@@ -737,13 +737,13 @@ static const char *ir_bin_op_id_str(IrBinOp op_id) {
             return "@maximum";
         case IrBinOpMinimum:
             return "@minimum";
-        case IrBinOpSatAdd:
+        case IrBinOpAddSat:
             return "@addWithSaturation";
-        case IrBinOpSatSub:
+        case IrBinOpSubSat:
             return "@subWithSaturation";
-        case IrBinOpSatMul:
+        case IrBinOpMultSat:
             return "@mulWithSaturation";
-        case IrBinOpSatShl:
+        case IrBinOpShlSat:
             return "@shlWithSaturation";
     }
     zig_unreachable();
src/stage1/parser.cpp
@@ -2381,6 +2381,7 @@ static AstNode *ast_parse_switch_item(ParseContext *pc) {
 //      / PLUSEQUAL
 //      / MINUSEQUAL
 //      / LARROW2EQUAL
+//      / LARROW2PIPEEQUAL
 //      / RARROW2EQUAL
 //      / AMPERSANDEQUAL
 //      / CARETEQUAL
@@ -2388,6 +2389,9 @@ static AstNode *ast_parse_switch_item(ParseContext *pc) {
 //      / ASTERISKPERCENTEQUAL
 //      / PLUSPERCENTEQUAL
 //      / MINUSPERCENTEQUAL
+//      / ASTERISKPIPEEQUAL
+//      / PLUSPIPEEQUAL
+//      / MINUSPIPEEQUAL
 //      / EQUAL
 static AstNode *ast_parse_assign_op(ParseContext *pc) {
     // In C, we have `T arr[N] = {[i] = T{}};` but it doesn't
@@ -2396,17 +2400,21 @@ static AstNode *ast_parse_assign_op(ParseContext *pc) {
     table[TokenIdBitAndEq] = BinOpTypeAssignBitAnd;
     table[TokenIdBitOrEq] = BinOpTypeAssignBitOr;
     table[TokenIdBitShiftLeftEq] = BinOpTypeAssignBitShiftLeft;
+    table[TokenIdBitShiftLeftPipeEq] = BinOpTypeAssignBitShiftLeftSat;
     table[TokenIdBitShiftRightEq] = BinOpTypeAssignBitShiftRight;
     table[TokenIdBitXorEq] = BinOpTypeAssignBitXor;
     table[TokenIdDivEq] = BinOpTypeAssignDiv;
     table[TokenIdEq] = BinOpTypeAssign;
     table[TokenIdMinusEq] = BinOpTypeAssignMinus;
     table[TokenIdMinusPercentEq] = BinOpTypeAssignMinusWrap;
+    table[TokenIdMinusPipeEq] = BinOpTypeAssignMinusSat;
     table[TokenIdModEq] = BinOpTypeAssignMod;
     table[TokenIdPlusEq] = BinOpTypeAssignPlus;
     table[TokenIdPlusPercentEq] = BinOpTypeAssignPlusWrap;
+    table[TokenIdPlusPipeEq] = BinOpTypeAssignPlusSat;
     table[TokenIdTimesEq] = BinOpTypeAssignTimes;
     table[TokenIdTimesPercentEq] = BinOpTypeAssignTimesWrap;
+    table[TokenIdTimesPipeEq] = BinOpTypeAssignTimesSat;
 
     BinOpType op = table[pc->token_ids[pc->current_token]];
     if (op != BinOpTypeInvalid) {
@@ -2483,10 +2491,12 @@ static AstNode *ast_parse_bitwise_op(ParseContext *pc) {
 
 // BitShiftOp
 //     <- LARROW2
+//      / LARROW2PIPE
 //      / RARROW2
 static AstNode *ast_parse_bit_shift_op(ParseContext *pc) {
     BinOpType table[TokenIdCount] = {};
     table[TokenIdBitShiftLeft] = BinOpTypeBitShiftLeft;
+    table[TokenIdBitShiftLeftPipe] = BinOpTypeBitShiftLeftSat;
     table[TokenIdBitShiftRight] = BinOpTypeBitShiftRight;
 
     BinOpType op = table[pc->token_ids[pc->current_token]];
@@ -2506,6 +2516,8 @@ static AstNode *ast_parse_bit_shift_op(ParseContext *pc) {
 //      / PLUS2
 //      / PLUSPERCENT
 //      / MINUSPERCENT
+//      / PLUSPIPE
+//      / MINUSPIPE
 static AstNode *ast_parse_addition_op(ParseContext *pc) {
     BinOpType table[TokenIdCount] = {};
     table[TokenIdPlus] = BinOpTypeAdd;
@@ -2513,6 +2525,8 @@ static AstNode *ast_parse_addition_op(ParseContext *pc) {
     table[TokenIdPlusPlus] = BinOpTypeArrayCat;
     table[TokenIdPlusPercent] = BinOpTypeAddWrap;
     table[TokenIdMinusPercent] = BinOpTypeSubWrap;
+    table[TokenIdPlusPipe] = BinOpTypeAddSat;
+    table[TokenIdMinusPipe] = BinOpTypeSubSat;
 
     BinOpType op = table[pc->token_ids[pc->current_token]];
     if (op != BinOpTypeInvalid) {
@@ -2532,6 +2546,7 @@ static AstNode *ast_parse_addition_op(ParseContext *pc) {
 //      / PERCENT
 //      / ASTERISK2
 //      / ASTERISKPERCENT
+//      / ASTERISKPIPE
 static AstNode *ast_parse_multiply_op(ParseContext *pc) {
     BinOpType table[TokenIdCount] = {};
     table[TokenIdBarBar] = BinOpTypeMergeErrorSets;
@@ -2540,6 +2555,7 @@ static AstNode *ast_parse_multiply_op(ParseContext *pc) {
     table[TokenIdPercent] = BinOpTypeMod;
     table[TokenIdStarStar] = BinOpTypeArrayMult;
     table[TokenIdTimesPercent] = BinOpTypeMultWrap;
+    table[TokenIdTimesPipe] = BinOpTypeMultSat;
 
     BinOpType op = table[pc->token_ids[pc->current_token]];
     if (op != BinOpTypeInvalid) {
src/stage1/tokenizer.cpp
@@ -226,8 +226,10 @@ enum TokenizeState {
     TokenizeState_pipe,
     TokenizeState_minus,
     TokenizeState_minus_percent,
+    TokenizeState_minus_pipe,
     TokenizeState_asterisk,
     TokenizeState_asterisk_percent,
+    TokenizeState_asterisk_pipe,
     TokenizeState_slash,
     TokenizeState_line_comment_start,
     TokenizeState_line_comment,
@@ -257,8 +259,10 @@ enum TokenizeState {
     TokenizeState_percent,
     TokenizeState_plus,
     TokenizeState_plus_percent,
+    TokenizeState_plus_pipe,
     TokenizeState_angle_bracket_left,
     TokenizeState_angle_bracket_angle_bracket_left,
+    TokenizeState_angle_bracket_angle_bracket_left_pipe,
     TokenizeState_angle_bracket_right,
     TokenizeState_angle_bracket_angle_bracket_right,
     TokenizeState_period,
@@ -548,6 +552,9 @@ void tokenize(const char *source, Tokenization *out) {
                     case '%':
                         t.state = TokenizeState_asterisk_percent;
                         break;
+                    case '|':
+                        t.state = TokenizeState_asterisk_pipe;
+                        break;
                     default:
                         t.state = TokenizeState_start;
                         continue;
@@ -568,6 +575,21 @@ void tokenize(const char *source, Tokenization *out) {
                         continue;
                 }
                 break;
+            case TokenizeState_asterisk_pipe:
+                switch (c) {
+                    case 0:
+                        t.out->ids.last() = TokenIdTimesPipe;
+                        goto eof;
+                    case '=':
+                        t.out->ids.last() = TokenIdTimesPipeEq;
+                        t.state = TokenizeState_start;
+                        break;
+                    default:
+                        t.out->ids.last() = TokenIdTimesPipe;
+                        t.state = TokenizeState_start;
+                        continue;
+                }
+                break;
             case TokenizeState_percent:
                 switch (c) {
                     case 0:
@@ -596,6 +618,9 @@ void tokenize(const char *source, Tokenization *out) {
                     case '%':
                         t.state = TokenizeState_plus_percent;
                         break;
+                    case '|':
+                        t.state = TokenizeState_plus_pipe;
+                        break;
                     default:
                         t.state = TokenizeState_start;
                         continue;
@@ -616,6 +641,21 @@ void tokenize(const char *source, Tokenization *out) {
                         continue;
                 }
                 break;
+            case TokenizeState_plus_pipe:
+                switch (c) {
+                    case 0:
+                        t.out->ids.last() = TokenIdPlusPipe;
+                        goto eof;
+                    case '=':
+                        t.out->ids.last() = TokenIdPlusPipeEq;
+                        t.state = TokenizeState_start;
+                        break;
+                    default:
+                        t.out->ids.last() = TokenIdPlusPipe;
+                        t.state = TokenizeState_start;
+                        continue;
+                }
+                break;
             case TokenizeState_caret:
                 switch (c) {
                     case 0:
@@ -891,6 +931,9 @@ void tokenize(const char *source, Tokenization *out) {
                     case '%':
                         t.state = TokenizeState_minus_percent;
                         break;
+                    case '|':
+                        t.state = TokenizeState_minus_pipe;
+                        break;
                     default:
                         t.state = TokenizeState_start;
                         continue;
@@ -911,6 +954,21 @@ void tokenize(const char *source, Tokenization *out) {
                         continue;
                 }
                 break;
+            case TokenizeState_minus_pipe:
+                switch (c) {
+                    case 0:
+                        t.out->ids.last() = TokenIdMinusPipe;
+                        goto eof;
+                    case '=':
+                        t.out->ids.last() = TokenIdMinusPipeEq;
+                        t.state = TokenizeState_start;
+                        break;
+                    default:
+                        t.out->ids.last() = TokenIdMinusPipe;
+                        t.state = TokenizeState_start;
+                        continue;
+                }
+                break;
             case TokenizeState_angle_bracket_left:
                 switch (c) {
                     case 0:
@@ -936,12 +994,31 @@ void tokenize(const char *source, Tokenization *out) {
                         t.out->ids.last() = TokenIdBitShiftLeftEq;
                         t.state = TokenizeState_start;
                         break;
+                    case '|':
+                        // t.out->ids.last() = TokenIdBitShiftLeftPipe;
+                        t.state = TokenizeState_angle_bracket_angle_bracket_left_pipe;
+                        break;
                     default:
                         t.out->ids.last() = TokenIdBitShiftLeft;
                         t.state = TokenizeState_start;
                         continue;
                 }
                 break;
+            case TokenizeState_angle_bracket_angle_bracket_left_pipe:
+                switch (c) {
+                    case 0:
+                        t.out->ids.last() = TokenIdBitShiftLeftPipe;
+                        goto eof;
+                    case '=':
+                        t.out->ids.last() = TokenIdBitShiftLeftPipeEq;
+                        t.state = TokenizeState_start;
+                        break;
+                    default:
+                        t.out->ids.last() = TokenIdBitShiftLeftPipe;
+                        t.state = TokenizeState_start;
+                        continue;
+                }
+                break;
             case TokenizeState_angle_bracket_right:
                 switch (c) {
                     case 0:
@@ -1437,6 +1514,8 @@ const char * token_name(TokenId id) {
         case TokenIdBitOrEq: return "|=";
         case TokenIdBitShiftLeft: return "<<";
         case TokenIdBitShiftLeftEq: return "<<=";
+        case TokenIdBitShiftLeftPipe: return "<<|";
+        case TokenIdBitShiftLeftPipeEq: return "<<|=";
         case TokenIdBitShiftRight: return ">>";
         case TokenIdBitShiftRightEq: return ">>=";
         case TokenIdBitXorEq: return "^=";
@@ -1521,12 +1600,16 @@ const char * token_name(TokenId id) {
         case TokenIdMinusEq: return "-=";
         case TokenIdMinusPercent: return "-%";
         case TokenIdMinusPercentEq: return "-%=";
+        case TokenIdMinusPipe: return "-|";
+        case TokenIdMinusPipeEq: return "-|=";
         case TokenIdModEq: return "%=";
         case TokenIdPercent: return "%";
         case TokenIdPlus: return "+";
         case TokenIdPlusEq: return "+=";
         case TokenIdPlusPercent: return "+%";
         case TokenIdPlusPercentEq: return "+%=";
+        case TokenIdPlusPipe: return "+|";
+        case TokenIdPlusPipeEq: return "+|=";
         case TokenIdPlusPlus: return "++";
         case TokenIdRBrace: return "}";
         case TokenIdRBracket: return "]";
@@ -1542,6 +1625,8 @@ const char * token_name(TokenId id) {
         case TokenIdTimesEq: return "*=";
         case TokenIdTimesPercent: return "*%";
         case TokenIdTimesPercentEq: return "*%=";
+        case TokenIdTimesPipe: return "*|";
+        case TokenIdTimesPipeEq: return "*|=";
         case TokenIdBuiltin: return "Builtin";
         case TokenIdCount:
             zig_unreachable();
src/stage1/tokenizer.hpp
@@ -23,6 +23,8 @@ enum TokenId : uint8_t {
     TokenIdBitOrEq,
     TokenIdBitShiftLeft,
     TokenIdBitShiftLeftEq,
+    TokenIdBitShiftLeftPipe,
+    TokenIdBitShiftLeftPipeEq,
     TokenIdBitShiftRight,
     TokenIdBitShiftRightEq,
     TokenIdBitXorEq,
@@ -108,12 +110,16 @@ enum TokenId : uint8_t {
     TokenIdMinusEq,
     TokenIdMinusPercent,
     TokenIdMinusPercentEq,
+    TokenIdMinusPipe,
+    TokenIdMinusPipeEq,
     TokenIdModEq,
     TokenIdPercent,
     TokenIdPlus,
     TokenIdPlusEq,
     TokenIdPlusPercent,
     TokenIdPlusPercentEq,
+    TokenIdPlusPipe,
+    TokenIdPlusPipeEq,
     TokenIdPlusPlus,
     TokenIdRBrace,
     TokenIdRBracket,
@@ -129,6 +135,8 @@ enum TokenId : uint8_t {
     TokenIdTimesEq,
     TokenIdTimesPercent,
     TokenIdTimesPercentEq,
+    TokenIdTimesPipe,
+    TokenIdTimesPipeEq,
 
     TokenIdCount,
 };
src/Air.zig
@@ -44,6 +44,11 @@ pub const Inst = struct {
         /// is the same as both operands.
         /// Uses the `bin_op` field.
         addwrap,
+        /// Saturating integer addition. 
+        /// 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,
         /// 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.
@@ -54,6 +59,11 @@ pub const Inst = struct {
         /// is the same as both operands.
         /// Uses the `bin_op` field.
         subwrap,
+        /// Saturating integer subtraction. 
+        /// 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,
         /// 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.
@@ -64,6 +74,11 @@ pub const Inst = struct {
         /// is the same as both operands.
         /// Uses the `bin_op` field.
         mulwrap,
+        /// Saturating integer multiplication. 
+        /// 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,
         /// 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.
@@ -110,6 +125,9 @@ pub const Inst = struct {
         /// Shift left. `<<`
         /// Uses the `bin_op` field.
         shl,
+        /// Shift left saturating. `<<|`
+        /// Uses the `bin_op` field.
+        shl_sat,
         /// Bitwise XOR. `^`
         /// Uses the `bin_op` field.
         xor,
@@ -568,10 +586,13 @@ pub fn typeOfIndex(air: Air, inst: Air.Inst.Index) Type {
 
         .add,
         .addwrap,
+        .addsat,
         .sub,
         .subwrap,
+        .subsat,
         .mul,
         .mulwrap,
+        .mulsat,
         .div,
         .rem,
         .mod,
@@ -582,6 +603,7 @@ pub fn typeOfIndex(air: Air, inst: Air.Inst.Index) Type {
         .ptr_sub,
         .shr,
         .shl,
+        .shl_sat,
         => return air.typeOf(datas[inst].bin_op.lhs),
 
         .cmp_lt,
src/AstGen.zig
@@ -318,27 +318,35 @@ fn lvalExpr(gz: *GenZir, scope: *Scope, node: Ast.Node.Index) InnerError!Zir.Ins
         .assign_bit_and,
         .assign_bit_or,
         .assign_bit_shift_left,
+        .assign_bit_shift_left_sat,
         .assign_bit_shift_right,
         .assign_bit_xor,
         .assign_div,
         .assign_sub,
         .assign_sub_wrap,
+        .assign_sub_sat,
         .assign_mod,
         .assign_add,
         .assign_add_wrap,
+        .assign_add_sat,
         .assign_mul,
         .assign_mul_wrap,
+        .assign_mul_sat,
         .add,
         .add_wrap,
+        .add_sat,
         .sub,
         .sub_wrap,
+        .sub_sat,
         .mul,
         .mul_wrap,
+        .mul_sat,
         .div,
         .mod,
         .bit_and,
         .bit_or,
         .bit_shift_left,
+        .bit_shift_left_sat,
         .bit_shift_right,
         .bit_xor,
         .bang_equal,
@@ -526,6 +534,10 @@ fn expr(gz: *GenZir, scope: *Scope, rl: ResultLoc, node: Ast.Node.Index) InnerEr
             try assignShift(gz, scope, node, .shl);
             return rvalue(gz, rl, .void_value, node);
         },
+        .assign_bit_shift_left_sat => {
+            try assignBinOpExt(gz, scope, node, .shl_with_saturation);
+            return rvalue(gz, rl, .void_value, node);
+        },
         .assign_bit_shift_right => {
             try assignShift(gz, scope, node, .shr);
             return rvalue(gz, rl, .void_value, node);
@@ -555,6 +567,10 @@ fn expr(gz: *GenZir, scope: *Scope, rl: ResultLoc, node: Ast.Node.Index) InnerEr
             try assignOp(gz, scope, node, .subwrap);
             return rvalue(gz, rl, .void_value, node);
         },
+        .assign_sub_sat => {
+            try assignBinOpExt(gz, scope, node, .sub_with_saturation);
+            return rvalue(gz, rl, .void_value, node);
+        },
         .assign_mod => {
             try assignOp(gz, scope, node, .mod_rem);
             return rvalue(gz, rl, .void_value, node);
@@ -567,6 +583,10 @@ fn expr(gz: *GenZir, scope: *Scope, rl: ResultLoc, node: Ast.Node.Index) InnerEr
             try assignOp(gz, scope, node, .addwrap);
             return rvalue(gz, rl, .void_value, node);
         },
+        .assign_add_sat => {
+            try assignBinOpExt(gz, scope, node, .add_with_saturation);
+            return rvalue(gz, rl, .void_value, node);
+        },
         .assign_mul => {
             try assignOp(gz, scope, node, .mul);
             return rvalue(gz, rl, .void_value, node);
@@ -575,17 +595,25 @@ fn expr(gz: *GenZir, scope: *Scope, rl: ResultLoc, node: Ast.Node.Index) InnerEr
             try assignOp(gz, scope, node, .mulwrap);
             return rvalue(gz, rl, .void_value, node);
         },
+        .assign_mul_sat => {
+            try assignBinOpExt(gz, scope, node, .mul_with_saturation);
+            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),
+        .bit_shift_left     => return shiftOp(gz, scope, rl, node, node_datas[node].lhs, node_datas[node].rhs, .shl),
+        .bit_shift_left_sat => return binOpExt(gz, scope, rl, node, node_datas[node].lhs, node_datas[node].rhs, .shl_with_saturation),
+        .bit_shift_right    => 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 binOpExt(gz, scope, rl, node, node_datas[node].lhs, node_datas[node].rhs, .add_with_saturation),
         .sub      => return simpleBinOp(gz, scope, rl, node, .sub),
         .sub_wrap => return simpleBinOp(gz, scope, rl, node, .subwrap),
+        .sub_sat  => return binOpExt(gz, scope, rl, node, node_datas[node].lhs, node_datas[node].rhs, .sub_with_saturation),
         .mul      => return simpleBinOp(gz, scope, rl, node, .mul),
         .mul_wrap => return simpleBinOp(gz, scope, rl, node, .mulwrap),
+        .mul_sat  => return binOpExt(gz, scope, rl, node, node_datas[node].lhs, node_datas[node].rhs, .mul_with_saturation),
         .div      => return simpleBinOp(gz, scope, rl, node, .div),
         .mod      => return simpleBinOp(gz, scope, rl, node, .mod_rem),
         .bit_and  => {
@@ -2685,6 +2713,31 @@ fn assignOp(
     _ = try gz.addBin(.store, lhs_ptr, result);
 }
 
+// TODO: is there an existing method to accomplish this?
+// TODO: likely rename this to indicate rhs type coercion or add more params to make it more general
+fn assignBinOpExt(
+    gz: *GenZir,
+    scope: *Scope,
+    infix_node: Ast.Node.Index,
+    op_inst_tag: Zir.Inst.Extended,
+) 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, Zir.Inst.BinNode{
+        .node = gz.nodeIndexToRelative(infix_node),
+        .lhs = lhs,
+        .rhs = rhs,
+    });
+    _ = try gz.addBin(.store, lhs_ptr, result);
+}
+
 fn assignShift(
     gz: *GenZir,
     scope: *Scope,
@@ -2708,6 +2761,29 @@ fn assignShift(
     _ = try gz.addBin(.store, lhs_ptr, result);
 }
 
+fn assignShiftSat(
+    gz: *GenZir,
+    scope: *Scope,
+    infix_node: ast.Node.Index,
+    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 rhs_type = try gz.addUnNode(.typeof, 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 boolNot(gz: *GenZir, scope: *Scope, rl: ResultLoc, node: Ast.Node.Index) InnerError!Zir.Inst.Ref {
     const astgen = gz.astgen;
     const tree = astgen.tree;
@@ -7827,6 +7903,26 @@ fn shiftOp(
     return rvalue(gz, rl, result, node);
 }
 
+// TODO: is there an existing way to do this?
+// TODO: likely rename this to reflect result_loc == .none or add more params to make it more general
+fn binOpExt(
+    gz: *GenZir,
+    scope: *Scope,
+    rl: ResultLoc,
+    node: Ast.Node.Index,
+    lhs_node: Ast.Node.Index,
+    rhs_node: Ast.Node.Index,
+    tag: Zir.Inst.Extended,
+) 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, Zir.Inst.Bin{
+        .lhs = lhs,
+        .rhs = rhs,
+    });
+    return rvalue(gz, rl, result, node);
+}
+
 fn cImport(
     gz: *GenZir,
     scope: *Scope,
@@ -8119,26 +8215,32 @@ fn nodeMayNeedMemoryLocation(tree: *const Ast, start_node: Ast.Node.Index) bool
             .asm_simple,
             .add,
             .add_wrap,
+            .add_sat,
             .array_cat,
             .array_mult,
             .assign,
             .assign_bit_and,
             .assign_bit_or,
             .assign_bit_shift_left,
+            .assign_bit_shift_left_sat,
             .assign_bit_shift_right,
             .assign_bit_xor,
             .assign_div,
             .assign_sub,
             .assign_sub_wrap,
+            .assign_sub_sat,
             .assign_mod,
             .assign_add,
             .assign_add_wrap,
+            .assign_add_sat,
             .assign_mul,
             .assign_mul_wrap,
+            .assign_mul_sat,
             .bang_equal,
             .bit_and,
             .bit_or,
             .bit_shift_left,
+            .bit_shift_left_sat,
             .bit_shift_right,
             .bit_xor,
             .bool_and,
@@ -8154,10 +8256,12 @@ fn nodeMayNeedMemoryLocation(tree: *const Ast, start_node: Ast.Node.Index) bool
             .mod,
             .mul,
             .mul_wrap,
+            .mul_sat,
             .switch_range,
             .field_access,
             .sub,
             .sub_wrap,
+            .sub_sat,
             .slice,
             .slice_open,
             .slice_sentinel,
@@ -8352,26 +8456,32 @@ fn nodeMayEvalToError(tree: *const Ast, start_node: Ast.Node.Index) enum { never
             .tagged_union_enum_tag_trailing,
             .add,
             .add_wrap,
+            .add_sat,
             .array_cat,
             .array_mult,
             .assign,
             .assign_bit_and,
             .assign_bit_or,
             .assign_bit_shift_left,
+            .assign_bit_shift_left_sat,
             .assign_bit_shift_right,
             .assign_bit_xor,
             .assign_div,
             .assign_sub,
             .assign_sub_wrap,
+            .assign_sub_sat,
             .assign_mod,
             .assign_add,
             .assign_add_wrap,
+            .assign_add_sat,
             .assign_mul,
             .assign_mul_wrap,
+            .assign_mul_sat,
             .bang_equal,
             .bit_and,
             .bit_or,
             .bit_shift_left,
+            .bit_shift_left_sat,
             .bit_shift_right,
             .bit_xor,
             .bool_and,
@@ -8387,9 +8497,11 @@ fn nodeMayEvalToError(tree: *const Ast, start_node: Ast.Node.Index) enum { never
             .mod,
             .mul,
             .mul_wrap,
+            .mul_sat,
             .switch_range,
             .sub,
             .sub_wrap,
+            .sub_sat,
             .slice,
             .slice_open,
             .slice_sentinel,
@@ -8524,26 +8636,32 @@ fn nodeImpliesRuntimeBits(tree: *const Ast, start_node: Ast.Node.Index) bool {
             .asm_simple,
             .add,
             .add_wrap,
+            .add_sat,
             .array_cat,
             .array_mult,
             .assign,
             .assign_bit_and,
             .assign_bit_or,
             .assign_bit_shift_left,
+            .assign_bit_shift_left_sat,
             .assign_bit_shift_right,
             .assign_bit_xor,
             .assign_div,
             .assign_sub,
             .assign_sub_wrap,
+            .assign_sub_sat,
             .assign_mod,
             .assign_add,
             .assign_add_wrap,
+            .assign_add_sat,
             .assign_mul,
             .assign_mul_wrap,
+            .assign_mul_sat,
             .bang_equal,
             .bit_and,
             .bit_or,
             .bit_shift_left,
+            .bit_shift_left_sat,
             .bit_shift_right,
             .bit_xor,
             .bool_and,
@@ -8559,10 +8677,12 @@ fn nodeImpliesRuntimeBits(tree: *const Ast, start_node: Ast.Node.Index) bool {
             .mod,
             .mul,
             .mul_wrap,
+            .mul_sat,
             .switch_range,
             .field_access,
             .sub,
             .sub_wrap,
+            .sub_sat,
             .slice,
             .slice_open,
             .slice_sentinel,
src/codegen.zig
@@ -826,10 +826,13 @@ fn Function(comptime arch: std.Target.Cpu.Arch) type {
                     // 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),
@@ -848,6 +851,7 @@ fn Function(comptime arch: std.Target.Cpu.Arch) type {
                     .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),
@@ -1320,6 +1324,14 @@ 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 {
+            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}),
+            };
+            return self.finishAir(inst, result, .{ bin_op.lhs, bin_op.rhs, .none });
+        }
+
         fn airMul(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,10 +226,13 @@ fn analyzeInst(
     switch (inst_tags[inst]) {
         .add,
         .addwrap,
+        .addsat,
         .sub,
         .subwrap,
+        .subsat,
         .mul,
         .mulwrap,
+        .mulsat,
         .div,
         .rem,
         .mod,
@@ -252,6 +255,7 @@ fn analyzeInst(
         .ptr_elem_val,
         .ptr_ptr_elem_val,
         .shl,
+        .shl_sat,
         .shr,
         .atomic_store_unordered,
         .atomic_store_monotonic,
src/print_air.zig
@@ -104,10 +104,13 @@ const Writer = struct {
 
             .add,
             .addwrap,
+            .addsat,
             .sub,
             .subwrap,
+            .subsat,
             .mul,
             .mulwrap,
+            .mulsat,
             .div,
             .rem,
             .mod,
@@ -130,6 +133,7 @@ const Writer = struct {
             .ptr_elem_val,
             .ptr_ptr_elem_val,
             .shl,
+            .shl_sat,
             .shr,
             .set_union_tag,
             => try w.writeBinOp(s, inst),
test/behavior/saturating_arithmetic.zig
@@ -11,13 +11,34 @@ fn testSaturatingOp(comptime op: Op, comptime T: type, test_data: [3]T) !void {
     const a = test_data[0];
     const b = test_data[1];
     const expected = test_data[2];
-    const actual = switch (op) {
-        .add => @addWithSaturation(a, b),
-        .sub => @subWithSaturation(a, b),
-        .mul => @mulWithSaturation(a, b),
-        .shl => @shlWithSaturation(a, b),
-    };
-    try expectEqual(expected, actual);
+    {
+        const actual = switch (op) {
+            .add => @addWithSaturation(a, b),
+            .sub => @subWithSaturation(a, b),
+            .mul => @mulWithSaturation(a, b),
+            .shl => @shlWithSaturation(a, b),
+        };
+        try expectEqual(expected, actual);
+    }
+    {
+        const actual = switch (op) {
+            .add => a +| b,
+            .sub => a -| b,
+            .mul => a *| b,
+            .shl => a <<| b,
+        };
+        try expectEqual(expected, actual);
+    }
+    {
+        var actual = a;
+        switch (op) {
+            .add => actual +|= b,
+            .sub => actual -|= b,
+            .mul => actual *|= b,
+            .shl => actual <<|= b,
+        }
+        try expectEqual(expected, actual);
+    }
 }
 
 test "@addWithSaturation" {