tint/transform: Implement div / mod polyfill

Prevents UB for divide-by-zero and integer overflow when dividing

Fixed: tint:1349
Change-Id: Ieef66d27d7aec3011628ced076b2bccc7770a8af
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/108925
Commit-Queue: Ben Clayton <bclayton@google.com>
Reviewed-by: Dan Sinclair <dsinclair@chromium.org>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/src/tint/number.h b/src/tint/number.h
index 0558c8f..c116ddf 100644
--- a/src/tint/number.h
+++ b/src/tint/number.h
@@ -124,6 +124,9 @@
     /// type is the underlying type of the Number
     using type = T;
 
+    /// Number of bits in the number.
+    static constexpr size_t kNumBits = sizeof(T) * 8;
+
     /// Highest finite representable value of this type.
     static constexpr type kHighestValue = std::numeric_limits<type>::max();
 
@@ -187,6 +190,9 @@
     /// C++ does not have a native float16 type, so we use a 32-bit float instead.
     using type = float;
 
+    /// Number of bits in the number.
+    static constexpr size_t kNumBits = 16;
+
     /// Highest finite representable value of this type.
     static constexpr type kHighestValue = 65504.0f;  // 2¹⁵ × (1 + 1023/1024)
 
diff --git a/src/tint/program_builder.h b/src/tint/program_builder.h
index 2f5b5e0..3d1f40e 100644
--- a/src/tint/program_builder.h
+++ b/src/tint/program_builder.h
@@ -3267,6 +3267,14 @@
 //! @cond Doxygen_Suppress
 // Various template specializations for ProgramBuilder::TypesBuilder::CToAST.
 template <>
+struct ProgramBuilder::TypesBuilder::CToAST<AInt> {
+    static const ast::Type* get(const ProgramBuilder::TypesBuilder*) { return nullptr; }
+};
+template <>
+struct ProgramBuilder::TypesBuilder::CToAST<AFloat> {
+    static const ast::Type* get(const ProgramBuilder::TypesBuilder*) { return nullptr; }
+};
+template <>
 struct ProgramBuilder::TypesBuilder::CToAST<i32> {
     static const ast::Type* get(const ProgramBuilder::TypesBuilder* t) { return t->i32(); }
 };
diff --git a/src/tint/transform/builtin_polyfill.cc b/src/tint/transform/builtin_polyfill.cc
index 17fcc20..4dfc149 100644
--- a/src/tint/transform/builtin_polyfill.cc
+++ b/src/tint/transform/builtin_polyfill.cc
@@ -14,6 +14,8 @@
 
 #include "src/tint/transform/builtin_polyfill.h"
 
+#include <algorithm>
+#include <tuple>
 #include <unordered_map>
 #include <utility>
 
@@ -29,6 +31,9 @@
 
 namespace tint::transform {
 
+/// BinaryOpSignature is tuple of a binary op, LHS type and RHS type
+using BinaryOpSignature = std::tuple<ast::BinaryOp, const sem::Type*, const sem::Type*>;
+
 /// PIMPL state for the transform
 struct BuiltinPolyfill::State {
     /// Constructor
@@ -36,15 +41,6 @@
     /// @param p the builtins to polyfill
     State(CloneContext& c, Builtins p) : ctx(c), polyfill(p) {}
 
-    /// The clone context
-    CloneContext& ctx;
-    /// The builtins to polyfill
-    Builtins polyfill;
-    /// The destination program builder
-    ProgramBuilder& b = *ctx.dst;
-    /// The source clone context
-    const sem::Info& sem = ctx.src->Sem();
-
     /// Builds the polyfill function for the `acosh` builtin
     /// @param ty the parameter and return type for the function
     /// @return the polyfill function name
@@ -563,6 +559,63 @@
         return name;
     }
 
+    /// Builds the polyfill function for a divide or modulo operator with integer scalar or vector
+    /// operands.
+    /// @param sig the signature of the binary operator
+    /// @return the polyfill function name
+    Symbol int_div_mod(const BinaryOpSignature& sig) {
+        const auto op = std::get<0>(sig);
+        const auto* lhs_ty = std::get<1>(sig);
+        const auto* rhs_ty = std::get<2>(sig);
+        const bool is_div = op == ast::BinaryOp::kDivide;
+
+        uint32_t lhs_width = 1;
+        uint32_t rhs_width = 1;
+        const auto* lhs_el_ty = sem::Type::ElementOf(lhs_ty, &lhs_width);
+        const auto* rhs_el_ty = sem::Type::ElementOf(rhs_ty, &rhs_width);
+
+        const uint32_t width = std::max(lhs_width, rhs_width);
+
+        const char* lhs = "lhs";
+        const char* rhs = "rhs";
+
+        utils::Vector<const ast::Statement*, 4> body;
+
+        if (lhs_width < width) {
+            // lhs is scalar, rhs is vector. Convert lhs to vector.
+            body.Push(b.Decl(b.Let("l", b.vec(T(lhs_el_ty), width, b.Expr(lhs)))));
+            lhs = "l";
+        }
+        if (rhs_width < width) {
+            // lhs is vector, rhs is scalar. Convert rhs to vector.
+            body.Push(b.Decl(b.Let("r", b.vec(T(rhs_el_ty), width, b.Expr(rhs)))));
+            rhs = "r";
+        }
+
+        auto name = b.Symbols().New(is_div ? "tint_div" : "tint_mod");
+        auto* use_one = b.Equal(rhs, ScalarOrVector(width, 0_a));
+        if (lhs_ty->is_signed_scalar_or_vector()) {
+            const auto bits = lhs_el_ty->Size() * 8;
+            auto min_int = AInt(AInt::kLowestValue >> (AInt::kNumBits - bits));
+            const ast::Expression* lhs_is_min = b.Equal(lhs, ScalarOrVector(width, min_int));
+            const ast::Expression* rhs_is_minus_one = b.Equal(rhs, ScalarOrVector(width, -1_a));
+            // use_one = use_one | ((lhs == MIN_INT) & (rhs == -1))
+            use_one = b.Or(use_one, b.And(lhs_is_min, rhs_is_minus_one));
+        }
+        auto* select = b.Call("select", rhs, ScalarOrVector(width, 1_a), use_one);
+
+        body.Push(b.Return(is_div ? b.Div(lhs, select) : b.Mod(lhs, select)));
+        b.Func(name,
+               utils::Vector{
+                   b.Param("lhs", T(lhs_ty)),
+                   b.Param("rhs", T(rhs_ty)),
+               },
+               width == 1 ? T(lhs_ty) : b.ty.vec(T(lhs_el_ty), width),  // return type
+               std::move(body));
+
+        return name;
+    }
+
     /// Builds the polyfill function for the `saturate` builtin
     /// @param ty the parameter and return type for the function
     /// @return the polyfill function name
@@ -625,6 +678,15 @@
     }
 
   private:
+    /// The clone context
+    CloneContext& ctx;
+    /// The builtins to polyfill
+    Builtins polyfill;
+    /// The destination program builder
+    ProgramBuilder& b = *ctx.dst;
+    /// The source clone context
+    const sem::Info& sem = ctx.src->Sem();
+
     /// @returns the AST type for the given sem type
     const ast::Type* T(const sem::Type* ty) const { return CreateASTTypeFor(ctx, ty); }
 
@@ -659,13 +721,14 @@
         return SkipTransform;
     }
 
-    auto& builtins = cfg->builtins;
+    auto& polyfill = cfg->builtins;
 
     utils::Hashmap<const sem::Builtin*, Symbol, 8> builtin_polyfills;
+    utils::Hashmap<BinaryOpSignature, Symbol, 8> binary_op_polyfills;
 
     ProgramBuilder b;
     CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
-    State s{ctx, builtins};
+    State s{ctx, polyfill};
 
     bool made_changes = false;
     for (auto* node : src->ASTNodes().Objects()) {
@@ -679,84 +742,84 @@
             if (!builtin) {
                 continue;
             }
-            Symbol polyfill;
+            Symbol fn;
             switch (builtin->Type()) {
                 case sem::BuiltinType::kAcosh:
-                    if (builtins.acosh != Level::kNone) {
-                        polyfill = builtin_polyfills.GetOrCreate(
+                    if (polyfill.acosh != Level::kNone) {
+                        fn = builtin_polyfills.GetOrCreate(
                             builtin, [&] { return s.acosh(builtin->ReturnType()); });
                     }
                     break;
                 case sem::BuiltinType::kAsinh:
-                    if (builtins.asinh) {
-                        polyfill = builtin_polyfills.GetOrCreate(
+                    if (polyfill.asinh) {
+                        fn = builtin_polyfills.GetOrCreate(
                             builtin, [&] { return s.asinh(builtin->ReturnType()); });
                     }
                     break;
                 case sem::BuiltinType::kAtanh:
-                    if (builtins.atanh != Level::kNone) {
-                        polyfill = builtin_polyfills.GetOrCreate(
+                    if (polyfill.atanh != Level::kNone) {
+                        fn = builtin_polyfills.GetOrCreate(
                             builtin, [&] { return s.atanh(builtin->ReturnType()); });
                     }
                     break;
                 case sem::BuiltinType::kClamp:
-                    if (builtins.clamp_int) {
+                    if (polyfill.clamp_int) {
                         auto& sig = builtin->Signature();
                         if (sig.parameters[0]->Type()->is_integer_scalar_or_vector()) {
-                            polyfill = builtin_polyfills.GetOrCreate(
+                            fn = builtin_polyfills.GetOrCreate(
                                 builtin, [&] { return s.clampInteger(builtin->ReturnType()); });
                         }
                     }
                     break;
                 case sem::BuiltinType::kCountLeadingZeros:
-                    if (builtins.count_leading_zeros) {
-                        polyfill = builtin_polyfills.GetOrCreate(
+                    if (polyfill.count_leading_zeros) {
+                        fn = builtin_polyfills.GetOrCreate(
                             builtin, [&] { return s.countLeadingZeros(builtin->ReturnType()); });
                     }
                     break;
                 case sem::BuiltinType::kCountTrailingZeros:
-                    if (builtins.count_trailing_zeros) {
-                        polyfill = builtin_polyfills.GetOrCreate(
+                    if (polyfill.count_trailing_zeros) {
+                        fn = builtin_polyfills.GetOrCreate(
                             builtin, [&] { return s.countTrailingZeros(builtin->ReturnType()); });
                     }
                     break;
                 case sem::BuiltinType::kExtractBits:
-                    if (builtins.extract_bits != Level::kNone) {
-                        polyfill = builtin_polyfills.GetOrCreate(
+                    if (polyfill.extract_bits != Level::kNone) {
+                        fn = builtin_polyfills.GetOrCreate(
                             builtin, [&] { return s.extractBits(builtin->ReturnType()); });
                     }
                     break;
                 case sem::BuiltinType::kFirstLeadingBit:
-                    if (builtins.first_leading_bit) {
-                        polyfill = builtin_polyfills.GetOrCreate(
+                    if (polyfill.first_leading_bit) {
+                        fn = builtin_polyfills.GetOrCreate(
                             builtin, [&] { return s.firstLeadingBit(builtin->ReturnType()); });
                     }
                     break;
                 case sem::BuiltinType::kFirstTrailingBit:
-                    if (builtins.first_trailing_bit) {
-                        polyfill = builtin_polyfills.GetOrCreate(
+                    if (polyfill.first_trailing_bit) {
+                        fn = builtin_polyfills.GetOrCreate(
                             builtin, [&] { return s.firstTrailingBit(builtin->ReturnType()); });
                     }
                     break;
                 case sem::BuiltinType::kInsertBits:
-                    if (builtins.insert_bits != Level::kNone) {
-                        polyfill = builtin_polyfills.GetOrCreate(
+                    if (polyfill.insert_bits != Level::kNone) {
+                        fn = builtin_polyfills.GetOrCreate(
                             builtin, [&] { return s.insertBits(builtin->ReturnType()); });
                     }
                     break;
                 case sem::BuiltinType::kSaturate:
-                    if (builtins.saturate) {
-                        polyfill = builtin_polyfills.GetOrCreate(
+                    if (polyfill.saturate) {
+                        fn = builtin_polyfills.GetOrCreate(
                             builtin, [&] { return s.saturate(builtin->ReturnType()); });
                     }
                     break;
                 case sem::BuiltinType::kTextureSampleBaseClampToEdge:
-                    if (builtins.texture_sample_base_clamp_to_edge_2d_f32) {
+                    if (polyfill.texture_sample_base_clamp_to_edge_2d_f32) {
                         auto& sig = builtin->Signature();
                         auto* tex = sig.Parameter(sem::ParameterUsage::kTexture);
                         if (auto* stex = tex->Type()->As<sem::SampledTexture>()) {
                             if (stex->type()->Is<sem::F32>()) {
-                                polyfill = builtin_polyfills.GetOrCreate(builtin, [&] {
+                                fn = builtin_polyfills.GetOrCreate(builtin, [&] {
                                     return s.textureSampleBaseClampToEdge_2d_f32();
                                 });
                             }
@@ -764,9 +827,9 @@
                     }
                     break;
                 case sem::BuiltinType::kQuantizeToF16:
-                    if (builtins.quantize_to_vec_f16) {
+                    if (polyfill.quantize_to_vec_f16) {
                         if (auto* vec = builtin->ReturnType()->As<sem::Vector>()) {
-                            polyfill = builtin_polyfills.GetOrCreate(
+                            fn = builtin_polyfills.GetOrCreate(
                                 builtin, [&] { return s.quantizeToF16(vec); });
                         }
                     }
@@ -776,16 +839,16 @@
                     break;
             }
 
-            if (polyfill.IsValid()) {
-                auto* replacement = s.b.Call(polyfill, ctx.Clone(call->Declaration()->args));
+            if (fn.IsValid()) {
+                auto* replacement = b.Call(fn, ctx.Clone(call->Declaration()->args));
                 ctx.Replace(call->Declaration(), replacement);
                 made_changes = true;
             }
         } else if (auto* bin_op = node->As<ast::BinaryExpression>()) {
             switch (bin_op->op) {
                 case ast::BinaryOp::kShiftLeft:
-                case ast::BinaryOp::kShiftRight:
-                    if (builtins.bitshift_modulo) {
+                case ast::BinaryOp::kShiftRight: {
+                    if (polyfill.bitshift_modulo) {
                         auto* lhs_ty = src->TypeOf(bin_op->lhs)->UnwrapRef();
                         auto* rhs_ty = src->TypeOf(bin_op->rhs)->UnwrapRef();
                         auto* lhs_el_ty = sem::Type::DeepestElementOf(lhs_ty);
@@ -798,6 +861,24 @@
                         made_changes = true;
                     }
                     break;
+                }
+                case ast::BinaryOp::kDivide:
+                case ast::BinaryOp::kModulo: {
+                    if (polyfill.int_div_mod) {
+                        auto* lhs_ty = src->TypeOf(bin_op->lhs)->UnwrapRef();
+                        if (lhs_ty->is_integer_scalar_or_vector()) {
+                            auto* rhs_ty = src->TypeOf(bin_op->rhs)->UnwrapRef();
+                            BinaryOpSignature sig{bin_op->op, lhs_ty, rhs_ty};
+                            auto fn = binary_op_polyfills.GetOrCreate(
+                                sig, [&] { return s.int_div_mod(sig); });
+                            auto* lhs = ctx.Clone(bin_op->lhs);
+                            auto* rhs = ctx.Clone(bin_op->rhs);
+                            ctx.Replace(bin_op, b.Call(fn, lhs, rhs));
+                            made_changes = true;
+                        }
+                    }
+                    break;
+                }
                 default:
                     break;
             }
diff --git a/src/tint/transform/builtin_polyfill.h b/src/tint/transform/builtin_polyfill.h
index 7083aa7..f9eb029 100644
--- a/src/tint/transform/builtin_polyfill.h
+++ b/src/tint/transform/builtin_polyfill.h
@@ -63,6 +63,9 @@
         bool first_trailing_bit = false;
         /// Should `insertBits()` be polyfilled?
         Level insert_bits = Level::kNone;
+        /// Should integer scalar / vector divides and modulos be polyfilled to avoid DBZ and
+        /// integer overflows?
+        bool int_div_mod = false;
         /// Should `saturate()` be polyfilled?
         bool saturate = false;
         /// Should `textureSampleBaseClampToEdge()` be polyfilled for texture_2d<f32> textures?
diff --git a/src/tint/transform/builtin_polyfill_test.cc b/src/tint/transform/builtin_polyfill_test.cc
index 1e380d4..b767167 100644
--- a/src/tint/transform/builtin_polyfill_test.cc
+++ b/src/tint/transform/builtin_polyfill_test.cc
@@ -1921,6 +1921,775 @@
 }
 
 ////////////////////////////////////////////////////////////////////////////////
+// int_div_mod
+////////////////////////////////////////////////////////////////////////////////
+DataMap polyfillIntDivMod() {
+    BuiltinPolyfill::Builtins builtins;
+    builtins.int_div_mod = true;
+    DataMap data;
+    data.Add<BuiltinPolyfill::Config>(builtins);
+    return data;
+}
+
+TEST_F(BuiltinPolyfillTest, ShouldRunIntDiv) {
+    auto* src = R"(
+fn f() {
+  let v = 10i;
+  let x = 20i / v;
+}
+)";
+
+    EXPECT_FALSE(ShouldRun<BuiltinPolyfill>(src));
+    EXPECT_TRUE(ShouldRun<BuiltinPolyfill>(src, polyfillIntDivMod()));
+}
+
+TEST_F(BuiltinPolyfillTest, ShouldRunIntMod) {
+    auto* src = R"(
+fn f() {
+  let v = 10i;
+  let x = 20i % v;
+}
+)";
+
+    EXPECT_FALSE(ShouldRun<BuiltinPolyfill>(src));
+    EXPECT_TRUE(ShouldRun<BuiltinPolyfill>(src, polyfillIntDivMod()));
+}
+
+TEST_F(BuiltinPolyfillTest, IntDiv_ai_i32) {
+    auto* src = R"(
+fn f() {
+  let v = 10i;
+  let x = 20 / v;
+}
+)";
+
+    auto* expect = R"(
+fn tint_div(lhs : i32, rhs : i32) -> i32 {
+  return (lhs / select(rhs, 1, ((rhs == 0) | ((lhs == -2147483648) & (rhs == -1)))));
+}
+
+fn f() {
+  let v = 10i;
+  let x = tint_div(20, v);
+}
+)";
+
+    auto got = Run<BuiltinPolyfill>(src, polyfillIntDivMod());
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(BuiltinPolyfillTest, IntMod_ai_i32) {
+    auto* src = R"(
+fn f() {
+  let v = 10i;
+  let x = 20 % v;
+}
+)";
+
+    auto* expect = R"(
+fn tint_mod(lhs : i32, rhs : i32) -> i32 {
+  return (lhs % select(rhs, 1, ((rhs == 0) | ((lhs == -2147483648) & (rhs == -1)))));
+}
+
+fn f() {
+  let v = 10i;
+  let x = tint_mod(20, v);
+}
+)";
+
+    auto got = Run<BuiltinPolyfill>(src, polyfillIntDivMod());
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(BuiltinPolyfillTest, IntDiv_i32_ai) {
+    auto* src = R"(
+fn f() {
+  let v = 10i;
+  let x = v / 20;
+}
+)";
+
+    auto* expect = R"(
+fn tint_div(lhs : i32, rhs : i32) -> i32 {
+  return (lhs / select(rhs, 1, ((rhs == 0) | ((lhs == -2147483648) & (rhs == -1)))));
+}
+
+fn f() {
+  let v = 10i;
+  let x = tint_div(v, 20);
+}
+)";
+
+    auto got = Run<BuiltinPolyfill>(src, polyfillIntDivMod());
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(BuiltinPolyfillTest, IntMod_i32_ai) {
+    auto* src = R"(
+fn f() {
+  let v = 10i;
+  let x = v % 20;
+}
+)";
+
+    auto* expect = R"(
+fn tint_mod(lhs : i32, rhs : i32) -> i32 {
+  return (lhs % select(rhs, 1, ((rhs == 0) | ((lhs == -2147483648) & (rhs == -1)))));
+}
+
+fn f() {
+  let v = 10i;
+  let x = tint_mod(v, 20);
+}
+)";
+
+    auto got = Run<BuiltinPolyfill>(src, polyfillIntDivMod());
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(BuiltinPolyfillTest, IntDiv_i32_i32) {
+    auto* src = R"(
+fn f() {
+  let v = 10i;
+  let x = 20i / v;
+}
+)";
+
+    auto* expect = R"(
+fn tint_div(lhs : i32, rhs : i32) -> i32 {
+  return (lhs / select(rhs, 1, ((rhs == 0) | ((lhs == -2147483648) & (rhs == -1)))));
+}
+
+fn f() {
+  let v = 10i;
+  let x = tint_div(20i, v);
+}
+)";
+
+    auto got = Run<BuiltinPolyfill>(src, polyfillIntDivMod());
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(BuiltinPolyfillTest, IntMod_i32_i32) {
+    auto* src = R"(
+fn f() {
+  let v = 10i;
+  let x = 20i % v;
+}
+)";
+
+    auto* expect = R"(
+fn tint_mod(lhs : i32, rhs : i32) -> i32 {
+  return (lhs % select(rhs, 1, ((rhs == 0) | ((lhs == -2147483648) & (rhs == -1)))));
+}
+
+fn f() {
+  let v = 10i;
+  let x = tint_mod(20i, v);
+}
+)";
+
+    auto got = Run<BuiltinPolyfill>(src, polyfillIntDivMod());
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(BuiltinPolyfillTest, IntDiv_ai_u32) {
+    auto* src = R"(
+fn f() {
+  let v = 10u;
+  let x = 20 / v;
+}
+)";
+
+    auto* expect = R"(
+fn tint_div(lhs : u32, rhs : u32) -> u32 {
+  return (lhs / select(rhs, 1, (rhs == 0)));
+}
+
+fn f() {
+  let v = 10u;
+  let x = tint_div(20, v);
+}
+)";
+
+    auto got = Run<BuiltinPolyfill>(src, polyfillIntDivMod());
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(BuiltinPolyfillTest, IntMod_ai_u32) {
+    auto* src = R"(
+fn f() {
+  let v = 10u;
+  let x = 20 % v;
+}
+)";
+
+    auto* expect = R"(
+fn tint_mod(lhs : u32, rhs : u32) -> u32 {
+  return (lhs % select(rhs, 1, (rhs == 0)));
+}
+
+fn f() {
+  let v = 10u;
+  let x = tint_mod(20, v);
+}
+)";
+
+    auto got = Run<BuiltinPolyfill>(src, polyfillIntDivMod());
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(BuiltinPolyfillTest, IntDiv_u32_ai) {
+    auto* src = R"(
+fn f() {
+  let v = 10u;
+  let x = v / 20;
+}
+)";
+
+    auto* expect = R"(
+fn tint_div(lhs : u32, rhs : u32) -> u32 {
+  return (lhs / select(rhs, 1, (rhs == 0)));
+}
+
+fn f() {
+  let v = 10u;
+  let x = tint_div(v, 20);
+}
+)";
+
+    auto got = Run<BuiltinPolyfill>(src, polyfillIntDivMod());
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(BuiltinPolyfillTest, IntMod_u32_ai) {
+    auto* src = R"(
+fn f() {
+  let v = 10u;
+  let x = v % 20;
+}
+)";
+
+    auto* expect = R"(
+fn tint_mod(lhs : u32, rhs : u32) -> u32 {
+  return (lhs % select(rhs, 1, (rhs == 0)));
+}
+
+fn f() {
+  let v = 10u;
+  let x = tint_mod(v, 20);
+}
+)";
+
+    auto got = Run<BuiltinPolyfill>(src, polyfillIntDivMod());
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(BuiltinPolyfillTest, IntDiv_u32_u32) {
+    auto* src = R"(
+fn f() {
+  let v = 10u;
+  let x = 20u / v;
+}
+)";
+
+    auto* expect = R"(
+fn tint_div(lhs : u32, rhs : u32) -> u32 {
+  return (lhs / select(rhs, 1, (rhs == 0)));
+}
+
+fn f() {
+  let v = 10u;
+  let x = tint_div(20u, v);
+}
+)";
+
+    auto got = Run<BuiltinPolyfill>(src, polyfillIntDivMod());
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(BuiltinPolyfillTest, IntMod_u32_u32) {
+    auto* src = R"(
+fn f() {
+  let v = 10u;
+  let x = 20u % v;
+}
+)";
+
+    auto* expect = R"(
+fn tint_mod(lhs : u32, rhs : u32) -> u32 {
+  return (lhs % select(rhs, 1, (rhs == 0)));
+}
+
+fn f() {
+  let v = 10u;
+  let x = tint_mod(20u, v);
+}
+)";
+
+    auto got = Run<BuiltinPolyfill>(src, polyfillIntDivMod());
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(BuiltinPolyfillTest, IntDiv_vec3_ai_i32) {
+    auto* src = R"(
+fn f() {
+  let v = 10i;
+  let x = vec3(20) / v;
+}
+)";
+
+    auto* expect = R"(
+fn tint_div(lhs : vec3<i32>, rhs : i32) -> vec3<i32> {
+  let r = vec3<i32>(rhs);
+  return (lhs / select(r, vec3(1), ((r == vec3(0)) | ((lhs == vec3(-2147483648)) & (r == vec3(-1))))));
+}
+
+fn f() {
+  let v = 10i;
+  let x = tint_div(vec3(20), v);
+}
+)";
+
+    auto got = Run<BuiltinPolyfill>(src, polyfillIntDivMod());
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(BuiltinPolyfillTest, IntMod_vec3_ai_i32) {
+    auto* src = R"(
+fn f() {
+  let v = 10i;
+  let x = vec3(20) % v;
+}
+)";
+
+    auto* expect = R"(
+fn tint_mod(lhs : vec3<i32>, rhs : i32) -> vec3<i32> {
+  let r = vec3<i32>(rhs);
+  return (lhs % select(r, vec3(1), ((r == vec3(0)) | ((lhs == vec3(-2147483648)) & (r == vec3(-1))))));
+}
+
+fn f() {
+  let v = 10i;
+  let x = tint_mod(vec3(20), v);
+}
+)";
+
+    auto got = Run<BuiltinPolyfill>(src, polyfillIntDivMod());
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(BuiltinPolyfillTest, IntDiv_vec3_i32_ai) {
+    auto* src = R"(
+fn f() {
+  let v = 10i;
+  let x = vec3(v) / 20;
+}
+)";
+
+    auto* expect = R"(
+fn tint_div(lhs : vec3<i32>, rhs : i32) -> vec3<i32> {
+  let r = vec3<i32>(rhs);
+  return (lhs / select(r, vec3(1), ((r == vec3(0)) | ((lhs == vec3(-2147483648)) & (r == vec3(-1))))));
+}
+
+fn f() {
+  let v = 10i;
+  let x = tint_div(vec3(v), 20);
+}
+)";
+
+    auto got = Run<BuiltinPolyfill>(src, polyfillIntDivMod());
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(BuiltinPolyfillTest, IntMod_vec3_i32_ai) {
+    auto* src = R"(
+fn f() {
+  let v = 10i;
+  let x = vec3(v) % 20;
+}
+)";
+
+    auto* expect = R"(
+fn tint_mod(lhs : vec3<i32>, rhs : i32) -> vec3<i32> {
+  let r = vec3<i32>(rhs);
+  return (lhs % select(r, vec3(1), ((r == vec3(0)) | ((lhs == vec3(-2147483648)) & (r == vec3(-1))))));
+}
+
+fn f() {
+  let v = 10i;
+  let x = tint_mod(vec3(v), 20);
+}
+)";
+
+    auto got = Run<BuiltinPolyfill>(src, polyfillIntDivMod());
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(BuiltinPolyfillTest, IntDiv_vec3_i32_i32) {
+    auto* src = R"(
+fn f() {
+  let v = 10i;
+  let x = vec3<i32>(20i) / v;
+}
+)";
+
+    auto* expect = R"(
+fn tint_div(lhs : vec3<i32>, rhs : i32) -> vec3<i32> {
+  let r = vec3<i32>(rhs);
+  return (lhs / select(r, vec3(1), ((r == vec3(0)) | ((lhs == vec3(-2147483648)) & (r == vec3(-1))))));
+}
+
+fn f() {
+  let v = 10i;
+  let x = tint_div(vec3<i32>(20i), v);
+}
+)";
+
+    auto got = Run<BuiltinPolyfill>(src, polyfillIntDivMod());
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(BuiltinPolyfillTest, IntMod_vec3_i32_i32) {
+    auto* src = R"(
+fn f() {
+  let v = 10i;
+  let x = vec3<i32>(20i) % v;
+}
+)";
+
+    auto* expect = R"(
+fn tint_mod(lhs : vec3<i32>, rhs : i32) -> vec3<i32> {
+  let r = vec3<i32>(rhs);
+  return (lhs % select(r, vec3(1), ((r == vec3(0)) | ((lhs == vec3(-2147483648)) & (r == vec3(-1))))));
+}
+
+fn f() {
+  let v = 10i;
+  let x = tint_mod(vec3<i32>(20i), v);
+}
+)";
+
+    auto got = Run<BuiltinPolyfill>(src, polyfillIntDivMod());
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(BuiltinPolyfillTest, IntDiv_vec3_u32_u32) {
+    auto* src = R"(
+fn f() {
+  let v = 10u;
+  let x = vec3<u32>(20u) / v;
+}
+)";
+
+    auto* expect = R"(
+fn tint_div(lhs : vec3<u32>, rhs : u32) -> vec3<u32> {
+  let r = vec3<u32>(rhs);
+  return (lhs / select(r, vec3(1), (r == vec3(0))));
+}
+
+fn f() {
+  let v = 10u;
+  let x = tint_div(vec3<u32>(20u), v);
+}
+)";
+
+    auto got = Run<BuiltinPolyfill>(src, polyfillIntDivMod());
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(BuiltinPolyfillTest, IntMod_vec3_u32_u32) {
+    auto* src = R"(
+fn f() {
+  let v = 10u;
+  let x = vec3<u32>(20u) % v;
+}
+)";
+
+    auto* expect = R"(
+fn tint_mod(lhs : vec3<u32>, rhs : u32) -> vec3<u32> {
+  let r = vec3<u32>(rhs);
+  return (lhs % select(r, vec3(1), (r == vec3(0))));
+}
+
+fn f() {
+  let v = 10u;
+  let x = tint_mod(vec3<u32>(20u), v);
+}
+)";
+
+    auto got = Run<BuiltinPolyfill>(src, polyfillIntDivMod());
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(BuiltinPolyfillTest, IntDiv_ai_vec3_i32) {
+    auto* src = R"(
+fn f() {
+  let v = 10i;
+  let x = 20 / vec3(v);
+}
+)";
+
+    auto* expect = R"(
+fn tint_div(lhs : i32, rhs : vec3<i32>) -> vec3<i32> {
+  let l = vec3<i32>(lhs);
+  return (l / select(rhs, vec3(1), ((rhs == vec3(0)) | ((l == vec3(-2147483648)) & (rhs == vec3(-1))))));
+}
+
+fn f() {
+  let v = 10i;
+  let x = tint_div(20, vec3(v));
+}
+)";
+
+    auto got = Run<BuiltinPolyfill>(src, polyfillIntDivMod());
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(BuiltinPolyfillTest, IntMod_ai_vec3_i32) {
+    auto* src = R"(
+fn f() {
+  let v = 10i;
+  let x = 20 % vec3(v);
+}
+)";
+
+    auto* expect = R"(
+fn tint_mod(lhs : i32, rhs : vec3<i32>) -> vec3<i32> {
+  let l = vec3<i32>(lhs);
+  return (l % select(rhs, vec3(1), ((rhs == vec3(0)) | ((l == vec3(-2147483648)) & (rhs == vec3(-1))))));
+}
+
+fn f() {
+  let v = 10i;
+  let x = tint_mod(20, vec3(v));
+}
+)";
+
+    auto got = Run<BuiltinPolyfill>(src, polyfillIntDivMod());
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(BuiltinPolyfillTest, IntDiv_i32_vec3_i32) {
+    auto* src = R"(
+fn f() {
+  let v = 10i;
+  let x = 20i / vec3<i32>(v);
+}
+)";
+
+    auto* expect = R"(
+fn tint_div(lhs : i32, rhs : vec3<i32>) -> vec3<i32> {
+  let l = vec3<i32>(lhs);
+  return (l / select(rhs, vec3(1), ((rhs == vec3(0)) | ((l == vec3(-2147483648)) & (rhs == vec3(-1))))));
+}
+
+fn f() {
+  let v = 10i;
+  let x = tint_div(20i, vec3<i32>(v));
+}
+)";
+
+    auto got = Run<BuiltinPolyfill>(src, polyfillIntDivMod());
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(BuiltinPolyfillTest, IntMod_i32_vec3_i32) {
+    auto* src = R"(
+fn f() {
+  let v = 10i;
+  let x = 20i % vec3<i32>(v);
+}
+)";
+
+    auto* expect = R"(
+fn tint_mod(lhs : i32, rhs : vec3<i32>) -> vec3<i32> {
+  let l = vec3<i32>(lhs);
+  return (l % select(rhs, vec3(1), ((rhs == vec3(0)) | ((l == vec3(-2147483648)) & (rhs == vec3(-1))))));
+}
+
+fn f() {
+  let v = 10i;
+  let x = tint_mod(20i, vec3<i32>(v));
+}
+)";
+
+    auto got = Run<BuiltinPolyfill>(src, polyfillIntDivMod());
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(BuiltinPolyfillTest, IntDiv_u32_vec3_u32) {
+    auto* src = R"(
+fn f() {
+  let v = 10u;
+  let x = 20u / vec3<u32>(v);
+}
+)";
+
+    auto* expect = R"(
+fn tint_div(lhs : u32, rhs : vec3<u32>) -> vec3<u32> {
+  let l = vec3<u32>(lhs);
+  return (l / select(rhs, vec3(1), (rhs == vec3(0))));
+}
+
+fn f() {
+  let v = 10u;
+  let x = tint_div(20u, vec3<u32>(v));
+}
+)";
+
+    auto got = Run<BuiltinPolyfill>(src, polyfillIntDivMod());
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(BuiltinPolyfillTest, IntMod_u32_vec3_u32) {
+    auto* src = R"(
+fn f() {
+  let v = 10u;
+  let x = 20u % vec3<u32>(v);
+}
+)";
+
+    auto* expect = R"(
+fn tint_mod(lhs : u32, rhs : vec3<u32>) -> vec3<u32> {
+  let l = vec3<u32>(lhs);
+  return (l % select(rhs, vec3(1), (rhs == vec3(0))));
+}
+
+fn f() {
+  let v = 10u;
+  let x = tint_mod(20u, vec3<u32>(v));
+}
+)";
+
+    auto got = Run<BuiltinPolyfill>(src, polyfillIntDivMod());
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(BuiltinPolyfillTest, IntDiv_vec3_i32_vec3_i32) {
+    auto* src = R"(
+fn f() {
+  let v = 10i;
+  let x = vec3<i32>(20i) / vec3<i32>(v);
+}
+)";
+
+    auto* expect = R"(
+fn tint_div(lhs : vec3<i32>, rhs : vec3<i32>) -> vec3<i32> {
+  return (lhs / select(rhs, vec3(1), ((rhs == vec3(0)) | ((lhs == vec3(-2147483648)) & (rhs == vec3(-1))))));
+}
+
+fn f() {
+  let v = 10i;
+  let x = tint_div(vec3<i32>(20i), vec3<i32>(v));
+}
+)";
+
+    auto got = Run<BuiltinPolyfill>(src, polyfillIntDivMod());
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(BuiltinPolyfillTest, IntMod_vec3_i32_vec3_i32) {
+    auto* src = R"(
+fn f() {
+  let v = 10i;
+  let x = vec3<i32>(20i) % vec3<i32>(v);
+}
+)";
+
+    auto* expect = R"(
+fn tint_mod(lhs : vec3<i32>, rhs : vec3<i32>) -> vec3<i32> {
+  return (lhs % select(rhs, vec3(1), ((rhs == vec3(0)) | ((lhs == vec3(-2147483648)) & (rhs == vec3(-1))))));
+}
+
+fn f() {
+  let v = 10i;
+  let x = tint_mod(vec3<i32>(20i), vec3<i32>(v));
+}
+)";
+
+    auto got = Run<BuiltinPolyfill>(src, polyfillIntDivMod());
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(BuiltinPolyfillTest, IntDiv_vec3_u32_vec3_u32) {
+    auto* src = R"(
+fn f() {
+  let v = 10u;
+  let x = vec3<u32>(20u) / vec3<u32>(v);
+}
+)";
+
+    auto* expect = R"(
+fn tint_div(lhs : vec3<u32>, rhs : vec3<u32>) -> vec3<u32> {
+  return (lhs / select(rhs, vec3(1), (rhs == vec3(0))));
+}
+
+fn f() {
+  let v = 10u;
+  let x = tint_div(vec3<u32>(20u), vec3<u32>(v));
+}
+)";
+
+    auto got = Run<BuiltinPolyfill>(src, polyfillIntDivMod());
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(BuiltinPolyfillTest, IntMod_vec3_u32_vec3_u32) {
+    auto* src = R"(
+fn f() {
+  let v = 10u;
+  let x = vec3<u32>(20u) % vec3<u32>(v);
+}
+)";
+
+    auto* expect = R"(
+fn tint_mod(lhs : vec3<u32>, rhs : vec3<u32>) -> vec3<u32> {
+  return (lhs % select(rhs, vec3(1), (rhs == vec3(0))));
+}
+
+fn f() {
+  let v = 10u;
+  let x = tint_mod(vec3<u32>(20u), vec3<u32>(v));
+}
+)";
+
+    auto got = Run<BuiltinPolyfill>(src, polyfillIntDivMod());
+
+    EXPECT_EQ(expect, str(got));
+}
+
+////////////////////////////////////////////////////////////////////////////////
 // saturate
 ////////////////////////////////////////////////////////////////////////////////
 DataMap polyfillSaturate() {
diff --git a/src/tint/writer/glsl/generator_impl.cc b/src/tint/writer/glsl/generator_impl.cc
index 29136cb..b8140b9 100644
--- a/src/tint/writer/glsl/generator_impl.cc
+++ b/src/tint/writer/glsl/generator_impl.cc
@@ -182,6 +182,9 @@
 
     manager.Add<transform::DisableUniformityAnalysis>();
 
+    // ExpandCompoundAssignment must come before BuiltinPolyfill
+    manager.Add<transform::ExpandCompoundAssignment>();
+
     {  // Builtin polyfills
         transform::BuiltinPolyfill::Builtins polyfills;
         polyfills.acosh = transform::BuiltinPolyfill::Level::kRangeCheck;
@@ -193,6 +196,7 @@
         polyfills.first_leading_bit = true;
         polyfills.first_trailing_bit = true;
         polyfills.insert_bits = transform::BuiltinPolyfill::Level::kClampParameters;
+        polyfills.int_div_mod = true;
         polyfills.saturate = true;
         polyfills.texture_sample_base_clamp_to_edge_2d_f32 = true;
         data.Add<transform::BuiltinPolyfill::Config>(polyfills);
@@ -214,7 +218,6 @@
         manager.Add<transform::ZeroInitWorkgroupMemory>();
     }
     manager.Add<transform::CanonicalizeEntryPointIO>();
-    manager.Add<transform::ExpandCompoundAssignment>();
     manager.Add<transform::PromoteSideEffectsToDecl>();
     manager.Add<transform::PadStructs>();
 
diff --git a/src/tint/writer/hlsl/generator_impl.cc b/src/tint/writer/hlsl/generator_impl.cc
index a7c3f00..3eb3358 100644
--- a/src/tint/writer/hlsl/generator_impl.cc
+++ b/src/tint/writer/hlsl/generator_impl.cc
@@ -157,6 +157,9 @@
 
     manager.Add<transform::DisableUniformityAnalysis>();
 
+    // ExpandCompoundAssignment must come before BuiltinPolyfill
+    manager.Add<transform::ExpandCompoundAssignment>();
+
     {  // Builtin polyfills
         transform::BuiltinPolyfill::Builtins polyfills;
         polyfills.acosh = transform::BuiltinPolyfill::Level::kFull;
@@ -172,6 +175,7 @@
         polyfills.first_leading_bit = true;
         polyfills.first_trailing_bit = true;
         polyfills.insert_bits = transform::BuiltinPolyfill::Level::kFull;
+        polyfills.int_div_mod = true;
         polyfills.texture_sample_base_clamp_to_edge_2d_f32 = true;
         data.Add<transform::BuiltinPolyfill::Config>(polyfills);
         manager.Add<transform::BuiltinPolyfill>();
@@ -211,7 +215,6 @@
     // assumes that num_workgroups builtins only appear as struct members and are
     // only accessed directly via member accessors.
     manager.Add<transform::NumWorkgroupsFromUniform>();
-    manager.Add<transform::ExpandCompoundAssignment>();
     manager.Add<transform::PromoteSideEffectsToDecl>();
     manager.Add<transform::VectorizeScalarMatrixInitializers>();
     manager.Add<transform::SimplifyPointers>();
@@ -661,117 +664,6 @@
     return true;
 }
 
-bool GeneratorImpl::EmitExpressionOrOneIfZero(std::ostream& out, const ast::Expression* expr) {
-    // For constants, replace literal 0 with 1.
-    if (const auto* val = builder_.Sem().Get(expr)->ConstantValue()) {
-        if (!val->AnyZero()) {
-            return EmitExpression(out, expr);
-        }
-
-        auto* ty = val->Type();
-
-        if (ty->IsAnyOf<sem::I32, sem::U32>()) {
-            return EmitValue(out, ty, 1);
-        }
-
-        if (auto* vec = ty->As<sem::Vector>()) {
-            auto* elem_ty = vec->type();
-
-            if (!EmitType(out, ty, ast::AddressSpace::kNone, ast::Access::kUndefined, "")) {
-                return false;
-            }
-
-            out << "(";
-            for (size_t i = 0; i < vec->Width(); ++i) {
-                if (i != 0) {
-                    out << ", ";
-                }
-                auto s = val->Index(i)->As<AInt>();
-                if (!EmitValue(out, elem_ty, (s == 0) ? 1 : static_cast<int>(s))) {
-                    return false;
-                }
-            }
-            out << ")";
-            return true;
-        }
-
-        TINT_ICE(Writer, diagnostics_)
-            << "EmitExpressionOrOneIfZero expects integer scalar or vector";
-        return false;
-    }
-
-    auto* ty = TypeOf(expr)->UnwrapRef();
-
-    // For non-constants, we need to emit runtime code to check if the value is 0,
-    // and return 1 in that case.
-    std::string zero;
-    {
-        std::ostringstream ss;
-        EmitValue(ss, ty, 0);
-        zero = ss.str();
-    }
-    std::string one;
-    {
-        std::ostringstream ss;
-        EmitValue(ss, ty, 1);
-        one = ss.str();
-    }
-
-    // For identifiers, no need for a function call as it's fine to evaluate
-    // `expr` more than once.
-    if (expr->Is<ast::IdentifierExpression>()) {
-        out << "(";
-        if (!EmitExpression(out, expr)) {
-            return false;
-        }
-        out << " == " << zero << " ? " << one << " : ";
-        if (!EmitExpression(out, expr)) {
-            return false;
-        }
-        out << ")";
-        return true;
-    }
-
-    // For non-identifier expressions, call a function to make sure `expr` is only
-    // evaluated once.
-    auto name = utils::GetOrCreate(value_or_one_if_zero_, ty, [&]() -> std::string {
-        // Example:
-        // int4 tint_value_or_one_if_zero_int4(int4 value) {
-        //   return value == 0 ? 0 : value;
-        // }
-        std::string ty_name;
-        {
-            std::ostringstream ss;
-            if (!EmitType(ss, ty, tint::ast::AddressSpace::kUndefined, ast::Access::kUndefined,
-                          "")) {
-                return "";
-            }
-            ty_name = ss.str();
-        }
-
-        std::string fn = UniqueIdentifier("value_or_one_if_zero_" + ty_name);
-        line(&helpers_) << ty_name << " " << fn << "(" << ty_name << " value) {";
-        {
-            ScopedIndent si(&helpers_);
-            line(&helpers_) << "return value == " << zero << " ? " << one << " : value;";
-        }
-        line(&helpers_) << "}";
-        line(&helpers_);
-        return fn;
-    });
-
-    if (name.empty()) {
-        return false;
-    }
-
-    out << name << "(";
-    if (!EmitExpression(out, expr)) {
-        return false;
-    }
-    out << ")";
-    return true;
-}
-
 bool GeneratorImpl::EmitBinary(std::ostream& out, const ast::BinaryExpression* expr) {
     if (expr->op == ast::BinaryOp::kLogicalAnd || expr->op == ast::BinaryOp::kLogicalOr) {
         auto name = UniqueIdentifier(kTempNamePrefix);
@@ -892,21 +784,9 @@
             break;
         case ast::BinaryOp::kDivide:
             out << "/";
-            // BUG(crbug.com/tint/1083): Integer divide/modulo by zero is a FXC
-            // compile error, and undefined behavior in WGSL.
-            if (TypeOf(expr->rhs)->UnwrapRef()->is_integer_scalar_or_vector()) {
-                out << " ";
-                return EmitExpressionOrOneIfZero(out, expr->rhs);
-            }
             break;
         case ast::BinaryOp::kModulo:
             out << "%";
-            // BUG(crbug.com/tint/1083): Integer divide/modulo by zero is a FXC
-            // compile error, and undefined behavior in WGSL.
-            if (TypeOf(expr->rhs)->UnwrapRef()->is_integer_scalar_or_vector()) {
-                out << " ";
-                return EmitExpressionOrOneIfZero(out, expr->rhs);
-            }
             break;
         case ast::BinaryOp::kNone:
             diagnostics_.add_error(diag::System::Writer, "missing binary operation type");
diff --git a/src/tint/writer/hlsl/generator_impl.h b/src/tint/writer/hlsl/generator_impl.h
index eca7734..16b824b 100644
--- a/src/tint/writer/hlsl/generator_impl.h
+++ b/src/tint/writer/hlsl/generator_impl.h
@@ -93,12 +93,6 @@
     /// @param stmt the statement to emit
     /// @returns true if the statement was emitted successfully
     bool EmitAssign(const ast::AssignmentStatement* stmt);
-    /// Emits code such that if `expr` is zero, it emits one, else `expr`.
-    /// Used to avoid divide-by-zeros by substituting constant zeros with ones.
-    /// @param out the output of the expression stream
-    /// @param expr the expression
-    /// @returns true if the expression was emitted, false otherwise
-    bool EmitExpressionOrOneIfZero(std::ostream& out, const ast::Expression* expr);
     /// Handles generating a binary expression
     /// @param out the output of the expression stream
     /// @param expr the binary expression
diff --git a/src/tint/writer/hlsl/generator_impl_binary_test.cc b/src/tint/writer/hlsl/generator_impl_binary_test.cc
index 1163078..6e2b3c7 100644
--- a/src/tint/writer/hlsl/generator_impl_binary_test.cc
+++ b/src/tint/writer/hlsl/generator_impl_binary_test.cc
@@ -653,295 +653,5 @@
 )");
 }
 
-namespace HlslGeneratorDivMod {
-
-struct Params {
-    enum class Type { Div, Mod };
-    Type type;
-};
-
-struct HlslGeneratorDivModTest : TestParamHelper<Params> {
-    std::string Token() { return GetParam().type == Params::Type::Div ? "/" : "%"; }
-
-    template <typename... Args>
-    auto Op(Args... args) {
-        return GetParam().type == Params::Type::Div ? Div(std::forward<Args>(args)...)
-                                                    : Mod(std::forward<Args>(args)...);
-    }
-};
-
-INSTANTIATE_TEST_SUITE_P(HlslGeneratorImplTest,
-                         HlslGeneratorDivModTest,
-                         testing::Values(Params{Params::Type::Div}, Params{Params::Type::Mod}));
-
-TEST_P(HlslGeneratorDivModTest, DivOrModByLiteralZero_i32) {
-    Func("fn", utils::Empty, ty.void_(),
-         utils::Vector{
-             Decl(Var("a", ty.i32())),
-             Decl(Let("r", Op("a", 0_i))),
-         });
-
-    GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate());
-    EXPECT_EQ(gen.result(), R"(void fn() {
-  int a = 0;
-  const int r = (a )" + Token() +
-                                R"( 1);
-}
-)");
-}
-
-TEST_P(HlslGeneratorDivModTest, DivOrModByLiteralZero_u32) {
-    Func("fn", utils::Empty, ty.void_(),
-         utils::Vector{
-             Decl(Var("a", ty.u32())),
-             Decl(Let("r", Op("a", 0_u))),
-         });
-
-    GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate());
-    EXPECT_EQ(gen.result(), R"(void fn() {
-  uint a = 0u;
-  const uint r = (a )" + Token() +
-                                R"( 1u);
-}
-)");
-}
-
-TEST_P(HlslGeneratorDivModTest, DivOrModByLiteralZero_vec_by_vec_i32) {
-    Func("fn", utils::Empty, ty.void_(),
-         utils::Vector{
-             Decl(Var("a", vec4<i32>(100_i, 100_i, 100_i, 100_i))),
-             Decl(Let("r", Op("a", vec4<i32>(50_i, 0_i, 25_i, 0_i)))),
-         });
-
-    GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate());
-    EXPECT_EQ(gen.result(), R"(void fn() {
-  int4 a = (100).xxxx;
-  const int4 r = (a )" + Token() +
-                                R"( int4(50, 1, 25, 1));
-}
-)");
-}
-
-TEST_P(HlslGeneratorDivModTest, DivOrModByLiteralZero_vec_by_scalar_i32) {
-    Func("fn", utils::Empty, ty.void_(),
-         utils::Vector{
-             Decl(Var("a", vec4<i32>(100_i, 100_i, 100_i, 100_i))),
-             Decl(Let("r", Op("a", 0_i))),
-         });
-
-    GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate());
-    EXPECT_EQ(gen.result(), R"(void fn() {
-  int4 a = (100).xxxx;
-  const int4 r = (a )" + Token() +
-                                R"( 1);
-}
-)");
-}
-
-TEST_P(HlslGeneratorDivModTest, DivOrModByIdentifier_i32) {
-    Func("fn", utils::Vector{Param("b", ty.i32())}, ty.void_(),
-         utils::Vector{
-             Decl(Var("a", ty.i32())),
-             Decl(Let("r", Op("a", "b"))),
-         });
-
-    GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate());
-    EXPECT_EQ(gen.result(), R"(void fn(int b) {
-  int a = 0;
-  const int r = (a )" + Token() +
-                                R"( (b == 0 ? 1 : b));
-}
-)");
-}
-
-TEST_P(HlslGeneratorDivModTest, DivOrModByIdentifier_u32) {
-    Func("fn", utils::Vector{Param("b", ty.u32())}, ty.void_(),
-         utils::Vector{
-             Decl(Var("a", ty.u32())),
-             Decl(Let("r", Op("a", "b"))),
-         });
-
-    GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate());
-    EXPECT_EQ(gen.result(), R"(void fn(uint b) {
-  uint a = 0u;
-  const uint r = (a )" + Token() +
-                                R"( (b == 0u ? 1u : b));
-}
-)");
-}
-
-TEST_P(HlslGeneratorDivModTest, DivOrModByIdentifier_vec_by_vec_i32) {
-    Func("fn", utils::Vector{Param("b", ty.vec3<i32>())}, ty.void_(),
-         utils::Vector{
-             Decl(Var("a", ty.vec3<i32>())),
-             Decl(Let("r", Op("a", "b"))),
-         });
-
-    GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate());
-    EXPECT_EQ(gen.result(), R"(void fn(int3 b) {
-  int3 a = int3(0, 0, 0);
-  const int3 r = (a )" + Token() +
-                                R"( (b == int3(0, 0, 0) ? int3(1, 1, 1) : b));
-}
-)");
-}
-
-TEST_P(HlslGeneratorDivModTest, DivOrModByIdentifier_vec_by_scalar_i32) {
-    Func("fn", utils::Vector{Param("b", ty.i32())}, ty.void_(),
-         utils::Vector{
-             Decl(Var("a", ty.vec3<i32>())),
-             Decl(Let("r", Op("a", "b"))),
-         });
-
-    GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate());
-    EXPECT_EQ(gen.result(), R"(void fn(int b) {
-  int3 a = int3(0, 0, 0);
-  const int3 r = (a )" + Token() +
-                                R"( (b == 0 ? 1 : b));
-}
-)");
-}
-
-TEST_P(HlslGeneratorDivModTest, DivOrModByExpression_i32) {
-    Func("zero", utils::Empty, ty.i32(),
-         utils::Vector{
-             Return(Expr(0_i)),
-         });
-
-    Func("fn", utils::Empty, ty.void_(),
-         utils::Vector{
-             Decl(Var("a", ty.i32())),
-             Decl(Let("r", Op("a", Call("zero")))),
-         });
-
-    GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate());
-    EXPECT_EQ(gen.result(), R"(int value_or_one_if_zero_int(int value) {
-  return value == 0 ? 1 : value;
-}
-
-int zero() {
-  return 0;
-}
-
-void fn() {
-  int a = 0;
-  const int r = (a )" + Token() +
-                                R"( value_or_one_if_zero_int(zero()));
-}
-)");
-}
-
-TEST_P(HlslGeneratorDivModTest, DivOrModByExpression_u32) {
-    Func("zero", utils::Empty, ty.u32(),
-         utils::Vector{
-             Return(Expr(0_u)),
-         });
-
-    Func("fn", utils::Empty, ty.void_(),
-         utils::Vector{
-             Decl(Var("a", ty.u32())),
-             Decl(Let("r", Op("a", Call("zero")))),
-         });
-
-    GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate());
-    EXPECT_EQ(gen.result(), R"(uint value_or_one_if_zero_uint(uint value) {
-  return value == 0u ? 1u : value;
-}
-
-uint zero() {
-  return 0u;
-}
-
-void fn() {
-  uint a = 0u;
-  const uint r = (a )" + Token() +
-                                R"( value_or_one_if_zero_uint(zero()));
-}
-)");
-}
-
-TEST_P(HlslGeneratorDivModTest, DivOrModByExpression_vec_by_vec_i32) {
-    Func("zero", utils::Empty, ty.vec3<i32>(),
-         utils::Vector{
-             Return(vec3<i32>(0_i, 0_i, 0_i)),
-         });
-
-    Func("fn", utils::Empty, ty.void_(),
-         utils::Vector{
-             Decl(Var("a", ty.vec3<i32>())),
-             Decl(Let("r", Op("a", Call("zero")))),
-         });
-
-    GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate());
-    EXPECT_EQ(gen.result(), R"(int3 value_or_one_if_zero_int3(int3 value) {
-  return value == int3(0, 0, 0) ? int3(1, 1, 1) : value;
-}
-
-int3 zero() {
-  return (0).xxx;
-}
-
-void fn() {
-  int3 a = int3(0, 0, 0);
-  const int3 r = (a )" + Token() +
-                                R"( value_or_one_if_zero_int3(zero()));
-}
-)");
-}
-
-TEST_P(HlslGeneratorDivModTest, DivOrModByExpression_vec_by_scalar_i32) {
-    Func("zero", utils::Empty, ty.i32(),
-         utils::Vector{
-             Return(0_i),
-         });
-
-    Func("fn", utils::Empty, ty.void_(),
-         utils::Vector{
-             Decl(Var("a", ty.vec3<i32>())),
-             Decl(Let("r", Op("a", Call("zero")))),
-         });
-
-    GeneratorImpl& gen = Build();
-
-    ASSERT_TRUE(gen.Generate());
-    EXPECT_EQ(gen.result(), R"(int value_or_one_if_zero_int(int value) {
-  return value == 0 ? 1 : value;
-}
-
-int zero() {
-  return 0;
-}
-
-void fn() {
-  int3 a = int3(0, 0, 0);
-  const int3 r = (a )" + Token() +
-                                R"( value_or_one_if_zero_int(zero()));
-}
-)");
-}
-}  // namespace HlslGeneratorDivMod
-
 }  // namespace
 }  // namespace tint::writer::hlsl
diff --git a/src/tint/writer/msl/generator_impl.cc b/src/tint/writer/msl/generator_impl.cc
index 88578df..351a6d4 100644
--- a/src/tint/writer/msl/generator_impl.cc
+++ b/src/tint/writer/msl/generator_impl.cc
@@ -167,6 +167,9 @@
 
     manager.Add<transform::DisableUniformityAnalysis>();
 
+    // ExpandCompoundAssignment must come before BuiltinPolyfill
+    manager.Add<transform::ExpandCompoundAssignment>();
+
     {  // Builtin polyfills
         transform::BuiltinPolyfill::Builtins polyfills;
         polyfills.acosh = transform::BuiltinPolyfill::Level::kRangeCheck;
@@ -177,6 +180,7 @@
         polyfills.first_leading_bit = true;
         polyfills.first_trailing_bit = true;
         polyfills.insert_bits = transform::BuiltinPolyfill::Level::kClampParameters;
+        polyfills.int_div_mod = true;
         polyfills.texture_sample_base_clamp_to_edge_2d_f32 = true;
         data.Add<transform::BuiltinPolyfill::Config>(polyfills);
         manager.Add<transform::BuiltinPolyfill>();
@@ -224,7 +228,6 @@
         manager.Add<transform::ZeroInitWorkgroupMemory>();
     }
     manager.Add<transform::CanonicalizeEntryPointIO>();
-    manager.Add<transform::ExpandCompoundAssignment>();
     manager.Add<transform::PromoteSideEffectsToDecl>();
     manager.Add<transform::PromoteInitializersToLet>();
 
diff --git a/src/tint/writer/spirv/generator_impl.cc b/src/tint/writer/spirv/generator_impl.cc
index 30dd186..18c7511 100644
--- a/src/tint/writer/spirv/generator_impl.cc
+++ b/src/tint/writer/spirv/generator_impl.cc
@@ -48,6 +48,9 @@
 
     manager.Add<transform::DisableUniformityAnalysis>();
 
+    // ExpandCompoundAssignment must come before BuiltinPolyfill
+    manager.Add<transform::ExpandCompoundAssignment>();
+
     {  // Builtin polyfills
         transform::BuiltinPolyfill::Builtins polyfills;
         polyfills.acosh = transform::BuiltinPolyfill::Level::kRangeCheck;
@@ -60,6 +63,7 @@
         polyfills.first_leading_bit = true;
         polyfills.first_trailing_bit = true;
         polyfills.insert_bits = transform::BuiltinPolyfill::Level::kClampParameters;
+        polyfills.int_div_mod = true;
         polyfills.saturate = true;
         polyfills.texture_sample_base_clamp_to_edge_2d_f32 = true;
         polyfills.quantize_to_vec_f16 = true;  // crbug.com/tint/1741
@@ -80,7 +84,6 @@
         manager.Add<transform::ZeroInitWorkgroupMemory>();
     }
     manager.Add<transform::RemoveUnreachableStatements>();
-    manager.Add<transform::ExpandCompoundAssignment>();
     manager.Add<transform::PromoteSideEffectsToDecl>();
     manager.Add<transform::SimplifyPointers>();  // Required for arrayLength()
     manager.Add<transform::RemovePhonies>();