Import Tint changes from Dawn

Changes:
  - cc636969b7b7bb04043ca15673c82dc94e7fdc17 [tint][wgsl] Replace size_t with uint32_t for IDs, Source. by Ben Clayton <bclayton@google.com>
  - 144ebed4c7ee7c6ecddeedfa81d607bbf256e8f2 tint/hlsl: widen scope of workaround of DXC bug with cons... by Antonio Maiorano <amaiorano@google.com>
  - 0880e0643379ff2535187662cb21a803ce974cfa Rename @index -> @blend_src by Corentin Wallez <cwallez@chromium.org>
  - dfb4eb05d2a4e65e22fbb3b0441e74f136fa9190 Add @blend_src as an alias for @index. by Corentin Wallez <cwallez@chromium.org>
  - 31a1592736e3465599145aa6f9ea9333bfd05196 [tint][sem] Replace more uses of std::vector with tint::V... by Ben Clayton <bclayton@google.com>
  - 8be957f698baa57c87eca49aa5f93315081172a3 [tint] Replace std containers with Tint's containers in n... by Ben Clayton <bclayton@google.com>
  - 5ff42f8900bae798cfc57d45953d387a994089a9 [tint][core] Don't create throw-away StructMembers by Ben Clayton <bclayton@google.com>
  - f9ffc45753f3eb668c058b0b8e62a77a0a3cb331 [tint][resolver] Don't create throwaway sem::Parameters by Ben Clayton <bclayton@google.com>
  - 10f1b6bdb4a7bd8e316d389230471654290cda66 [tint][utils] Fix Hashmap's small vector size by Ben Clayton <bclayton@google.com>
  - 80144d262ecef9331783faaa8ca4dfa6dfcfd60c [tint][hlsl] Fix names of vector / matrix setter utils by Ben Clayton <bclayton@google.com>
  - 652353953be36fd60ffc26788e7b5f3833a41dc4 [tint] Move unrestricted_pointer_paramters to kShippedWit... by Ben Clayton <bclayton@google.com>
  - 4eeb0bd6e2dc3f608a079b7e62ff3fd3befccdef Implement push_constant-based firstIndex transform. by Stephen White <senorblanco@chromium.org>
  - c65ac40803c289104fa26156113ebfd37280dd7a [tint][wgsl] Consider builtin calls as part of ptr analysis by Ben Clayton <bclayton@google.com>
  - 868380669fb051756b154d531beb805a990a6674 Move ClampFragDepth transform to WGSL transforms. by Stephen White <senorblanco@chromium.org>
  - c6ed507397f87c5ee96b01d2eed3eb77a0899c25 [tint][wgsl] Use tint's containers for ptr analysis by Ben Clayton <bclayton@google.com>
  - 02262d895ff373badfc4d2233f8e14108d185687 [tint][msl] Fix C++17 warning. by Ben Clayton <bclayton@google.com>
  - a840d5132ac88153efb26c92a24178d7d68eddef Replace CMAKE_SOURCE_DIR with PROJECT_SOURCE_DIR by amirsojoodi <amir@distributive.network>
  - 15d2710c84b3420bd20da25b8ccbe5681521df84 Remove data returned from `TextureBuiltinFromUniform` tra... by dan sinclair <dsinclair@chromium.org>
GitOrigin-RevId: cc636969b7b7bb04043ca15673c82dc94e7fdc17
Change-Id: I949ec6ab831105ee01a2b5e92e96db023a9a469e
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/171360
Commit-Queue: Ben Clayton <bclayton@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index 9a4eba5..eb95b38 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -690,10 +690,10 @@
 
   install(FILES ${TINT_HEADERS}  DESTINATION  ${CMAKE_INSTALL_INCLUDEDIR}/tint/)
 
-  file(GLOB_RECURSE TINT_SRC_HEADERS RELATIVE ${CMAKE_SOURCE_DIR}/src/tint/ "*.h")
+  file(GLOB_RECURSE TINT_SRC_HEADERS RELATIVE ${TINT_ROOT_SOURCE_DIR} "*.h")
 
   foreach(TINT_HEADER_FILE ${TINT_SRC_HEADERS})
       get_filename_component(TINT_HEADER_DIR ${TINT_HEADER_FILE} DIRECTORY)
-      install(FILES ${CMAKE_SOURCE_DIR}/src/tint/${TINT_HEADER_FILE}  DESTINATION  ${CMAKE_INSTALL_INCLUDEDIR}/src/tint/${TINT_HEADER_DIR})
+      install(FILES ${TINT_ROOT_SOURCE_DIR}/${TINT_HEADER_FILE}  DESTINATION  ${CMAKE_INSTALL_INCLUDEDIR}/src/tint/${TINT_HEADER_DIR})
   endforeach ()
 endif()
diff --git a/src/tint/api/options/texture_builtins_from_uniform.h b/src/tint/api/options/texture_builtins_from_uniform.h
index bddb274..ac107e5 100644
--- a/src/tint/api/options/texture_builtins_from_uniform.h
+++ b/src/tint/api/options/texture_builtins_from_uniform.h
@@ -28,8 +28,7 @@
 #ifndef SRC_TINT_API_OPTIONS_TEXTURE_BUILTINS_FROM_UNIFORM_H_
 #define SRC_TINT_API_OPTIONS_TEXTURE_BUILTINS_FROM_UNIFORM_H_
 
-#include <unordered_map>
-#include <utility>
+#include <vector>
 
 #include "src/tint/api/common/binding_point.h"
 #include "src/tint/utils/reflection/reflection.h"
@@ -39,25 +38,16 @@
 /// Options used to specify a mapping of binding points to indices into a UBO
 /// from which to load buffer sizes.
 struct TextureBuiltinsFromUniformOptions {
-    /// Indicate the type of field for each entry to push.
-    enum class Field {
-        /// The number of mip levels of the bonnd texture view.
-        TextureNumLevels,
-        /// The number of samples per texel of the bound multipsampled texture.
-        TextureNumSamples,
-    };
-
-    /// Records the field and the byte offset of the data to push in the internal uniform buffer.
-    using FieldAndOffset = std::pair<Field, uint32_t>;
-    /// Maps from binding point to data entry with the information to populate the data.
-    using BindingPointToFieldAndOffset = std::unordered_map<BindingPoint, FieldAndOffset>;
-
     /// The binding point to use to generate a uniform buffer from which to read
     /// buffer sizes.
     BindingPoint ubo_binding = {};
 
+    /// Ordered list of binding points in the uniform buffer for polyfilling `textureNumSamples` and
+    /// `textureNumLevels`
+    std::vector<BindingPoint> ubo_bindingpoint_ordering = {};
+
     /// Reflect the fields of this class so that it can be used by tint::ForeachField()
-    TINT_REFLECT(ubo_binding);
+    TINT_REFLECT(ubo_binding, ubo_bindingpoint_ordering);
 };
 
 }  // namespace tint
diff --git a/src/tint/cmd/tint/main.cc b/src/tint/cmd/tint/main.cc
index 474088e..9797feb 100644
--- a/src/tint/cmd/tint/main.cc
+++ b/src/tint/cmd/tint/main.cc
@@ -1040,6 +1040,8 @@
     std::cerr << "GLSL writer not enabled in tint build" << std::endl;
     return false;
 #else
+    tint::inspector::Inspector inspector(program);
+
     auto generate = [&](const tint::Program& prg, const std::string entry_point_name,
                         [[maybe_unused]] tint::ast::PipelineStage stage) -> bool {
         tint::glsl::writer::Options gen_options;
@@ -1049,7 +1051,20 @@
 
         tint::TextureBuiltinsFromUniformOptions textureBuiltinsFromUniform;
         constexpr uint32_t kMaxBindGroups = 4u;
+
         textureBuiltinsFromUniform.ubo_binding = {kMaxBindGroups, 0u};
+
+        auto textureBuiltinsFromUniformData = inspector.GetTextureQueries(entry_point_name);
+        if (!textureBuiltinsFromUniformData.empty()) {
+            for (size_t i = 0; i < textureBuiltinsFromUniformData.size(); ++i) {
+                const auto& info = textureBuiltinsFromUniformData[i];
+
+                // This is the unmodified binding point from the WGSL shader.
+                tint::BindingPoint srcBindingPoint{info.group, info.binding};
+                textureBuiltinsFromUniform.ubo_bindingpoint_ordering.emplace_back(srcBindingPoint);
+            }
+        }
+
         gen_options.texture_builtins_from_uniform = std::move(textureBuiltinsFromUniform);
 
         auto result = tint::glsl::writer::Generate(prg, gen_options, entry_point_name);
@@ -1086,8 +1101,6 @@
         return true;
     };
 
-    tint::inspector::Inspector inspector(program);
-
     if (inspector.GetEntryPoints().empty()) {
         // Pass empty string here so that the GLSL generator will generate
         // code for all functions, reachable or not.
diff --git a/src/tint/lang/core/attribute.cc b/src/tint/lang/core/attribute.cc
index 2568294..c6fa693 100644
--- a/src/tint/lang/core/attribute.cc
+++ b/src/tint/lang/core/attribute.cc
@@ -48,6 +48,9 @@
     if (str == "binding") {
         return Attribute::kBinding;
     }
+    if (str == "blend_src") {
+        return Attribute::kBlendSrc;
+    }
     if (str == "builtin") {
         return Attribute::kBuiltin;
     }
@@ -69,9 +72,6 @@
     if (str == "id") {
         return Attribute::kId;
     }
-    if (str == "index") {
-        return Attribute::kIndex;
-    }
     if (str == "interpolate") {
         return Attribute::kInterpolate;
     }
@@ -104,6 +104,8 @@
             return "align";
         case Attribute::kBinding:
             return "binding";
+        case Attribute::kBlendSrc:
+            return "blend_src";
         case Attribute::kBuiltin:
             return "builtin";
         case Attribute::kColor:
@@ -118,8 +120,6 @@
             return "group";
         case Attribute::kId:
             return "id";
-        case Attribute::kIndex:
-            return "index";
         case Attribute::kInterpolate:
             return "interpolate";
         case Attribute::kInvariant:
diff --git a/src/tint/lang/core/attribute.h b/src/tint/lang/core/attribute.h
index 46d7619..28dd3d4 100644
--- a/src/tint/lang/core/attribute.h
+++ b/src/tint/lang/core/attribute.h
@@ -51,6 +51,7 @@
     kUndefined,
     kAlign,
     kBinding,
+    kBlendSrc,
     kBuiltin,
     kColor,
     kCompute,
@@ -58,7 +59,6 @@
     kFragment,
     kGroup,
     kId,
-    kIndex,
     kInterpolate,
     kInvariant,
     kLocation,
@@ -86,9 +86,9 @@
 Attribute ParseAttribute(std::string_view str);
 
 constexpr std::string_view kAttributeStrings[] = {
-    "align",    "binding",  "builtin", "color",  "compute",        "diagnostic",
-    "fragment", "group",    "id",      "index",  "interpolate",    "invariant",
-    "location", "must_use", "size",    "vertex", "workgroup_size",
+    "align",      "binding",  "blend_src", "builtin", "color",          "compute",
+    "diagnostic", "fragment", "group",     "id",      "interpolate",    "invariant",
+    "location",   "must_use", "size",      "vertex",  "workgroup_size",
 };
 
 }  // namespace tint::core
diff --git a/src/tint/lang/core/attribute_bench.cc b/src/tint/lang/core/attribute_bench.cc
index 16a660c..924456c 100644
--- a/src/tint/lang/core/attribute_bench.cc
+++ b/src/tint/lang/core/attribute_bench.cc
@@ -59,111 +59,111 @@
         "bindiivg",
         "8WWnding",
         "bxxding",
-        "bXltigg",
-        "ultXn",
-        "buil3in",
+        "bXnd_ggrc",
+        "bleXVuc",
+        "3lend_src",
+        "blend_src",
+        "blend_sEc",
+        "blTTPn_src",
+        "ddlen_sxxc",
+        "b44iltin",
+        "buSSltVVn",
+        "RuiR22n",
         "builtin",
-        "Euiltin",
-        "bPTTltin",
-        "builtdxx",
-        "c44lor",
-        "coVVSSr",
-        "22RRr",
+        "bFlt9n",
+        "buitin",
+        "VOORRHin",
+        "olyr",
+        "n77rrllGr",
+        "colo40",
         "color",
-        "cFor",
-        "colr",
-        "ROOHVr",
-        "copuye",
-        "llnorrp77te",
-        "comp4t00",
+        "oor",
+        "cozz",
+        "1ippor",
+        "compuXXe",
+        "9II5onnpute",
+        "comaSSrHHYe",
         "compute",
-        "opooe",
-        "zzpute",
-        "ciimppu1",
-        "XXiagnostic",
-        "IIia99nonnt55c",
-        "dYagSSrrstHHac",
+        "cokke",
+        "jomgRu",
+        "cbput",
+        "diagnojtic",
+        "dagnostic",
+        "diagosqi",
         "diagnostic",
-        "dakkoHtc",
-        "jiagnsgRR",
-        "diagbost",
-        "fragjent",
-        "fragmnt",
-        "frqent",
+        "diagnNNstc",
+        "dignvvtic",
+        "diagnostiQ",
+        "frafmrt",
+        "frjgment",
+        "frawNN82t",
         "fragment",
-        "fragenNN",
-        "ravvent",
-        "frgmQQnt",
-        "grof",
-        "grojp",
-        "NNrw2u",
+        "fragmnt",
+        "frrrgment",
+        "fGagment",
+        "grFFup",
+        "gE",
+        "grrrp",
         "group",
-        "grup",
-        "grroup",
-        "Group",
-        "iFF",
-        "NN",
-        "iAA",
+        "grp",
+        "grJJD",
+        "gu",
+        "K",
+        "FsJ",
+        "KK_v",
         "id",
-        "d",
-        "L",
-        "yy",
-        "nek",
-        "indx",
-        "Jndx",
-        "index",
-        "incex",
-        "iOdex",
-        "__nttKKvv",
-        "int8rpoxx5e",
-        "inteqq__lte",
-        "interpqlate",
-        "interpolate",
-        "33ntOpolat66",
+        "5O8",
+        "",
+        "DDbbB",
+        "interpolaKKe",
+        "33terpolate",
         "intoott6QQlate",
+        "interpolate",
         "66terpolate",
-        "zzxvO6rint",
-        "invayyiant",
+        "intxp66latOz",
+        "yynterpolate",
         "HHnariZt",
-        "invariant",
         "iWW44rianq",
         "iOOvaiant",
+        "invariant",
         "ivariYnt",
-        "ltion",
-        "loaFion",
+        "nvaria",
+        "invaranF",
         "wocatio",
-        "location",
         "Kcatoff",
         "qocKKtion",
+        "location",
         "lFcmmt3on",
-        "mustuse",
-        "must_se",
+        "locaion",
+        "locaton",
         "ubbt_ube",
-        "must_use",
         "mstiius",
         "muqt_uOe",
+        "must_use",
         "muTTt_usvv",
-        "FFize",
-        "QP00",
+        "FFust_use",
+        "fu00QPus",
         "siPe",
-        "size",
         "sis77",
         "CiRbbe",
+        "size",
         "sizXX",
-        "CCrtOOOO",
-        "vrsuL",
+        "qCCOOO",
+        "szL",
         "verteX",
-        "vertex",
         "verte",
         "qqrx",
+        "vertex",
         "verte22",
-        "workgou0yzzizXX",
-        "workgrop_VPize",
+        "vzzyrte",
+        "eriVPx",
         "wokgroupnnsCze",
-        "workgroup_size",
         "workgrouq_sizHA",
         "workgrup_size",
+        "workgroup_size",
         "forroupKKsize",
+        "workrop_Pigge",
+        "workgoup_size",
     };
     for (auto _ : state) {
         for (auto* str : kStrings) {
diff --git a/src/tint/lang/core/attribute_test.cc b/src/tint/lang/core/attribute_test.cc
index 10d8e72..4d4ed0d 100644
--- a/src/tint/lang/core/attribute_test.cc
+++ b/src/tint/lang/core/attribute_test.cc
@@ -59,6 +59,7 @@
 static constexpr Case kValidCases[] = {
     {"align", Attribute::kAlign},
     {"binding", Attribute::kBinding},
+    {"blend_src", Attribute::kBlendSrc},
     {"builtin", Attribute::kBuiltin},
     {"color", Attribute::kColor},
     {"compute", Attribute::kCompute},
@@ -66,7 +67,6 @@
     {"fragment", Attribute::kFragment},
     {"group", Attribute::kGroup},
     {"id", Attribute::kId},
-    {"index", Attribute::kIndex},
     {"interpolate", Attribute::kInterpolate},
     {"invariant", Attribute::kInvariant},
     {"location", Attribute::kLocation},
@@ -83,30 +83,30 @@
     {"bind1ng", Attribute::kUndefined},
     {"bqnJing", Attribute::kUndefined},
     {"bindin7ll", Attribute::kUndefined},
-    {"ppqqiliHH", Attribute::kUndefined},
-    {"bucv", Attribute::kUndefined},
-    {"biltGn", Attribute::kUndefined},
-    {"covior", Attribute::kUndefined},
-    {"co8WWr", Attribute::kUndefined},
-    {"Mxxlo", Attribute::kUndefined},
-    {"cXputgg", Attribute::kUndefined},
-    {"opuXe", Attribute::kUndefined},
-    {"comp3te", Attribute::kUndefined},
-    {"diagnostiE", Attribute::kUndefined},
-    {"TTiagnosPPi", Attribute::kUndefined},
-    {"diagdoxxtic", Attribute::kUndefined},
-    {"44ragment", Attribute::kUndefined},
-    {"fSSagmenVV", Attribute::kUndefined},
-    {"Rag2Rent", Attribute::kUndefined},
-    {"gFup", Attribute::kUndefined},
-    {"grop", Attribute::kUndefined},
-    {"ROOHVp", Attribute::kUndefined},
-    {"y", Attribute::kUndefined},
-    {"Gn77rl", Attribute::kUndefined},
-    {"04d", Attribute::kUndefined},
-    {"oox", Attribute::kUndefined},
-    {"inzz", Attribute::kUndefined},
-    {"1ippex", Attribute::kUndefined},
+    {"blen_sppqHH", Attribute::kUndefined},
+    {"blen_r", Attribute::kUndefined},
+    {"bbndGsrc", Attribute::kUndefined},
+    {"builiivn", Attribute::kUndefined},
+    {"8WWiltin", Attribute::kUndefined},
+    {"bxxltin", Attribute::kUndefined},
+    {"cggor", Attribute::kUndefined},
+    {"VoX", Attribute::kUndefined},
+    {"colo3", Attribute::kUndefined},
+    {"Eompute", Attribute::kUndefined},
+    {"cPTTpute", Attribute::kUndefined},
+    {"compudxx", Attribute::kUndefined},
+    {"diagno44tic", Attribute::kUndefined},
+    {"diaVVSnostic", Attribute::kUndefined},
+    {"2iRRnostic", Attribute::kUndefined},
+    {"rFg9ent", Attribute::kUndefined},
+    {"frament", Attribute::kUndefined},
+    {"ROOrHgeVt", Attribute::kUndefined},
+    {"royp", Attribute::kUndefined},
+    {"n77rrloGp", Attribute::kUndefined},
+    {"grou40", Attribute::kUndefined},
+    {"HH", Attribute::kUndefined},
+    {"p", Attribute::kUndefined},
+    {"1ii", Attribute::kUndefined},
     {"interpoXXate", Attribute::kUndefined},
     {"intII99r55olate", Attribute::kUndefined},
     {"intaarpoSSrHHYe", Attribute::kUndefined},
diff --git a/src/tint/lang/core/core.def b/src/tint/lang/core/core.def
index 2606174..4a5ed76 100644
--- a/src/tint/lang/core/core.def
+++ b/src/tint/lang/core/core.def
@@ -198,13 +198,13 @@
 enum attribute {
   align
   binding
+  blend_src
   builtin
   compute
   diagnostic
   fragment
   group
   id
-  index
   interpolate
   invariant
   location
diff --git a/src/tint/lang/core/ir/binary/decode.cc b/src/tint/lang/core/ir/binary/decode.cc
index 8b3d75d..a65c48f 100644
--- a/src/tint/lang/core/ir/binary/decode.cc
+++ b/src/tint/lang/core/ir/binary/decode.cc
@@ -610,8 +610,8 @@
                 if (attributes_in.has_location()) {
                     attributes_out.location = attributes_in.location();
                 }
-                if (attributes_in.has_index()) {
-                    attributes_out.index = attributes_in.index();
+                if (attributes_in.has_blend_src()) {
+                    attributes_out.blend_src = attributes_in.blend_src();
                 }
                 if (attributes_in.has_color()) {
                     attributes_out.color = attributes_in.color();
diff --git a/src/tint/lang/core/ir/binary/encode.cc b/src/tint/lang/core/ir/binary/encode.cc
index 6f57223..ed03faa 100644
--- a/src/tint/lang/core/ir/binary/encode.cc
+++ b/src/tint/lang/core/ir/binary/encode.cc
@@ -423,8 +423,8 @@
             if (attrs_in.location) {
                 member_out.mutable_attributes()->set_location(*attrs_in.location);
             }
-            if (attrs_in.index) {
-                member_out.mutable_attributes()->set_index(*attrs_in.index);
+            if (attrs_in.blend_src) {
+                member_out.mutable_attributes()->set_blend_src(*attrs_in.blend_src);
             }
             if (attrs_in.color) {
                 member_out.mutable_attributes()->set_color(*attrs_in.color);
diff --git a/src/tint/lang/core/ir/binary/ir.proto b/src/tint/lang/core/ir/binary/ir.proto
index 20933ce..e59a9cb 100644
--- a/src/tint/lang/core/ir/binary/ir.proto
+++ b/src/tint/lang/core/ir/binary/ir.proto
@@ -366,7 +366,7 @@
 ////////////////////////////////////////////////////////////////////////////////
 message AttributesStructMember {
     optional uint32 location = 1;
-    optional uint32 index = 2;
+    optional uint32 blend_src = 2;
     optional uint32 color = 3;
     optional BuiltinValue builtin = 4;
     optional Interpolation interpolation = 5;
diff --git a/src/tint/lang/core/ir/binary/roundtrip_test.cc b/src/tint/lang/core/ir/binary/roundtrip_test.cc
index cf6bbcc..94e2f5b 100644
--- a/src/tint/lang/core/ir/binary/roundtrip_test.cc
+++ b/src/tint/lang/core/ir/binary/roundtrip_test.cc
@@ -276,7 +276,7 @@
 TEST_F(IRBinaryRoundtripTest, StructMemberAttributes) {
     type::StructMemberAttributes attrs{};
     attrs.location = 1;
-    attrs.index = 2;
+    attrs.blend_src = 2;
     attrs.color = 3;
     attrs.builtin = core::BuiltinValue::kFragDepth;
     attrs.interpolation = core::Interpolation{
diff --git a/src/tint/lang/core/ir/disassembler.cc b/src/tint/lang/core/ir/disassembler.cc
index cfa2e30..6397f8a 100644
--- a/src/tint/lang/core/ir/disassembler.cc
+++ b/src/tint/lang/core/ir/disassembler.cc
@@ -506,8 +506,8 @@
             if (v->Attributes().location.has_value()) {
                 out_ << " @location(" << v->Attributes().location.value() << ")";
             }
-            if (v->Attributes().index.has_value()) {
-                out_ << " @index(" << v->Attributes().index.value() << ")";
+            if (v->Attributes().blend_src.has_value()) {
+                out_ << " @blend_src(" << v->Attributes().blend_src.value() << ")";
             }
             if (v->Attributes().interpolation.has_value()) {
                 auto& interp = v->Attributes().interpolation.value();
diff --git a/src/tint/lang/core/ir/var.h b/src/tint/lang/core/ir/var.h
index 4220a4c..d6a73d3 100644
--- a/src/tint/lang/core/ir/var.h
+++ b/src/tint/lang/core/ir/var.h
@@ -42,8 +42,8 @@
 struct IOAttributes {
     /// The value of a `@location` attribute.
     std::optional<uint32_t> location;
-    /// The value of a `@index` attribute.
-    std::optional<uint32_t> index;
+    /// The value of a `@blend_src` attribute.
+    std::optional<uint32_t> blend_src;
     /// The value of a `@builtin` attribute.
     std::optional<core::BuiltinValue> builtin;
     /// The values of a `@interpolate` attribute.
diff --git a/src/tint/lang/core/ir/var_test.cc b/src/tint/lang/core/ir/var_test.cc
index 3353672..5e41f29 100644
--- a/src/tint/lang/core/ir/var_test.cc
+++ b/src/tint/lang/core/ir/var_test.cc
@@ -100,8 +100,8 @@
     EXPECT_TRUE(attrs.location.has_value());
     EXPECT_EQ(3u, attrs.location.value());
 
-    EXPECT_TRUE(attrs.index.has_value());
-    EXPECT_EQ(4u, attrs.index.value());
+    EXPECT_TRUE(attrs.blend_src.has_value());
+    EXPECT_EQ(4u, attrs.blend_src.value());
 
     EXPECT_TRUE(attrs.builtin.has_value());
     EXPECT_EQ(core::BuiltinValue::kFragDepth, attrs.builtin.value());
diff --git a/src/tint/lang/core/type/builtin_structs.cc b/src/tint/lang/core/type/builtin_structs.cc
index 8f319d5..31af873 100644
--- a/src/tint/lang/core/type/builtin_structs.cc
+++ b/src/tint/lang/core/type/builtin_structs.cc
@@ -70,7 +70,11 @@
 
 Struct* CreateModfResult(Manager& types, SymbolTable& symbols, const Type* ty) {
     auto build = [&](core::BuiltinType name, const Type* t) {
-        return types.Struct(symbols.Register(tint::ToString(name)),
+        auto symbol = symbols.Register(tint::ToString(name));
+        if (auto* existing = types.Find<type::Struct>(symbol)) {
+            return existing;
+        }
+        return types.Struct(symbol,
                             {{symbols.Register("fract"), t}, {symbols.Register("whole"), t}});
     };
     return Switch(
@@ -127,9 +131,12 @@
 
 Struct* CreateFrexpResult(Manager& types, SymbolTable& symbols, const Type* ty) {
     auto build = [&](core::BuiltinType name, const Type* fract_ty, const Type* exp_ty) {
+        auto symbol = symbols.Register(tint::ToString(name));
+        if (auto* existing = types.Find<type::Struct>(symbol)) {
+            return existing;
+        }
         return types.Struct(
-            symbols.Register(tint::ToString(name)),
-            {{symbols.Register("fract"), fract_ty}, {symbols.Register("exp"), exp_ty}});
+            symbol, {{symbols.Register("fract"), fract_ty}, {symbols.Register("exp"), exp_ty}});
     };
     return Switch(
         ty,  //
@@ -172,11 +179,14 @@
 
 Struct* CreateAtomicCompareExchangeResult(Manager& types, SymbolTable& symbols, const Type* ty) {
     auto build = [&](core::BuiltinType name) {
-        return types.Struct(symbols.Register(tint::ToString(name)),
-                            {
-                                {symbols.Register("old_value"), ty},
-                                {symbols.Register("exchanged"), types.bool_()},
-                            });
+        auto symbol = symbols.Register(tint::ToString(name));
+        if (auto* existing = types.Find<type::Struct>(symbol)) {
+            return existing;
+        }
+        return types.Struct(symbol, {
+                                        {symbols.Register("old_value"), ty},
+                                        {symbols.Register("exchanged"), types.bool_()},
+                                    });
     };
     return Switch(
         ty,  //
diff --git a/src/tint/lang/core/type/manager.cc b/src/tint/lang/core/type/manager.cc
index ff70390..f158770 100644
--- a/src/tint/lang/core/type/manager.cc
+++ b/src/tint/lang/core/type/manager.cc
@@ -42,6 +42,7 @@
 #include "src/tint/lang/core/type/u32.h"
 #include "src/tint/lang/core/type/vector.h"
 #include "src/tint/lang/core/type/void.h"
+#include "src/tint/utils/macros/compiler.h"
 
 namespace tint::core::type {
 
@@ -198,6 +199,11 @@
 }
 
 core::type::Struct* Manager::Struct(Symbol name, VectorRef<const StructMember*> members) {
+    if (auto* existing = Find<type::Struct>(name); TINT_UNLIKELY(existing)) {
+        TINT_ICE() << "attempting to construct two structs named " << name.NameView();
+        return existing;
+    }
+
     uint32_t max_align = 0u;
     for (const auto& m : members) {
         max_align = std::max(max_align, m->Align());
@@ -208,6 +214,11 @@
 }
 
 core::type::Struct* Manager::Struct(Symbol name, VectorRef<StructMemberDesc> md) {
+    if (auto* existing = Find<type::Struct>(name); TINT_UNLIKELY(existing)) {
+        TINT_ICE() << "attempting to construct two structs named " << name.NameView();
+        return existing;
+    }
+
     tint::Vector<const StructMember*, 4> members;
     uint32_t current_size = 0u;
     uint32_t max_align = 0u;
diff --git a/src/tint/lang/core/type/manager.h b/src/tint/lang/core/type/manager.h
index 6c12a6c..f02290b 100644
--- a/src/tint/lang/core/type/manager.h
+++ b/src/tint/lang/core/type/manager.h
@@ -475,18 +475,21 @@
     /// Create a new structure declaration.
     /// @param name the name of the structure
     /// @param members the list of structure members
+    /// @note a structure must not already exist with the same name
     /// @returns the structure type
     core::type::Struct* Struct(Symbol name, VectorRef<const StructMember*> members);
 
     /// Create a new structure declaration.
     /// @param name the name of the structure
     /// @param members the list of structure member descriptors
+    /// @note a structure must not already exist with the same name
     /// @returns the structure type
     core::type::Struct* Struct(Symbol name, VectorRef<StructMemberDesc> members);
 
     /// Create a new structure declaration.
     /// @param name the name of the structure
     /// @param members the list of structure member descriptors
+    /// @note a structure must not already exist with the same name
     /// @returns the structure type
     core::type::Struct* Struct(Symbol name, std::initializer_list<StructMemberDesc> members) {
         return Struct(name, tint::Vector<StructMemberDesc, 4>(members));
diff --git a/src/tint/lang/core/type/struct.cc b/src/tint/lang/core/type/struct.cc
index c916e4b..ff08e35 100644
--- a/src/tint/lang/core/type/struct.cc
+++ b/src/tint/lang/core/type/struct.cc
@@ -43,8 +43,8 @@
 namespace tint::core::type {
 namespace {
 
-core::type::Flags FlagsFrom(VectorRef<const StructMember*> members) {
-    core::type::Flags flags{
+Flags FlagsFrom(VectorRef<const StructMember*> members) {
+    Flags flags{
         Flag::kConstructable,
         Flag::kCreationFixedFootprint,
         Flag::kFixedFootprint,
@@ -65,6 +65,14 @@
 
 }  // namespace
 
+Struct::Struct(Symbol name)
+    : Base(Hash(tint::TypeInfo::Of<Struct>().full_hashcode, name), type::Flags{}),
+      name_(name),
+      members_{},
+      align_(0),
+      size_(0),
+      size_no_padding_(0) {}
+
 Struct::Struct(Symbol name,
                VectorRef<const StructMember*> members,
                uint32_t align,
diff --git a/src/tint/lang/core/type/struct.h b/src/tint/lang/core/type/struct.h
index 228c8b8..539c27b 100644
--- a/src/tint/lang/core/type/struct.h
+++ b/src/tint/lang/core/type/struct.h
@@ -31,7 +31,6 @@
 #include <cstdint>
 #include <optional>
 #include <string>
-#include <unordered_set>
 #include <utility>
 
 #include "src/tint/lang/core/address_space.h"
@@ -39,6 +38,7 @@
 #include "src/tint/lang/core/interpolation.h"
 #include "src/tint/lang/core/type/node.h"
 #include "src/tint/lang/core/type/type.h"
+#include "src/tint/utils/containers/hashset.h"
 #include "src/tint/utils/containers/vector.h"
 #include "src/tint/utils/symbol/symbol.h"
 
@@ -72,6 +72,12 @@
 class Struct : public Castable<Struct, Type> {
   public:
     /// Constructor
+    /// Note: this constructs an empty structure, which should only be used find a struct with the
+    /// same name in a type::Manager.
+    /// @param name the name of the structure
+    explicit Struct(Symbol name);
+
+    /// Constructor
     /// @param name the name of the structure
     /// @param members the structure members
     /// @param align the byte alignment of the structure
@@ -130,16 +136,14 @@
 
     /// Adds the AddressSpace usage to the structure.
     /// @param usage the storage usage
-    void AddUsage(core::AddressSpace usage) { address_space_usage_.emplace(usage); }
+    void AddUsage(core::AddressSpace usage) { address_space_usage_.Add(usage); }
 
     /// @returns the set of address space uses of this structure
-    const std::unordered_set<core::AddressSpace>& AddressSpaceUsage() const {
-        return address_space_usage_;
-    }
+    const Hashset<core::AddressSpace, 1>& AddressSpaceUsage() const { return address_space_usage_; }
 
     /// @param usage the AddressSpace usage type to query
     /// @returns true iff this structure has been used as the given address space
-    bool UsedAs(core::AddressSpace usage) const { return address_space_usage_.count(usage) > 0; }
+    bool UsedAs(core::AddressSpace usage) const { return address_space_usage_.Contains(usage); }
 
     /// @returns true iff this structure has been used by address space that's
     /// host-shareable.
@@ -154,12 +158,10 @@
 
     /// Adds the pipeline stage usage to the structure.
     /// @param usage the storage usage
-    void AddUsage(PipelineStageUsage usage) { pipeline_stage_uses_.emplace(usage); }
+    void AddUsage(PipelineStageUsage usage) { pipeline_stage_uses_.Add(usage); }
 
     /// @returns the set of entry point uses of this structure
-    const std::unordered_set<PipelineStageUsage>& PipelineStageUses() const {
-        return pipeline_stage_uses_;
-    }
+    const Hashset<PipelineStageUsage, 1>& PipelineStageUses() const { return pipeline_stage_uses_; }
 
     /// @returns the name for this type that closely resembles how it would be
     /// declared in WGSL.
@@ -195,8 +197,8 @@
     const uint32_t size_;
     const uint32_t size_no_padding_;
     core::type::StructFlags struct_flags_;
-    std::unordered_set<core::AddressSpace> address_space_usage_;
-    std::unordered_set<PipelineStageUsage> pipeline_stage_uses_;
+    Hashset<core::AddressSpace, 1> address_space_usage_;
+    Hashset<PipelineStageUsage, 1> pipeline_stage_uses_;
     tint::Vector<const Struct*, 2> concrete_types_;
 };
 
@@ -204,8 +206,8 @@
 struct StructMemberAttributes {
     /// The value of a `@location` attribute
     std::optional<uint32_t> location;
-    /// The value of a `@index` attribute
-    std::optional<uint32_t> index;
+    /// The value of a `@blend_src` attribute
+    std::optional<uint32_t> blend_src;
     /// The value of a `@color` attribute
     std::optional<uint32_t> color;
     /// The value of a `@builtin` attribute
diff --git a/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc b/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc
index 2026bd2..1a66806 100644
--- a/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc
@@ -67,6 +67,7 @@
 #include "src/tint/lang/wgsl/ast/transform/expand_compound_assignment.h"
 #include "src/tint/lang/wgsl/ast/transform/manager.h"
 #include "src/tint/lang/wgsl/ast/transform/multiplanar_external_texture.h"
+#include "src/tint/lang/wgsl/ast/transform/offset_first_index.h"
 #include "src/tint/lang/wgsl/ast/transform/preserve_padding.h"
 #include "src/tint/lang/wgsl/ast/transform/promote_initializers_to_let.h"
 #include "src/tint/lang/wgsl/ast/transform/promote_side_effects_to_decl.h"
@@ -152,8 +153,6 @@
         data.Add<ast::transform::SingleEntryPoint::Config>(entry_point);
     }
 
-    data.Add<ast::transform::AddBlockAttribute::Config>(true);
-
     manager.Add<ast::transform::PreservePadding>();  // Must come before DirectVariableAccess
 
     manager.Add<ast::transform::Unshadow>();  // Must come before DirectVariableAccess
@@ -203,6 +202,8 @@
         manager.Add<ast::transform::ZeroInitWorkgroupMemory>();
     }
 
+    manager.Add<ast::transform::OffsetFirstIndex>();
+
     // CanonicalizeEntryPointIO must come after Robustness
     manager.Add<ast::transform::CanonicalizeEntryPointIO>();
 
@@ -217,11 +218,10 @@
     // TextureBuiltinsFromUniform must come before CombineSamplers to preserve texture binding point
     // info, instead of combined sampler binding point. As a result, TextureBuiltinsFromUniform also
     // comes before BindingRemapper so the binding point info it reflects is before remapping.
-    if (options.texture_builtins_from_uniform) {
-        manager.Add<TextureBuiltinsFromUniform>();
-        data.Add<TextureBuiltinsFromUniform::Config>(
-            options.texture_builtins_from_uniform->ubo_binding);
-    }
+    manager.Add<TextureBuiltinsFromUniform>();
+    data.Add<TextureBuiltinsFromUniform::Config>(
+        options.texture_builtins_from_uniform.ubo_binding,
+        options.texture_builtins_from_uniform.ubo_bindingpoint_ordering);
 
     data.Add<CombineSamplers::BindingInfo>(options.binding_map, options.placeholder_binding_point);
     manager.Add<CombineSamplers>();
@@ -246,13 +246,11 @@
     data.Add<ast::transform::CanonicalizeEntryPointIO::Config>(
         ast::transform::CanonicalizeEntryPointIO::ShaderStyle::kGlsl);
 
+    data.Add<ast::transform::OffsetFirstIndex::Config>(std::nullopt, options.first_instance_offset);
+
     SanitizedResult result;
     ast::transform::DataMap outputs;
     result.program = manager.Run(in, data, outputs);
-    if (auto* res = outputs.Get<TextureBuiltinsFromUniform::Result>()) {
-        result.needs_internal_uniform_buffer = true;
-        result.bindpoint_to_data = std::move(res->bindpoint_to_data);
-    }
     return result;
 }
 
@@ -302,7 +300,8 @@
                 }
                 bool is_block =
                     ast::HasAttribute<ast::transform::AddBlockAttribute::BlockAttribute>(
-                        str->attributes);
+                        str->attributes) &&
+                    !sem->UsedAs(core::AddressSpace::kPushConstant);
                 if (!has_rt_arr && !is_block) {
                     EmitStructType(current_buffer_, sem);
                 }
@@ -2098,6 +2097,7 @@
 
     auto name = decl->name->symbol.Name();
     auto* type = var->Type()->UnwrapRef();
+    out << "layout(location=0) ";
     EmitTypeAndName(out, type, var->AddressSpace(), var->Access(), name);
     out << ";";
 }
diff --git a/src/tint/lang/glsl/writer/ast_printer/ast_printer.h b/src/tint/lang/glsl/writer/ast_printer/ast_printer.h
index cfaf716..b35664f 100644
--- a/src/tint/lang/glsl/writer/ast_printer/ast_printer.h
+++ b/src/tint/lang/glsl/writer/ast_printer/ast_printer.h
@@ -67,13 +67,6 @@
 
     /// The sanitized program.
     Program program;
-
-    /// True if the shader needs a UBO.
-    bool needs_internal_uniform_buffer = false;
-
-    /// Store a map of global texture variable binding point to the byte offset and data type to
-    /// push into the internal uniform buffer.
-    TextureBuiltinsFromUniformOptions::BindingPointToFieldAndOffset bindpoint_to_data;
 };
 
 /// Sanitize a program in preparation for generating GLSL.
diff --git a/src/tint/lang/glsl/writer/ast_raise/BUILD.bazel b/src/tint/lang/glsl/writer/ast_raise/BUILD.bazel
index 540f314..c23fd29 100644
--- a/src/tint/lang/glsl/writer/ast_raise/BUILD.bazel
+++ b/src/tint/lang/glsl/writer/ast_raise/BUILD.bazel
@@ -52,7 +52,6 @@
   ],
   deps = [
     "//src/tint/api/common",
-    "//src/tint/api/options",
     "//src/tint/lang/core",
     "//src/tint/lang/core/constant",
     "//src/tint/lang/core/type",
@@ -92,7 +91,6 @@
   ],
   deps = [
     "//src/tint/api/common",
-    "//src/tint/api/options",
     "//src/tint/lang/core",
     "//src/tint/lang/core/constant",
     "//src/tint/lang/core/ir",
diff --git a/src/tint/lang/glsl/writer/ast_raise/BUILD.cmake b/src/tint/lang/glsl/writer/ast_raise/BUILD.cmake
index 0ca869d..1a658c0 100644
--- a/src/tint/lang/glsl/writer/ast_raise/BUILD.cmake
+++ b/src/tint/lang/glsl/writer/ast_raise/BUILD.cmake
@@ -53,7 +53,6 @@
 
 tint_target_add_dependencies(tint_lang_glsl_writer_ast_raise lib
   tint_api_common
-  tint_api_options
   tint_lang_core
   tint_lang_core_constant
   tint_lang_core_type
@@ -96,7 +95,6 @@
 
 tint_target_add_dependencies(tint_lang_glsl_writer_ast_raise_test test
   tint_api_common
-  tint_api_options
   tint_lang_core
   tint_lang_core_constant
   tint_lang_core_ir
diff --git a/src/tint/lang/glsl/writer/ast_raise/BUILD.gn b/src/tint/lang/glsl/writer/ast_raise/BUILD.gn
index 23c0067..166a313 100644
--- a/src/tint/lang/glsl/writer/ast_raise/BUILD.gn
+++ b/src/tint/lang/glsl/writer/ast_raise/BUILD.gn
@@ -55,7 +55,6 @@
     ]
     deps = [
       "${tint_src_dir}/api/common",
-      "${tint_src_dir}/api/options",
       "${tint_src_dir}/lang/core",
       "${tint_src_dir}/lang/core/constant",
       "${tint_src_dir}/lang/core/type",
@@ -96,7 +95,6 @@
       deps = [
         "${tint_src_dir}:gmock_and_gtest",
         "${tint_src_dir}/api/common",
-        "${tint_src_dir}/api/options",
         "${tint_src_dir}/lang/core",
         "${tint_src_dir}/lang/core/constant",
         "${tint_src_dir}/lang/core/ir",
diff --git a/src/tint/lang/glsl/writer/ast_raise/texture_builtins_from_uniform.cc b/src/tint/lang/glsl/writer/ast_raise/texture_builtins_from_uniform.cc
index bc40549..5ac9fec 100644
--- a/src/tint/lang/glsl/writer/ast_raise/texture_builtins_from_uniform.cc
+++ b/src/tint/lang/glsl/writer/ast_raise/texture_builtins_from_uniform.cc
@@ -27,8 +27,6 @@
 
 #include "src/tint/lang/glsl/writer/ast_raise/texture_builtins_from_uniform.h"
 
-#include <memory>
-#include <queue>
 #include <string>
 #include <utility>
 #include <variant>
@@ -49,7 +47,6 @@
 
 TINT_INSTANTIATE_TYPEINFO(tint::glsl::writer::TextureBuiltinsFromUniform);
 TINT_INSTANTIATE_TYPEINFO(tint::glsl::writer::TextureBuiltinsFromUniform::Config);
-TINT_INSTANTIATE_TYPEINFO(tint::glsl::writer::TextureBuiltinsFromUniform::Result);
 
 namespace tint::glsl::writer {
 
@@ -58,27 +55,10 @@
 /// The member name of the texture builtin values.
 constexpr std::string_view kTextureBuiltinValuesMemberNamePrefix = "texture_builtin_value_";
 
-bool ShouldRun(const Program& program) {
-    for (auto* fn : program.AST().Functions()) {
-        if (auto* sem_fn = program.Sem().Get(fn)) {
-            for (auto* builtin : sem_fn->DirectlyCalledBuiltins()) {
-                // GLSL ES  has no native support for the counterpart of
-                // textureNumLevels (textureQueryLevels) and textureNumSamples (textureSamples)
-                if (builtin->Fn() == wgsl::BuiltinFn::kTextureNumLevels) {
-                    return true;
-                }
-                if (builtin->Fn() == wgsl::BuiltinFn::kTextureNumSamples) {
-                    return true;
-                }
-            }
-        }
-    }
-    return false;
-}
-
 }  // namespace
 
 TextureBuiltinsFromUniform::TextureBuiltinsFromUniform() = default;
+
 TextureBuiltinsFromUniform::~TextureBuiltinsFromUniform() = default;
 
 /// PIMPL state for the transform
@@ -86,11 +66,8 @@
     /// Constructor
     /// @param program the source program
     /// @param in the input transform data
-    /// @param out the output transform data
-    explicit State(const Program& program,
-                   const ast::transform::DataMap& in,
-                   ast::transform::DataMap& out)
-        : src(program), inputs(in), outputs(out) {}
+    explicit State(const Program& program, const ast::transform::DataMap& in)
+        : src(program), inputs(in) {}
 
     /// Runs the transform
     /// @returns the new program or SkipTransform if the transform is not required
@@ -103,12 +80,14 @@
                     std::string(tint::TypeInfo::Of<TextureBuiltinsFromUniform>().name));
             return resolver::Resolve(b);
         }
+        ubo_bindingpoint_ordering = cfg->ubo_bindingpoint_ordering;
 
-        if (!ShouldRun(src)) {
+        // If there's no interested texture builtin at all, skip the transform.
+        if (ubo_bindingpoint_ordering.empty()) {
             return SkipTransform;
         }
 
-        // The dependency order declartions guaranteed that we traverse interested functions in the
+        // The dependency order declarations guaranteed that we traverse interested functions in the
         // following order:
         // 1. texture builtins
         // 2. user function directly calls texture builtins
@@ -143,21 +122,17 @@
                             auto* texture_sem = sem.GetVal(texture_expr)->RootIdentifier();
                             TINT_ASSERT(texture_sem);
 
-                            TextureBuiltinsFromUniformOptions::Field dataType =
-                                GetFieldFromBuiltinFunctionType(builtin->Fn());
-
                             tint::Switch(
                                 texture_sem,
                                 [&](const sem::GlobalVariable* global) {
                                     // This texture variable is a global variable.
-                                    auto binding = GetAndRecordGlobalBinding(global, dataType);
+                                    auto binding = GetGlobalBinding(global);
                                     // Record the call and binding to be replaced later.
                                     builtin_to_replace.Add(call_expr, binding);
                                 },
                                 [&](const sem::Variable* variable) {
                                     // This texture variable is a user function parameter.
-                                    auto new_param =
-                                        GetAndRecordFunctionParameter(fn, variable, dataType);
+                                    auto new_param = GetAndRecordFunctionParameter(fn, variable);
                                     // Record the call and new_param to be replaced later.
                                     builtin_to_replace.Add(call_expr, new_param);
                                 },  //
@@ -187,15 +162,14 @@
                                         texture_sem,
                                         [&](const sem::GlobalVariable* global) {
                                             // This texture variable is a global variable.
-                                            auto binding =
-                                                GetAndRecordGlobalBinding(global, info->field);
+                                            auto binding = GetGlobalBinding(global);
                                             // Record the binding to add to args.
                                             args.Push(binding);
                                         },
                                         [&](const sem::Variable* variable) {
                                             // This texture variable is a user function parameter.
-                                            auto new_param = GetAndRecordFunctionParameter(
-                                                fn, variable, info->field);
+                                            auto new_param =
+                                                GetAndRecordFunctionParameter(fn, variable);
                                             // Record adding extra function parameter
                                             args.Push(new_param);
                                         },  //
@@ -207,11 +181,6 @@
             }
         }
 
-        // If there's no interested texture builtin at all, skip the transform.
-        if (bindpoint_to_data.empty()) {
-            return SkipTransform;
-        }
-
         // If any functions need extra params, add them now.
         if (!fn_to_data.IsEmpty()) {
             for (auto pair : fn_to_data) {
@@ -263,8 +232,6 @@
             }
         }
 
-        outputs.Add<Result>(bindpoint_to_data);
-
         ctx.Clone();
         return resolver::Resolve(b);
     }
@@ -274,8 +241,6 @@
     const Program& src;
     /// The transform inputs
     const ast::transform::DataMap& inputs;
-    /// The transform outputs
-    ast::transform::DataMap& outputs;
     /// The target program builder
     ProgramBuilder b;
     /// The clone context
@@ -283,19 +248,10 @@
     /// Alias to the semantic info in ctx.src
     const sem::Info& sem = src.Sem();
 
-    /// The bindpoint to byte offset and field to pass out in transform result.
-    /// For one texture type, it could only be passed into one of the
-    /// textureNumLevels or textureNumSamples because their accepting param texture
-    /// type is different. There cannot be a binding entry with both field type.
-    /// Note: because this transform must be run before CombineSampler and BindingRemapper,
-    /// the binding number here is before remapped.
-    Result::BindingPointToFieldAndOffset bindpoint_to_data;
+    /// Ordered list of binding points for where they appear in the UBO
+    std::vector<BindingPoint> ubo_bindingpoint_ordering;
 
     struct FunctionExtraParamInfo {
-        using Field = TextureBuiltinsFromUniformOptions::Field;
-        // The kind of texture information this parameter holds.
-        Field field = Field::TextureNumLevels;
-
         // The extra passed in param that corresponds to the texture param.
         const ast::Parameter* param = nullptr;
 
@@ -337,6 +293,7 @@
 
     /// The internal uniform buffer
     const ast::Variable* ubo = nullptr;
+
     /// Get or create a UBO including u32 scalars for texture builtin values.
     /// @returns the symbol of the uniform buffer variable.
     Symbol GetUboSym() {
@@ -348,15 +305,13 @@
         auto* cfg = inputs.Get<Config>();
 
         Vector<const ast::StructMember*, 16> new_members;
-        new_members.Resize(bindpoint_to_data.size());
-        for (auto it : bindpoint_to_data) {
-            // Emit a u32 scalar for each binding that needs builtin value passed in.
-            size_t i = it.second.second / sizeof(uint32_t);
-            TINT_ASSERT(i < new_members.Length());
+        new_members.Resize(ubo_bindingpoint_ordering.size());
+
+        for (size_t i = 0; i < ubo_bindingpoint_ordering.size(); ++i) {
             // Append the vector index with the variable name to avoid unstable naming issue.
             auto sym = b.Symbols().New(std::string(kTextureBuiltinValuesMemberNamePrefix) +
                                        std::to_string(i));
-            bindpoint_to_syms.Add(it.first, sym);
+            bindpoint_to_syms.Add(ubo_bindingpoint_ordering[i], sym);
             new_members[i] = b.Member(sym, b.ty.u32());
         }
 
@@ -403,48 +358,31 @@
     /// @param binding of the global variable.
     /// @returns an expression of the builtin value.
     const ast::Expression* GetUniformValue(const BindingPoint& binding) {
-        auto iter = bindpoint_to_data.find(binding);
-        TINT_ASSERT(iter != bindpoint_to_data.end());
-
         // Make sure GetUboSym() is called first to initialize the uniform buffer struct.
         auto ubo_sym = GetUboSym();
+
         // Load the builtin value from the UBO.
         auto member_sym = bindpoint_to_syms.Get(binding);
         TINT_ASSERT(member_sym.has_value());
-        auto* builtin_value = b.MemberAccessor(ubo_sym, *member_sym);
 
-        return builtin_value;
+        return b.MemberAccessor(ubo_sym, *member_sym);
     }
 
-    /// Get and return the binding of the global texture variable. Record in bindpoint_to_data if
-    /// first visited.
+    /// Get and return the binding of the global texture variable.
     /// @param global global variable of the texture variable.
-    /// @param field type of the interested builtin function data related to this texture.
     /// @returns binding of the global variable.
-    BindingPoint GetAndRecordGlobalBinding(const sem::GlobalVariable* global,
-                                           TextureBuiltinsFromUniformOptions::Field field) {
-        auto binding = global->Attributes().binding_point.value();
-        auto iter = bindpoint_to_data.find(binding);
-        if (iter == bindpoint_to_data.end()) {
-            // First visit, recording the binding.
-            uint32_t index = static_cast<uint32_t>(bindpoint_to_data.size());
-            bindpoint_to_data.emplace(
-                binding,
-                Result::FieldAndOffset{field, index * static_cast<uint32_t>(sizeof(uint32_t))});
-        }
-        return binding;
+    BindingPoint GetGlobalBinding(const sem::GlobalVariable* global) {
+        return global->Attributes().binding_point.value();
     }
 
     /// Find which function param is the given texture variable.
-    /// Add a new u32 param relates to this texture param. Record in fn_to_data if first visited.
+    /// Add a new u32 param relates to this texture param. Record in fn_to_data if first
+    /// visited.
     /// @param fn the current function scope.
     /// @param var the texture variable.
-    /// @param field type of the interested builtin function data related to this texture.
     /// @returns the new u32 function parameter.
-    const ast::Parameter* GetAndRecordFunctionParameter(
-        const sem::Function* fn,
-        const sem::Variable* var,
-        TextureBuiltinsFromUniformOptions::Field field) {
+    const ast::Parameter* GetAndRecordFunctionParameter(const sem::Function* fn,
+                                                        const sem::Variable* var) {
         auto& param_to_info = fn_to_data.GetOrCreate(
             fn, [&] { return Hashmap<const ast::Parameter*, FunctionExtraParamInfo, 4>(); });
 
@@ -456,50 +394,36 @@
             }
         }
         TINT_ASSERT(param);
+
         // Get or record a new u32 param to this function if first visited.
         auto entry = param_to_info.Get(param);
         if (entry.has_value()) {
             return entry->param;
         }
+
         const ast::Parameter* new_param = b.Param(b.Sym(), b.ty.u32());
         size_t idx = param_to_info.Count();
-        param_to_info.Add(param, FunctionExtraParamInfo{field, new_param, idx});
+        param_to_info.Add(param, FunctionExtraParamInfo{new_param, idx});
         return new_param;
     }
-
-    /// Get the uniform options field for the builtin function.
-    /// @param type of the builtin function
-    /// @returns corresponding TextureBuiltinsFromUniformOptions::Field for the builtin
-    static TextureBuiltinsFromUniformOptions::Field GetFieldFromBuiltinFunctionType(
-        wgsl::BuiltinFn type) {
-        switch (type) {
-            case wgsl::BuiltinFn::kTextureNumLevels:
-                return TextureBuiltinsFromUniformOptions::Field::TextureNumLevels;
-            case wgsl::BuiltinFn::kTextureNumSamples:
-                return TextureBuiltinsFromUniformOptions::Field::TextureNumSamples;
-            default:
-                TINT_UNREACHABLE() << "unsupported builtin function type " << type;
-        }
-        return TextureBuiltinsFromUniformOptions::Field::TextureNumLevels;
-    }
 };
 
 ast::transform::Transform::ApplyResult TextureBuiltinsFromUniform::Apply(
     const Program& src,
     const ast::transform::DataMap& inputs,
-    ast::transform::DataMap& outputs) const {
-    return State{src, inputs, outputs}.Run();
+    ast::transform::DataMap&) const {
+    return State{src, inputs}.Run();
 }
 
-TextureBuiltinsFromUniform::Config::Config(BindingPoint ubo_bp) : ubo_binding(ubo_bp) {}
+TextureBuiltinsFromUniform::Config::Config(BindingPoint ubo_bp,
+                                           const std::vector<BindingPoint>& ordering)
+    : ubo_binding(ubo_bp), ubo_bindingpoint_ordering(ordering) {}
+
 TextureBuiltinsFromUniform::Config::Config(const Config&) = default;
+
 TextureBuiltinsFromUniform::Config& TextureBuiltinsFromUniform::Config::operator=(const Config&) =
     default;
-TextureBuiltinsFromUniform::Config::~Config() = default;
 
-TextureBuiltinsFromUniform::Result::Result(BindingPointToFieldAndOffset bindpoint_to_data_in)
-    : bindpoint_to_data(std::move(bindpoint_to_data_in)) {}
-TextureBuiltinsFromUniform::Result::Result(const Result&) = default;
-TextureBuiltinsFromUniform::Result::~Result() = default;
+TextureBuiltinsFromUniform::Config::~Config() = default;
 
 }  // namespace tint::glsl::writer
diff --git a/src/tint/lang/glsl/writer/ast_raise/texture_builtins_from_uniform.h b/src/tint/lang/glsl/writer/ast_raise/texture_builtins_from_uniform.h
index a54f02b..550e0f8 100644
--- a/src/tint/lang/glsl/writer/ast_raise/texture_builtins_from_uniform.h
+++ b/src/tint/lang/glsl/writer/ast_raise/texture_builtins_from_uniform.h
@@ -28,11 +28,9 @@
 #ifndef SRC_TINT_LANG_GLSL_WRITER_AST_RAISE_TEXTURE_BUILTINS_FROM_UNIFORM_H_
 #define SRC_TINT_LANG_GLSL_WRITER_AST_RAISE_TEXTURE_BUILTINS_FROM_UNIFORM_H_
 
-#include <unordered_map>
-#include <unordered_set>
+#include <vector>
 
 #include "src/tint/api/common/binding_point.h"
-#include "src/tint/api/options/texture_builtins_from_uniform.h"
 #include "src/tint/lang/wgsl/ast/transform/transform.h"
 
 // Forward declarations
@@ -47,6 +45,7 @@
 /// builtin functions are not available in some version of GLSL.
 ///
 /// The generated uniform buffer will have the form:
+///
 /// ```
 /// struct internal_uniform {
 ///  texture_builtin_value_0 : u32,
@@ -54,15 +53,17 @@
 ///
 /// @group(0) @binding(0) var tex : texture_2d<f32>;
 /// ```
+///
 /// The binding group and number used for this uniform buffer are provided via
 /// the `Config` transform input.
 ///
-/// The transform coverts the texture builtins calls into values lookup from the internal
-/// buffer. If the texture is a function parameter instead of a global variable, this transform
-/// also takes care of adding extra paramters and arguments to these functions and their callsites.
+/// The transform coverts the texture builtins calls into values lookup from the internal buffer. If
+/// the texture is a function parameter instead of a global variable, this transform also takes care
+/// of adding extra parameters and arguments to these functions and their call-sites.
 ///
 /// This transform must run before `CombineSamplers` transform so that the binding point of the
 /// original texture object can be preserved.
+///
 class TextureBuiltinsFromUniform final
     : public Castable<TextureBuiltinsFromUniform, ast::transform::Transform> {
   public:
@@ -75,7 +76,8 @@
     struct Config final : public Castable<Config, ast::transform::Data> {
         /// Constructor
         /// @param ubo_bp the binding point to use for the generated uniform buffer.
-        explicit Config(BindingPoint ubo_bp);
+        /// @param ordering the ordered list of binding points to appear in the UBO
+        Config(BindingPoint ubo_bp, const std::vector<BindingPoint>& ordering);
 
         /// Copy constructor
         Config(const Config&);
@@ -89,34 +91,14 @@
 
         /// The binding point to use for the generated uniform buffer.
         BindingPoint ubo_binding;
-    };
 
-    /// Information produced about what the transform did.
-    /// If there were no calls to the textureNumLevels() or textureNumSamples() builtin, then no
-    /// Result will be emitted.
-    struct Result final : public Castable<Result, ast::transform::Data> {
-        /// Using for shorter names
-        /// Records the field and the byte offset of the data to push in the internal uniform
-        /// buffer.
-        using FieldAndOffset = TextureBuiltinsFromUniformOptions::FieldAndOffset;
-        /// Maps from binding point to data entry with the information to populate the data.
-        using BindingPointToFieldAndOffset =
-            TextureBuiltinsFromUniformOptions::BindingPointToFieldAndOffset;
-
-        /// Constructor
-        /// @param bindpoint_to_data_in mapping from binding points of global texture variables to
-        /// the byte offsets and data types needed to be pushed into the internal uniform buffer.
-        explicit Result(BindingPointToFieldAndOffset bindpoint_to_data_in);
-
-        /// Copy constructor
-        Result(const Result&);
-
-        /// Destructor
-        ~Result() override;
-
-        /// A map of global texture variable binding point to the byte offset and data type to push
-        /// into the internal uniform buffer.
-        BindingPointToFieldAndOffset bindpoint_to_data;
+        /// The set of binding points which will be in the extra UBO buffer. The binding points are
+        /// provided in the order that the data will be in the UBO buffer.
+        ///
+        /// Note, a given `BindingPoint` _must_ only appear once in the ordering list. This works
+        /// because the two types of calls we're substituting `textureNumLevels` and
+        /// `textureNumSamples` work on a disjoint set of texture types.
+        std::vector<BindingPoint> ubo_bindingpoint_ordering;
     };
 
     /// @copydoc ast::transform::Transform::Apply
diff --git a/src/tint/lang/glsl/writer/ast_raise/texture_builtins_from_uniform_test.cc b/src/tint/lang/glsl/writer/ast_raise/texture_builtins_from_uniform_test.cc
index cabe7e1..3ee9775 100644
--- a/src/tint/lang/glsl/writer/ast_raise/texture_builtins_from_uniform_test.cc
+++ b/src/tint/lang/glsl/writer/ast_raise/texture_builtins_from_uniform_test.cc
@@ -36,53 +36,6 @@
 
 using TextureBuiltinsFromUniformTest = ast::transform::TransformTest;
 
-TEST_F(TextureBuiltinsFromUniformTest, ShouldRunEmptyModule) {
-    auto* src = R"()";
-
-    TextureBuiltinsFromUniform::Config cfg({0, 30u});
-
-    ast::transform::DataMap data;
-    data.Add<TextureBuiltinsFromUniform::Config>(std::move(cfg));
-
-    EXPECT_FALSE(ShouldRun<TextureBuiltinsFromUniform>(src, data));
-}
-
-TEST_F(TextureBuiltinsFromUniformTest, ShouldRunNoTextureNumLevels) {
-    auto* src = R"(
-@group(0) @binding(0) var t : texture_2d<f32>;
-
-@compute @workgroup_size(1)
-fn main() {
-  _ = textureDimensions(t);
-}
-)";
-
-    TextureBuiltinsFromUniform::Config cfg({0, 30u});
-
-    ast::transform::DataMap data;
-    data.Add<TextureBuiltinsFromUniform::Config>(std::move(cfg));
-
-    EXPECT_FALSE(ShouldRun<TextureBuiltinsFromUniform>(src, data));
-}
-
-TEST_F(TextureBuiltinsFromUniformTest, ShouldRunWithTextureNumLevels) {
-    auto* src = R"(
-@group(0) @binding(0) var t : texture_2d<f32>;
-
-@compute @workgroup_size(1)
-fn main() {
-  var len : u32 = textureNumLevels(t);
-}
-)";
-
-    TextureBuiltinsFromUniform::Config cfg({0, 30u});
-
-    ast::transform::DataMap data;
-    data.Add<TextureBuiltinsFromUniform::Config>(std::move(cfg));
-
-    EXPECT_TRUE(ShouldRun<TextureBuiltinsFromUniform>(src, data));
-}
-
 TEST_F(TextureBuiltinsFromUniformTest, Error_MissingTransformData) {
     auto* src = R"(
 @group(0) @binding(0) var t : texture_2d<f32>;
@@ -126,25 +79,14 @@
 }
 )";
 
-    TextureBuiltinsFromUniform::Config cfg({0, 30u});
+    std::vector<BindingPoint> bps = {{0, 0}};
+    TextureBuiltinsFromUniform::Config cfg({0, 30u}, bps);
 
     ast::transform::DataMap data;
     data.Add<TextureBuiltinsFromUniform::Config>(std::move(cfg));
 
     auto got = Run<TextureBuiltinsFromUniform>(src, data);
-
     EXPECT_EQ(expect, str(got));
-    auto* val = got.data.Get<TextureBuiltinsFromUniform::Result>();
-    ASSERT_NE(val, nullptr);
-    // Note: Using the following EXPECT_EQ directly on BindingPointToFieldAndOffset seems to cause
-    // compiler to hang. EXPECT_EQ(
-    //     TextureBuiltinsFromUniformOptions::BindingPointToFieldAndOffset{
-    //         {BindgPoint{0u, 0u},
-    //          std::make_pair(TextureBuiltinsFromUniformOptions::Field::TextureNumLevels, 0u)}},
-    //     val->bindpoint_to_data);
-    EXPECT_EQ(1u, val->bindpoint_to_data.size());
-    EXPECT_EQ(std::make_pair(TextureBuiltinsFromUniformOptions::Field::TextureNumLevels, 0u),
-              val->bindpoint_to_data.at(BindingPoint{0, 0}));
 }
 
 TEST_F(TextureBuiltinsFromUniformTest, BasicTextureNumSamples) {
@@ -172,19 +114,14 @@
 }
 )";
 
-    TextureBuiltinsFromUniform::Config cfg({0, 30u});
+    std::vector<BindingPoint> bps = {{0, 0}};
+    TextureBuiltinsFromUniform::Config cfg({0, 30u}, bps);
 
     ast::transform::DataMap data;
     data.Add<TextureBuiltinsFromUniform::Config>(std::move(cfg));
 
     auto got = Run<TextureBuiltinsFromUniform>(src, data);
-
     EXPECT_EQ(expect, str(got));
-    auto* val = got.data.Get<TextureBuiltinsFromUniform::Result>();
-    ASSERT_NE(val, nullptr);
-    EXPECT_EQ(1u, val->bindpoint_to_data.size());
-    EXPECT_EQ(std::make_pair(TextureBuiltinsFromUniformOptions::Field::TextureNumSamples, 0u),
-              val->bindpoint_to_data.at(BindingPoint{0, 0}));
 }
 
 TEST_F(TextureBuiltinsFromUniformTest, SameBuiltinCalledMultipleTimes) {
@@ -214,19 +151,14 @@
 }
 )";
 
-    TextureBuiltinsFromUniform::Config cfg({0, 30u});
+    std::vector<BindingPoint> bps = {{0, 0}};
+    TextureBuiltinsFromUniform::Config cfg({0, 30u}, bps);
 
     ast::transform::DataMap data;
     data.Add<TextureBuiltinsFromUniform::Config>(std::move(cfg));
 
     auto got = Run<TextureBuiltinsFromUniform>(src, data);
-
     EXPECT_EQ(expect, str(got));
-    auto* val = got.data.Get<TextureBuiltinsFromUniform::Result>();
-    ASSERT_NE(val, nullptr);
-    EXPECT_EQ(1u, val->bindpoint_to_data.size());
-    EXPECT_EQ(std::make_pair(TextureBuiltinsFromUniformOptions::Field::TextureNumLevels, 0u),
-              val->bindpoint_to_data.at(BindingPoint{0, 0}));
 }
 
 TEST_F(TextureBuiltinsFromUniformTest, SameBuiltinCalledMultipleTimesTextureNumSamples) {
@@ -256,19 +188,14 @@
 }
 )";
 
-    TextureBuiltinsFromUniform::Config cfg({0, 30u});
+    std::vector<BindingPoint> bps = {{0, 0}};
+    TextureBuiltinsFromUniform::Config cfg({0, 30u}, bps);
 
     ast::transform::DataMap data;
     data.Add<TextureBuiltinsFromUniform::Config>(std::move(cfg));
 
     auto got = Run<TextureBuiltinsFromUniform>(src, data);
-
     EXPECT_EQ(expect, str(got));
-    auto* val = got.data.Get<TextureBuiltinsFromUniform::Result>();
-    ASSERT_NE(val, nullptr);
-    EXPECT_EQ(1u, val->bindpoint_to_data.size());
-    EXPECT_EQ(std::make_pair(TextureBuiltinsFromUniformOptions::Field::TextureNumSamples, 0u),
-              val->bindpoint_to_data.at(BindingPoint{0, 0}));
 }
 
 TEST_F(TextureBuiltinsFromUniformTest, TextureAsFunctionParameterBasic) {
@@ -304,19 +231,14 @@
 }
 )";
 
-    TextureBuiltinsFromUniform::Config cfg({0, 30u});
+    std::vector<BindingPoint> bps = {{0, 0}};
+    TextureBuiltinsFromUniform::Config cfg({0, 30u}, bps);
 
     ast::transform::DataMap data;
     data.Add<TextureBuiltinsFromUniform::Config>(std::move(cfg));
 
     auto got = Run<TextureBuiltinsFromUniform>(src, data);
-
     EXPECT_EQ(expect, str(got));
-    auto* val = got.data.Get<TextureBuiltinsFromUniform::Result>();
-    ASSERT_NE(val, nullptr);
-    EXPECT_EQ(1u, val->bindpoint_to_data.size());
-    EXPECT_EQ(std::make_pair(TextureBuiltinsFromUniformOptions::Field::TextureNumLevels, 0u),
-              val->bindpoint_to_data.at(BindingPoint{0, 0}));
 }
 
 TEST_F(TextureBuiltinsFromUniformTest, TextureAsFunctionParameterUsedTwice) {
@@ -356,19 +278,14 @@
 }
 )";
 
-    TextureBuiltinsFromUniform::Config cfg({0, 30u});
+    std::vector<BindingPoint> bps = {{0, 0}};
+    TextureBuiltinsFromUniform::Config cfg({0, 30u}, bps);
 
     ast::transform::DataMap data;
     data.Add<TextureBuiltinsFromUniform::Config>(std::move(cfg));
 
     auto got = Run<TextureBuiltinsFromUniform>(src, data);
-
     EXPECT_EQ(expect, str(got));
-    auto* val = got.data.Get<TextureBuiltinsFromUniform::Result>();
-    ASSERT_NE(val, nullptr);
-    EXPECT_EQ(1u, val->bindpoint_to_data.size());
-    EXPECT_EQ(std::make_pair(TextureBuiltinsFromUniformOptions::Field::TextureNumLevels, 0u),
-              val->bindpoint_to_data.at(BindingPoint{0, 0}));
 }
 
 TEST_F(TextureBuiltinsFromUniformTest, TextureAsFunctionParameterMultipleParameters) {
@@ -412,23 +329,14 @@
 }
 )";
 
-    TextureBuiltinsFromUniform::Config cfg({0, 30u});
+    std::vector<BindingPoint> bps = {{0, 0}, {0, 1}, {0, 2}};
+    TextureBuiltinsFromUniform::Config cfg({0, 30u}, bps);
 
     ast::transform::DataMap data;
     data.Add<TextureBuiltinsFromUniform::Config>(std::move(cfg));
 
     auto got = Run<TextureBuiltinsFromUniform>(src, data);
-
     EXPECT_EQ(expect, str(got));
-    auto* val = got.data.Get<TextureBuiltinsFromUniform::Result>();
-    ASSERT_NE(val, nullptr);
-    EXPECT_EQ(3u, val->bindpoint_to_data.size());
-    EXPECT_EQ(std::make_pair(TextureBuiltinsFromUniformOptions::Field::TextureNumLevels, 0u),
-              val->bindpoint_to_data.at(BindingPoint{0, 0}));
-    EXPECT_EQ(std::make_pair(TextureBuiltinsFromUniformOptions::Field::TextureNumLevels, 4u),
-              val->bindpoint_to_data.at(BindingPoint{0, 1}));
-    EXPECT_EQ(std::make_pair(TextureBuiltinsFromUniformOptions::Field::TextureNumLevels, 8u),
-              val->bindpoint_to_data.at(BindingPoint{0, 2}));
 }
 
 TEST_F(TextureBuiltinsFromUniformTest, TextureAsFunctionParameterNested) {
@@ -472,19 +380,14 @@
 }
 )";
 
-    TextureBuiltinsFromUniform::Config cfg({0, 30u});
+    std::vector<BindingPoint> bps = {{0, 0}};
+    TextureBuiltinsFromUniform::Config cfg({0, 30u}, bps);
 
     ast::transform::DataMap data;
     data.Add<TextureBuiltinsFromUniform::Config>(std::move(cfg));
 
     auto got = Run<TextureBuiltinsFromUniform>(src, data);
-
     EXPECT_EQ(expect, str(got));
-    auto* val = got.data.Get<TextureBuiltinsFromUniform::Result>();
-    ASSERT_NE(val, nullptr);
-    EXPECT_EQ(1u, val->bindpoint_to_data.size());
-    EXPECT_EQ(std::make_pair(TextureBuiltinsFromUniformOptions::Field::TextureNumLevels, 0u),
-              val->bindpoint_to_data.at(BindingPoint{0, 0}));
 }
 
 TEST_F(TextureBuiltinsFromUniformTest, TextureAsFunctionParameterMixed) {
@@ -549,25 +452,14 @@
 }
 )";
 
-    TextureBuiltinsFromUniform::Config cfg({0, 30u});
+    std::vector<BindingPoint> bps = {{0, 1}, {0, 3}, {0, 0}, {0, 2}};
+    TextureBuiltinsFromUniform::Config cfg({0, 30u}, bps);
 
     ast::transform::DataMap data;
     data.Add<TextureBuiltinsFromUniform::Config>(std::move(cfg));
 
     auto got = Run<TextureBuiltinsFromUniform>(src, data);
-
     EXPECT_EQ(expect, str(got));
-    auto* val = got.data.Get<TextureBuiltinsFromUniform::Result>();
-    ASSERT_NE(val, nullptr);
-    EXPECT_EQ(4u, val->bindpoint_to_data.size());
-    EXPECT_EQ(std::make_pair(TextureBuiltinsFromUniformOptions::Field::TextureNumLevels, 0u),
-              val->bindpoint_to_data.at(BindingPoint{0, 1}));
-    EXPECT_EQ(std::make_pair(TextureBuiltinsFromUniformOptions::Field::TextureNumLevels, 4u),
-              val->bindpoint_to_data.at(BindingPoint{0, 3}));
-    EXPECT_EQ(std::make_pair(TextureBuiltinsFromUniformOptions::Field::TextureNumLevels, 8u),
-              val->bindpoint_to_data.at(BindingPoint{0, 0}));
-    EXPECT_EQ(std::make_pair(TextureBuiltinsFromUniformOptions::Field::TextureNumLevels, 12u),
-              val->bindpoint_to_data.at(BindingPoint{0, 2}));
 }
 
 TEST_F(TextureBuiltinsFromUniformTest, MultipleTextures) {
@@ -625,7 +517,8 @@
 }
 )";
 
-    TextureBuiltinsFromUniform::Config cfg({0, 30u});
+    std::vector<BindingPoint> bps = {{0, 0}, {0, 1}, {0, 2}, {0, 3}, {0, 4}, {1, 0}};
+    TextureBuiltinsFromUniform::Config cfg({0, 30u}, bps);
 
     ast::transform::DataMap data;
     data.Add<TextureBuiltinsFromUniform::Config>(std::move(cfg));
@@ -633,21 +526,6 @@
     auto got = Run<TextureBuiltinsFromUniform>(src, data);
 
     EXPECT_EQ(expect, str(got));
-    auto* val = got.data.Get<TextureBuiltinsFromUniform::Result>();
-    ASSERT_NE(val, nullptr);
-    EXPECT_EQ(6u, val->bindpoint_to_data.size());
-    EXPECT_EQ(std::make_pair(TextureBuiltinsFromUniformOptions::Field::TextureNumLevels, 0u),
-              val->bindpoint_to_data.at(BindingPoint{0, 0}));
-    EXPECT_EQ(std::make_pair(TextureBuiltinsFromUniformOptions::Field::TextureNumSamples, 4u),
-              val->bindpoint_to_data.at(BindingPoint{0, 1}));
-    EXPECT_EQ(std::make_pair(TextureBuiltinsFromUniformOptions::Field::TextureNumLevels, 8u),
-              val->bindpoint_to_data.at(BindingPoint{0, 2}));
-    EXPECT_EQ(std::make_pair(TextureBuiltinsFromUniformOptions::Field::TextureNumLevels, 12u),
-              val->bindpoint_to_data.at(BindingPoint{0, 3}));
-    EXPECT_EQ(std::make_pair(TextureBuiltinsFromUniformOptions::Field::TextureNumLevels, 16u),
-              val->bindpoint_to_data.at(BindingPoint{0, 4}));
-    EXPECT_EQ(std::make_pair(TextureBuiltinsFromUniformOptions::Field::TextureNumSamples, 20u),
-              val->bindpoint_to_data.at(BindingPoint{1, 0}));
 }
 
 TEST_F(TextureBuiltinsFromUniformTest, BindingPointExist) {
@@ -682,7 +560,8 @@
 }
 )";
 
-    TextureBuiltinsFromUniform::Config cfg({0, 30u});
+    std::vector<BindingPoint> bps = {{0, 0}};
+    TextureBuiltinsFromUniform::Config cfg({0, 30u}, bps);
 
     ast::transform::DataMap data;
     data.Add<TextureBuiltinsFromUniform::Config>(std::move(cfg));
@@ -690,10 +569,6 @@
     auto got = Run<TextureBuiltinsFromUniform>(src, data);
 
     EXPECT_EQ(expect, str(got));
-    auto* val = got.data.Get<TextureBuiltinsFromUniform::Result>();
-    ASSERT_NE(val, nullptr);
-    EXPECT_EQ(std::make_pair(TextureBuiltinsFromUniformOptions::Field::TextureNumLevels, 0u),
-              val->bindpoint_to_data.at(BindingPoint{0, 0}));
 }
 
 }  // namespace
diff --git a/src/tint/lang/glsl/writer/common/BUILD.bazel b/src/tint/lang/glsl/writer/common/BUILD.bazel
index 1a61a9b..71f4677 100644
--- a/src/tint/lang/glsl/writer/common/BUILD.bazel
+++ b/src/tint/lang/glsl/writer/common/BUILD.bazel
@@ -50,7 +50,6 @@
   deps = [
     "//src/tint/api/common",
     "//src/tint/api/options",
-    "//src/tint/lang/core",
     "//src/tint/lang/wgsl/sem",
     "//src/tint/utils/macros",
     "//src/tint/utils/math",
diff --git a/src/tint/lang/glsl/writer/common/BUILD.cmake b/src/tint/lang/glsl/writer/common/BUILD.cmake
index 8c176fe..a015049 100644
--- a/src/tint/lang/glsl/writer/common/BUILD.cmake
+++ b/src/tint/lang/glsl/writer/common/BUILD.cmake
@@ -51,7 +51,6 @@
 tint_target_add_dependencies(tint_lang_glsl_writer_common lib
   tint_api_common
   tint_api_options
-  tint_lang_core
   tint_lang_wgsl_sem
   tint_utils_macros
   tint_utils_math
diff --git a/src/tint/lang/glsl/writer/common/BUILD.gn b/src/tint/lang/glsl/writer/common/BUILD.gn
index a91c610..e833eb3 100644
--- a/src/tint/lang/glsl/writer/common/BUILD.gn
+++ b/src/tint/lang/glsl/writer/common/BUILD.gn
@@ -49,7 +49,6 @@
     deps = [
       "${tint_src_dir}/api/common",
       "${tint_src_dir}/api/options",
-      "${tint_src_dir}/lang/core",
       "${tint_src_dir}/lang/wgsl/sem",
       "${tint_src_dir}/utils/macros",
       "${tint_src_dir}/utils/math",
diff --git a/src/tint/lang/glsl/writer/common/options.h b/src/tint/lang/glsl/writer/common/options.h
index b795d10..1f5298f 100644
--- a/src/tint/lang/glsl/writer/common/options.h
+++ b/src/tint/lang/glsl/writer/common/options.h
@@ -35,7 +35,6 @@
 #include "src/tint/api/options/binding_remapper.h"
 #include "src/tint/api/options/external_texture.h"
 #include "src/tint/api/options/texture_builtins_from_uniform.h"
-#include "src/tint/lang/core/access.h"
 #include "src/tint/lang/glsl/writer/common/version.h"
 #include "src/tint/lang/wgsl/sem/sampler_texture_pair.h"
 
@@ -80,10 +79,13 @@
     /// Options used in the binding mappings for external textures
     ExternalTextureOptions external_texture_options = {};
 
+    /// Offset of the firstInstance push constant.
+    std::optional<int32_t> first_instance_offset;
+
     /// Options used to map WGSL textureNumLevels/textureNumSamples builtins to internal uniform
     /// buffer values. If not specified, emits corresponding GLSL builtins
     /// textureQueryLevels/textureSamples directly.
-    std::optional<TextureBuiltinsFromUniformOptions> texture_builtins_from_uniform = std::nullopt;
+    TextureBuiltinsFromUniformOptions texture_builtins_from_uniform = {};
 
     /// Reflect the fields of this class so that it can be used by tint::ForeachField()
     TINT_REFLECT(disable_robustness,
diff --git a/src/tint/lang/glsl/writer/output.h b/src/tint/lang/glsl/writer/output.h
index bb52015..8f205d1 100644
--- a/src/tint/lang/glsl/writer/output.h
+++ b/src/tint/lang/glsl/writer/output.h
@@ -32,7 +32,6 @@
 #include <utility>
 #include <vector>
 
-#include "src/tint/api/options/texture_builtins_from_uniform.h"
 #include "src/tint/lang/wgsl/ast/pipeline_stage.h"
 
 namespace tint::glsl::writer {
@@ -50,13 +49,6 @@
 
     /// The generated GLSL.
     std::string glsl = "";
-
-    /// True if the shader needs a UBO.
-    bool needs_internal_uniform_buffer = false;
-
-    /// Store a map of global texture variable binding points to the byte offset and data type to
-    /// push into the internal uniform buffer.
-    TextureBuiltinsFromUniformOptions::BindingPointToFieldAndOffset bindpoint_to_data;
 };
 
 }  // namespace tint::glsl::writer
diff --git a/src/tint/lang/glsl/writer/writer.cc b/src/tint/lang/glsl/writer/writer.cc
index c1ff290..17bd7ef 100644
--- a/src/tint/lang/glsl/writer/writer.cc
+++ b/src/tint/lang/glsl/writer/writer.cc
@@ -76,8 +76,6 @@
     }
 
     output.glsl = impl->Result();
-    output.needs_internal_uniform_buffer = sanitized_result.needs_internal_uniform_buffer;
-    output.bindpoint_to_data = std::move(sanitized_result.bindpoint_to_data);
 
     return output;
 }
diff --git a/src/tint/lang/hlsl/writer/ast_printer/assign_test.cc b/src/tint/lang/hlsl/writer/ast_printer/assign_test.cc
index 40384c2..f2734ca 100644
--- a/src/tint/lang/hlsl/writer/ast_printer/assign_test.cc
+++ b/src/tint/lang/hlsl/writer/ast_printer/assign_test.cc
@@ -69,15 +69,15 @@
 
     ASSERT_TRUE(gen.Generate());
     EXPECT_EQ(gen.Result(),
-              R"(void set_float3(inout float3 vec, int idx, float val) {
+              R"(void set_vector_element(inout float3 vec, int idx, float val) {
   vec = (idx.xxx == int3(0, 1, 2)) ? val.xxx : vec;
 }
 
 void fn() {
   float3 lhs = float3(0.0f, 0.0f, 0.0f);
   float rhs = 0.0f;
-  const uint index = 0u;
-  set_float3(lhs, index, rhs);
+  uint index = 0u;
+  set_vector_element(lhs, index, rhs);
 }
 )");
 }
@@ -116,7 +116,7 @@
 
     ASSERT_TRUE(gen.Generate());
     EXPECT_EQ(gen.Result(),
-              R"(void set_float3(inout float3 vec, int idx, float val) {
+              R"(void set_vector_element(inout float3 vec, int idx, float val) {
   vec = (idx.xxx == int3(0, 1, 2)) ? val.xxx : vec;
 }
 
@@ -124,7 +124,7 @@
   float3 lhs = float3(0.0f, 0.0f, 0.0f);
   float rhs = 0.0f;
   uint index = 0u;
-  set_float3(lhs, index, rhs);
+  set_vector_element(lhs, index, rhs);
 }
 )");
 }
@@ -142,7 +142,7 @@
 
     ASSERT_TRUE(gen.Generate());
     EXPECT_EQ(gen.Result(),
-              R"(void set_vector_float4x2(inout float4x2 mat, int col, float2 val) {
+              R"(void set_matrix_column(inout float4x2 mat, int col, float2 val) {
   switch (col) {
     case 0: mat[0] = val; break;
     case 1: mat[1] = val; break;
@@ -154,8 +154,8 @@
 void fn() {
   float4x2 lhs = float4x2(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f);
   float2 rhs = float2(0.0f, 0.0f);
-  const uint index = 0u;
-  set_vector_float4x2(lhs, index, rhs);
+  uint index = 0u;
+  set_matrix_column(lhs, index, rhs);
 }
 )");
 }
@@ -194,7 +194,7 @@
 
     ASSERT_TRUE(gen.Generate());
     EXPECT_EQ(gen.Result(),
-              R"(void set_vector_float4x2(inout float4x2 mat, int col, float2 val) {
+              R"(void set_matrix_column(inout float4x2 mat, int col, float2 val) {
   switch (col) {
     case 0: mat[0] = val; break;
     case 1: mat[1] = val; break;
@@ -207,7 +207,7 @@
   float4x2 lhs = float4x2(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f);
   float2 rhs = float2(0.0f, 0.0f);
   uint index = 0u;
-  set_vector_float4x2(lhs, index, rhs);
+  set_matrix_column(lhs, index, rhs);
 }
 )");
 }
@@ -228,7 +228,7 @@
 
     ASSERT_TRUE(gen.Generate());
     EXPECT_EQ(gen.Result(),
-              R"(void set_scalar_float4x2(inout float4x2 mat, int col, int row, float val) {
+              R"(void set_matrix_scalar(inout float4x2 mat, int col, int row, float val) {
   switch (col) {
     case 0:
       mat[0] = (row.xx == int2(0, 1)) ? val.xx : mat[0];
@@ -248,9 +248,9 @@
 void fn() {
   float4x2 lhs = float4x2(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f);
   float rhs = 0.0f;
-  const uint col = 0u;
-  const uint row = 1u;
-  set_scalar_float4x2(lhs, col, row, rhs);
+  uint col = 0u;
+  uint row = 1u;
+  set_matrix_scalar(lhs, col, row, rhs);
 }
 )");
 }
@@ -295,7 +295,7 @@
 
     ASSERT_TRUE(gen.Generate());
     EXPECT_EQ(gen.Result(),
-              R"(void set_scalar_float4x2(inout float4x2 mat, int col, int row, float val) {
+              R"(void set_matrix_scalar(inout float4x2 mat, int col, int row, float val) {
   switch (col) {
     case 0:
       mat[0] = (row.xx == int2(0, 1)) ? val.xx : mat[0];
@@ -317,7 +317,119 @@
   float rhs = 0.0f;
   uint col = 0u;
   uint row = 0u;
-  set_scalar_float4x2(lhs, col, row, rhs);
+  set_matrix_scalar(lhs, col, row, rhs);
+}
+)");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Assignment to composites of f16
+// See crbug.com/tint/2146
+////////////////////////////////////////////////////////////////////////////////
+TEST_F(HlslASTPrinterTest_Assign, Emit_Vector_f16_Assign) {
+    Enable(wgsl::Extension::kF16);
+
+    Func("fn", tint::Empty, ty.void_(),
+         Vector{
+             Decl(Var("lhs", ty.vec3<f16>())),
+             Decl(Var("rhs", ty.f16())),
+             Decl(Let("index", ty.u32(), Expr(0_u))),
+             Assign(IndexAccessor("lhs", "index"), "rhs"),
+         });
+
+    ASTPrinter& gen = Build();
+
+    ASSERT_TRUE(gen.Generate());
+    EXPECT_EQ(gen.Result(),
+              R"(void set_vector_element(inout vector<float16_t, 3> vec, int idx, float16_t val) {
+  vec = (idx.xxx == int3(0, 1, 2)) ? val.xxx : vec;
+}
+
+void fn() {
+  vector<float16_t, 3> lhs = vector<float16_t, 3>(float16_t(0.0h), float16_t(0.0h), float16_t(0.0h));
+  float16_t rhs = float16_t(0.0h);
+  uint index = 0u;
+  set_vector_element(lhs, index, rhs);
+}
+)");
+}
+
+TEST_F(HlslASTPrinterTest_Assign, Emit_Matrix_f16_Assign_Vector) {
+    Enable(wgsl::Extension::kF16);
+
+    Func("fn", tint::Empty, ty.void_(),
+         Vector{
+             Decl(Var("lhs", ty.mat4x2<f16>())),
+             Decl(Var("rhs", ty.vec2<f16>())),
+             Decl(Let("index", ty.u32(), Expr(0_u))),
+             Assign(IndexAccessor("lhs", "index"), "rhs"),
+         });
+
+    ASTPrinter& gen = Build();
+
+    ASSERT_TRUE(gen.Generate());
+    EXPECT_EQ(
+        gen.Result(),
+        R"(void set_matrix_column(inout matrix<float16_t, 4, 2> mat, int col, vector<float16_t, 2> val) {
+  switch (col) {
+    case 0: mat[0] = val; break;
+    case 1: mat[1] = val; break;
+    case 2: mat[2] = val; break;
+    case 3: mat[3] = val; break;
+  }
+}
+
+void fn() {
+  matrix<float16_t, 4, 2> lhs = matrix<float16_t, 4, 2>(float16_t(0.0h), float16_t(0.0h), float16_t(0.0h), float16_t(0.0h), float16_t(0.0h), float16_t(0.0h), float16_t(0.0h), float16_t(0.0h));
+  vector<float16_t, 2> rhs = vector<float16_t, 2>(float16_t(0.0h), float16_t(0.0h));
+  uint index = 0u;
+  set_matrix_column(lhs, index, rhs);
+}
+)");
+}
+
+TEST_F(HlslASTPrinterTest_Assign, Emit_Matrix_f16_Assign_Scalar) {
+    Enable(wgsl::Extension::kF16);
+
+    auto* col = IndexAccessor("lhs", "col");
+    auto* el = IndexAccessor(col, "row");
+    Func("fn", tint::Empty, ty.void_(),
+         Vector{
+             Decl(Var("lhs", ty.mat4x2<f16>())),
+             Decl(Var("rhs", ty.f16())),
+             Decl(Let("col", ty.u32(), Expr(0_u))),
+             Decl(Let("row", ty.u32(), Expr(1_u))),
+             Assign(el, "rhs"),
+         });
+
+    ASTPrinter& gen = Build();
+
+    ASSERT_TRUE(gen.Generate());
+    EXPECT_EQ(
+        gen.Result(),
+        R"(void set_matrix_scalar(inout matrix<float16_t, 4, 2> mat, int col, int row, float16_t val) {
+  switch (col) {
+    case 0:
+      mat[0] = (row.xx == int2(0, 1)) ? val.xx : mat[0];
+      break;
+    case 1:
+      mat[1] = (row.xx == int2(0, 1)) ? val.xx : mat[1];
+      break;
+    case 2:
+      mat[2] = (row.xx == int2(0, 1)) ? val.xx : mat[2];
+      break;
+    case 3:
+      mat[3] = (row.xx == int2(0, 1)) ? val.xx : mat[3];
+      break;
+  }
+}
+
+void fn() {
+  matrix<float16_t, 4, 2> lhs = matrix<float16_t, 4, 2>(float16_t(0.0h), float16_t(0.0h), float16_t(0.0h), float16_t(0.0h), float16_t(0.0h), float16_t(0.0h), float16_t(0.0h), float16_t(0.0h));
+  float16_t rhs = float16_t(0.0h);
+  uint col = 0u;
+  uint row = 1u;
+  set_matrix_scalar(lhs, col, row, rhs);
 }
 )");
 }
diff --git a/src/tint/lang/hlsl/writer/ast_printer/ast_printer.cc b/src/tint/lang/hlsl/writer/ast_printer/ast_printer.cc
index d6eec1a..9070b70 100644
--- a/src/tint/lang/hlsl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/hlsl/writer/ast_printer/ast_printer.cc
@@ -425,9 +425,9 @@
             [&](const ast::Struct* str) {
                 auto* ty = builder_.Sem().Get(str);
                 auto address_space_uses = ty->AddressSpaceUsage();
-                if (address_space_uses.size() !=
-                    (address_space_uses.count(core::AddressSpace::kStorage) +
-                     address_space_uses.count(core::AddressSpace::kUniform))) {
+                if (address_space_uses.Count() !=
+                    ((address_space_uses.Contains(core::AddressSpace::kStorage) ? 1u : 0u) +
+                     (address_space_uses.Contains(core::AddressSpace::kUniform) ? 1u : 0u))) {
                     // The structure is used as something other than a storage buffer or
                     // uniform buffer, so it needs to be emitted.
                     // Storage buffer are read and written to via a ByteAddressBuffer
@@ -461,15 +461,7 @@
 bool ASTPrinter::EmitDynamicVectorAssignment(const ast::AssignmentStatement* stmt,
                                              const core::type::Vector* vec) {
     auto name = tint::GetOrCreate(dynamic_vector_write_, vec, [&]() -> std::string {
-        std::string fn;
-        {
-            StringStream ss;
-            if (!EmitType(ss, vec, tint::core::AddressSpace::kUndefined, core::Access::kUndefined,
-                          "")) {
-                return "";
-            }
-            fn = UniqueIdentifier("set_" + ss.str());
-        }
+        std::string fn = UniqueIdentifier("set_vector_element");
         {
             auto out = Line(&helpers_);
             out << "void " << fn << "(inout ";
@@ -534,15 +526,7 @@
 bool ASTPrinter::EmitDynamicMatrixVectorAssignment(const ast::AssignmentStatement* stmt,
                                                    const core::type::Matrix* mat) {
     auto name = tint::GetOrCreate(dynamic_matrix_vector_write_, mat, [&]() -> std::string {
-        std::string fn;
-        {
-            StringStream ss;
-            if (!EmitType(ss, mat, tint::core::AddressSpace::kUndefined, core::Access::kUndefined,
-                          "")) {
-                return "";
-            }
-            fn = UniqueIdentifier("set_vector_" + ss.str());
-        }
+        std::string fn = UniqueIdentifier("set_matrix_column");
         {
             auto out = Line(&helpers_);
             out << "void " << fn << "(inout ";
@@ -603,15 +587,7 @@
     auto* lhs_col_access = lhs_row_access->object->As<ast::IndexAccessorExpression>();
 
     auto name = tint::GetOrCreate(dynamic_matrix_scalar_write_, mat, [&]() -> std::string {
-        std::string fn;
-        {
-            StringStream ss;
-            if (!EmitType(ss, mat, tint::core::AddressSpace::kUndefined, core::Access::kUndefined,
-                          "")) {
-                return "";
-            }
-            fn = UniqueIdentifier("set_scalar_" + ss.str());
-        }
+        std::string fn = UniqueIdentifier("set_matrix_scalar");
         {
             auto out = Line(&helpers_);
             out << "void " << fn << "(inout ";
@@ -4578,21 +4554,22 @@
 
             if (auto location = attributes.location) {
                 auto& pipeline_stage_uses = str->PipelineStageUses();
-                if (TINT_UNLIKELY(pipeline_stage_uses.size() != 1)) {
+                if (TINT_UNLIKELY(pipeline_stage_uses.Count() != 1)) {
                     TINT_ICE() << "invalid entry point IO struct uses";
                 }
-                if (pipeline_stage_uses.count(core::type::PipelineStageUsage::kVertexInput)) {
+                if (pipeline_stage_uses.Contains(core::type::PipelineStageUsage::kVertexInput)) {
                     post += " : TEXCOORD" + std::to_string(location.value());
-                } else if (pipeline_stage_uses.count(
+                } else if (pipeline_stage_uses.Contains(
                                core::type::PipelineStageUsage::kVertexOutput)) {
                     post += " : TEXCOORD" + std::to_string(location.value());
-                } else if (pipeline_stage_uses.count(
+                } else if (pipeline_stage_uses.Contains(
                                core::type::PipelineStageUsage::kFragmentInput)) {
                     post += " : TEXCOORD" + std::to_string(location.value());
-                } else if (TINT_LIKELY(pipeline_stage_uses.count(
+                } else if (TINT_LIKELY(pipeline_stage_uses.Contains(
                                core::type::PipelineStageUsage::kFragmentOutput))) {
-                    if (auto index = attributes.index) {
-                        post += " : SV_Target" + std::to_string(location.value() + index.value());
+                    if (auto blend_src = attributes.blend_src) {
+                        post +=
+                            " : SV_Target" + std::to_string(location.value() + blend_src.value());
                     } else {
                         post += " : SV_Target" + std::to_string(location.value());
                     }
@@ -4688,41 +4665,11 @@
     return true;
 }
 
-bool ASTPrinter::IsStructOrArrayOfMatrix(const core::type::Type* ty) {
-    if (!ty->IsAnyOf<core::type::Struct, core::type::Array>()) {
-        return false;
-    }
-    return GetOrCreate(is_struct_or_array_of_matrix_, ty, [&]() {
-        Vector<const core::type::Type*, 4> to_visit({ty});
-        while (!to_visit.IsEmpty()) {
-            auto* curr = to_visit.Pop();
-            if (curr->Is<core::type::Matrix>()) {
-                return true;
-            }
-            auto [child_ty, child_count] = curr->Elements();
-            if (child_ty) {
-                to_visit.Push(child_ty);
-            } else {
-                for (uint32_t i = 0; i < child_count; ++i) {
-                    to_visit.Push(curr->Element(i));
-                }
-            }
-        }
-        return false;
-    });
-}
-
 bool ASTPrinter::EmitLet(const ast::Let* let) {
     auto* sem = builder_.Sem().Get(let);
     auto* type = sem->Type()->UnwrapRef();
 
     auto out = Line();
-
-    // TODO(crbug.com/tint/2059): Workaround DXC bug with const instances of struct/array-of-matrix.
-    if (!IsStructOrArrayOfMatrix(type)) {
-        out << "const ";
-    }
-
     if (!EmitTypeAndName(out, type, core::AddressSpace::kUndefined, core::Access::kUndefined,
                          let->name->symbol.Name())) {
         return false;
diff --git a/src/tint/lang/hlsl/writer/ast_printer/ast_printer.h b/src/tint/lang/hlsl/writer/ast_printer/ast_printer.h
index 3d14eac..9e60718 100644
--- a/src/tint/lang/hlsl/writer/ast_printer/ast_printer.h
+++ b/src/tint/lang/hlsl/writer/ast_printer/ast_printer.h
@@ -596,10 +596,6 @@
         return builder_.TypeOf(ptr);
     }
 
-    /// @return true if ty is a struct or array with a matrix member (recursively), false otherwise.
-    /// @param ty the type that will be queried.
-    bool IsStructOrArrayOfMatrix(const core::type::Type* ty);
-
     ProgramBuilder builder_;
 
     /// Helper functions emitted at the top of the output
@@ -618,7 +614,6 @@
     std::unordered_map<const core::type::Matrix*, std::string> dynamic_matrix_scalar_write_;
     std::unordered_map<const core::type::Type*, std::string> value_or_one_if_zero_;
     std::unordered_set<const core::type::Struct*> emitted_structs_;
-    std::unordered_map<const core::type::Type*, bool> is_struct_or_array_of_matrix_;
 
     // The line index in current_buffer_ of the current global declaration / function.
     size_t global_insertion_point_ = 0;
diff --git a/src/tint/lang/hlsl/writer/ast_printer/bitcast_test.cc b/src/tint/lang/hlsl/writer/ast_printer/bitcast_test.cc
index 8c5b101..bb89d6f 100644
--- a/src/tint/lang/hlsl/writer/ast_printer/bitcast_test.cc
+++ b/src/tint/lang/hlsl/writer/ast_printer/bitcast_test.cc
@@ -128,13 +128,13 @@
 
 [numthreads(1, 1, 1)]
 void test_function() {
-  const vector<float16_t, 2> a = vector<float16_t, 2>(float16_t(1.0h), float16_t(2.0h));
-  const int b = tint_bitcast_from_f16(a);
-  const vector<float16_t, 2> c = tint_bitcast_to_f16(b);
-  const float d = tint_bitcast_from_f16_1(c);
-  const vector<float16_t, 2> e = tint_bitcast_to_f16_1(d);
-  const uint f = tint_bitcast_from_f16_2(e);
-  const vector<float16_t, 2> g = tint_bitcast_to_f16_2(f);
+  vector<float16_t, 2> a = vector<float16_t, 2>(float16_t(1.0h), float16_t(2.0h));
+  int b = tint_bitcast_from_f16(a);
+  vector<float16_t, 2> c = tint_bitcast_to_f16(b);
+  float d = tint_bitcast_from_f16_1(c);
+  vector<float16_t, 2> e = tint_bitcast_to_f16_1(d);
+  uint f = tint_bitcast_from_f16_2(e);
+  vector<float16_t, 2> g = tint_bitcast_to_f16_2(f);
   return;
 }
 )");
@@ -194,13 +194,13 @@
 
 [numthreads(1, 1, 1)]
 void test_function() {
-  const vector<float16_t, 4> a = vector<float16_t, 4>(float16_t(1.0h), float16_t(2.0h), float16_t(3.0h), float16_t(4.0h));
-  const int2 b = tint_bitcast_from_f16(a);
-  const vector<float16_t, 4> c = tint_bitcast_to_f16(b);
-  const float2 d = tint_bitcast_from_f16_1(c);
-  const vector<float16_t, 4> e = tint_bitcast_to_f16_1(d);
-  const uint2 f = tint_bitcast_from_f16_2(e);
-  const vector<float16_t, 4> g = tint_bitcast_to_f16_2(f);
+  vector<float16_t, 4> a = vector<float16_t, 4>(float16_t(1.0h), float16_t(2.0h), float16_t(3.0h), float16_t(4.0h));
+  int2 b = tint_bitcast_from_f16(a);
+  vector<float16_t, 4> c = tint_bitcast_to_f16(b);
+  float2 d = tint_bitcast_from_f16_1(c);
+  vector<float16_t, 4> e = tint_bitcast_to_f16_1(d);
+  uint2 f = tint_bitcast_from_f16_2(e);
+  vector<float16_t, 4> g = tint_bitcast_to_f16_2(f);
   return;
 }
 )");
diff --git a/src/tint/lang/hlsl/writer/ast_printer/builtin_test.cc b/src/tint/lang/hlsl/writer/ast_printer/builtin_test.cc
index 803be81..00788ed 100644
--- a/src/tint/lang/hlsl/writer/ast_printer/builtin_test.cc
+++ b/src/tint/lang/hlsl/writer/ast_printer/builtin_test.cc
@@ -407,8 +407,8 @@
 
 [numthreads(1, 1, 1)]
 void test_function() {
-  const float f = 1.5f;
-  const modf_result_f32 v = tint_modf(f);
+  float f = 1.5f;
+  modf_result_f32 v = tint_modf(f);
   return;
 }
 )");
@@ -435,8 +435,8 @@
 
 [numthreads(1, 1, 1)]
 void test_function() {
-  const float16_t f = float16_t(1.5h);
-  const modf_result_f16 v = tint_modf(f);
+  float16_t f = float16_t(1.5h);
+  modf_result_f16 v = tint_modf(f);
   return;
 }
 )");
@@ -461,8 +461,8 @@
 
 [numthreads(1, 1, 1)]
 void test_function() {
-  const float3 f = float3(1.5f, 2.5f, 3.5f);
-  const modf_result_vec3_f32 v = tint_modf(f);
+  float3 f = float3(1.5f, 2.5f, 3.5f);
+  modf_result_vec3_f32 v = tint_modf(f);
   return;
 }
 )");
@@ -489,8 +489,8 @@
 
 [numthreads(1, 1, 1)]
 void test_function() {
-  const vector<float16_t, 3> f = vector<float16_t, 3>(float16_t(1.5h), float16_t(2.5h), float16_t(3.5h));
-  const modf_result_vec3_f16 v = tint_modf(f);
+  vector<float16_t, 3> f = vector<float16_t, 3>(float16_t(1.5h), float16_t(2.5h), float16_t(3.5h));
+  modf_result_vec3_f16 v = tint_modf(f);
   return;
 }
 )");
@@ -508,7 +508,7 @@
 };
 [numthreads(1, 1, 1)]
 void test_function() {
-  const modf_result_f32 v = {0.5f, 1.0f};
+  modf_result_f32 v = {0.5f, 1.0f};
   return;
 }
 )");
@@ -528,7 +528,7 @@
 };
 [numthreads(1, 1, 1)]
 void test_function() {
-  const modf_result_f16 v = {float16_t(0.5h), float16_t(1.0h)};
+  modf_result_f16 v = {float16_t(0.5h), float16_t(1.0h)};
   return;
 }
 )");
@@ -546,7 +546,7 @@
 };
 [numthreads(1, 1, 1)]
 void test_function() {
-  const modf_result_vec3_f32 v = {(0.5f).xxx, float3(1.0f, 2.0f, 3.0f)};
+  modf_result_vec3_f32 v = {(0.5f).xxx, float3(1.0f, 2.0f, 3.0f)};
   return;
 }
 )");
@@ -566,7 +566,7 @@
 };
 [numthreads(1, 1, 1)]
 void test_function() {
-  const modf_result_vec3_f16 v = {(float16_t(0.5h)).xxx, vector<float16_t, 3>(float16_t(1.0h), float16_t(2.0h), float16_t(3.0h))};
+  modf_result_vec3_f16 v = {(float16_t(0.5h)).xxx, vector<float16_t, 3>(float16_t(1.0h), float16_t(2.0h), float16_t(3.0h))};
   return;
 }
 )");
@@ -722,7 +722,7 @@
 };
 [numthreads(1, 1, 1)]
 void test_function() {
-  const frexp_result_f32 v = {0.5f, 1};
+  frexp_result_f32 v = {0.5f, 1};
   return;
 }
 )");
@@ -742,7 +742,7 @@
 };
 [numthreads(1, 1, 1)]
 void test_function() {
-  const frexp_result_f16 v = {float16_t(0.5h), 1};
+  frexp_result_f16 v = {float16_t(0.5h), 1};
   return;
 }
 )");
@@ -760,7 +760,7 @@
 };
 [numthreads(1, 1, 1)]
 void test_function() {
-  const frexp_result_vec3_f32 v = (frexp_result_vec3_f32)0;
+  frexp_result_vec3_f32 v = (frexp_result_vec3_f32)0;
   return;
 }
 )");
@@ -780,7 +780,7 @@
 };
 [numthreads(1, 1, 1)]
 void test_function() {
-  const frexp_result_vec3_f16 v = (frexp_result_vec3_f16)0;
+  frexp_result_vec3_f16 v = (frexp_result_vec3_f16)0;
   return;
 }
 )");
@@ -827,7 +827,7 @@
 [numthreads(1, 1, 1)]
 void test_function() {
   float val = 0.0f;
-  const float tint_symbol = tint_degrees(val);
+  float tint_symbol = tint_degrees(val);
   return;
 }
 )");
@@ -848,7 +848,7 @@
 [numthreads(1, 1, 1)]
 void test_function() {
   float3 val = float3(0.0f, 0.0f, 0.0f);
-  const float3 tint_symbol = tint_degrees(val);
+  float3 tint_symbol = tint_degrees(val);
   return;
 }
 )");
@@ -871,7 +871,7 @@
 [numthreads(1, 1, 1)]
 void test_function() {
   float16_t val = float16_t(0.0h);
-  const float16_t tint_symbol = tint_degrees(val);
+  float16_t tint_symbol = tint_degrees(val);
   return;
 }
 )");
@@ -894,7 +894,7 @@
 [numthreads(1, 1, 1)]
 void test_function() {
   vector<float16_t, 3> val = vector<float16_t, 3>(float16_t(0.0h), float16_t(0.0h), float16_t(0.0h));
-  const vector<float16_t, 3> tint_symbol = tint_degrees(val);
+  vector<float16_t, 3> tint_symbol = tint_degrees(val);
   return;
 }
 )");
@@ -915,7 +915,7 @@
 [numthreads(1, 1, 1)]
 void test_function() {
   float val = 0.0f;
-  const float tint_symbol = tint_radians(val);
+  float tint_symbol = tint_radians(val);
   return;
 }
 )");
@@ -936,7 +936,7 @@
 [numthreads(1, 1, 1)]
 void test_function() {
   float3 val = float3(0.0f, 0.0f, 0.0f);
-  const float3 tint_symbol = tint_radians(val);
+  float3 tint_symbol = tint_radians(val);
   return;
 }
 )");
@@ -959,7 +959,7 @@
 [numthreads(1, 1, 1)]
 void test_function() {
   float16_t val = float16_t(0.0h);
-  const float16_t tint_symbol = tint_radians(val);
+  float16_t tint_symbol = tint_radians(val);
   return;
 }
 )");
@@ -982,7 +982,7 @@
 [numthreads(1, 1, 1)]
 void test_function() {
   vector<float16_t, 3> val = vector<float16_t, 3>(float16_t(0.0h), float16_t(0.0h), float16_t(0.0h));
-  const vector<float16_t, 3> tint_symbol = tint_radians(val);
+  vector<float16_t, 3> tint_symbol = tint_radians(val);
   return;
 }
 )");
@@ -999,7 +999,7 @@
     EXPECT_EQ(gen.Result(), R"([numthreads(1, 1, 1)]
 void test_function() {
   int val = 0;
-  const int tint_symbol = int(sign(val));
+  int tint_symbol = int(sign(val));
   return;
 }
 )");
@@ -1016,7 +1016,7 @@
     EXPECT_EQ(gen.Result(), R"([numthreads(1, 1, 1)]
 void test_function() {
   int3 val = int3(0, 0, 0);
-  const int3 tint_symbol = int3(sign(val));
+  int3 tint_symbol = int3(sign(val));
   return;
 }
 )");
@@ -1033,7 +1033,7 @@
     EXPECT_EQ(gen.Result(), R"([numthreads(1, 1, 1)]
 void test_function() {
   float val = 0.0f;
-  const float tint_symbol = float(sign(val));
+  float tint_symbol = float(sign(val));
   return;
 }
 )");
@@ -1050,7 +1050,7 @@
     EXPECT_EQ(gen.Result(), R"([numthreads(1, 1, 1)]
 void test_function() {
   float3 val = float3(0.0f, 0.0f, 0.0f);
-  const float3 tint_symbol = float3(sign(val));
+  float3 tint_symbol = float3(sign(val));
   return;
 }
 )");
@@ -1069,7 +1069,7 @@
     EXPECT_EQ(gen.Result(), R"([numthreads(1, 1, 1)]
 void test_function() {
   float16_t val = float16_t(0.0h);
-  const float16_t tint_symbol = float16_t(sign(val));
+  float16_t tint_symbol = float16_t(sign(val));
   return;
 }
 )");
@@ -1088,7 +1088,7 @@
     EXPECT_EQ(gen.Result(), R"([numthreads(1, 1, 1)]
 void test_function() {
   vector<float16_t, 3> val = vector<float16_t, 3>(float16_t(0.0h), float16_t(0.0h), float16_t(0.0h));
-  const vector<float16_t, 3> tint_symbol = vector<float16_t, 3>(sign(val));
+  vector<float16_t, 3> tint_symbol = vector<float16_t, 3>(sign(val));
   return;
 }
 )");
@@ -1109,7 +1109,7 @@
 [numthreads(1, 1, 1)]
 void test_function() {
   float val = 0.0f;
-  const float tint_symbol = tint_trunc(val);
+  float tint_symbol = tint_trunc(val);
   return;
 }
 )");
@@ -1130,7 +1130,7 @@
 [numthreads(1, 1, 1)]
 void test_function() {
   float3 val = float3(0.0f, 0.0f, 0.0f);
-  const float3 tint_symbol = tint_trunc(val);
+  float3 tint_symbol = tint_trunc(val);
   return;
 }
 )");
@@ -1153,7 +1153,7 @@
 [numthreads(1, 1, 1)]
 void test_function() {
   float16_t val = float16_t(0.0h);
-  const float16_t tint_symbol = tint_trunc(val);
+  float16_t tint_symbol = tint_trunc(val);
   return;
 }
 )");
@@ -1176,7 +1176,7 @@
 [numthreads(1, 1, 1)]
 void test_function() {
   vector<float16_t, 3> val = vector<float16_t, 3>(float16_t(0.0h), float16_t(0.0h), float16_t(0.0h));
-  const vector<float16_t, 3> tint_symbol = tint_trunc(val);
+  vector<float16_t, 3> tint_symbol = tint_trunc(val);
   return;
 }
 )");
@@ -1468,7 +1468,7 @@
 void test_function() {
   uint val1 = 0u;
   uint val2 = 0u;
-  const int tint_symbol = tint_dot4I8Packed(val1, val2);
+  int tint_symbol = tint_dot4I8Packed(val1, val2);
   return;
 }
 )");
@@ -1494,7 +1494,7 @@
 void test_function() {
   uint val1 = 0u;
   uint val2 = 0u;
-  const uint tint_symbol = tint_dot4U8Packed(val1, val2);
+  uint tint_symbol = tint_dot4U8Packed(val1, val2);
   return;
 }
 )");
@@ -1511,7 +1511,7 @@
     EXPECT_EQ(gen.Result(), R"([numthreads(1, 1, 1)]
 void test_function() {
   int val1 = 0;
-  const int tint_symbol = asint(countbits(asuint(val1)));
+  int tint_symbol = asint(countbits(asuint(val1)));
   return;
 }
 )");
@@ -1528,7 +1528,7 @@
     EXPECT_EQ(gen.Result(), R"([numthreads(1, 1, 1)]
 void test_function() {
   int val1 = 0;
-  const int tint_symbol = asint(reversebits(asuint(val1)));
+  int tint_symbol = asint(reversebits(asuint(val1)));
   return;
 }
 )");
diff --git a/src/tint/lang/hlsl/writer/ast_printer/constructor_test.cc b/src/tint/lang/hlsl/writer/ast_printer/constructor_test.cc
index 7b8b51b..5a9a9a3 100644
--- a/src/tint/lang/hlsl/writer/ast_printer/constructor_test.cc
+++ b/src/tint/lang/hlsl/writer/ast_printer/constructor_test.cc
@@ -204,7 +204,7 @@
 
     ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
     EXPECT_THAT(gen.Result(), HasSubstr(R"(float v = 2.0f;
-  const float3 tint_symbol = float3((v).xxx);)"));
+  float3 tint_symbol = float3((v).xxx);)"));
 }
 
 TEST_F(HlslASTPrinterTest_Constructor, Type_Vec_SingleScalar_F16_Var) {
@@ -218,7 +218,7 @@
 
     ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
     EXPECT_THAT(gen.Result(), HasSubstr(R"(float16_t v = float16_t(2.0h);
-  const vector<float16_t, 3> tint_symbol = vector<float16_t, 3>((v).xxx);)"));
+  vector<float16_t, 3> tint_symbol = vector<float16_t, 3>((v).xxx);)"));
 }
 
 TEST_F(HlslASTPrinterTest_Constructor, Type_Vec_SingleScalar_Bool_Literal) {
@@ -239,7 +239,7 @@
 
     ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
     EXPECT_THAT(gen.Result(), HasSubstr(R"(bool v = true;
-  const bool3 tint_symbol = bool3((v).xxx);)"));
+  bool3 tint_symbol = bool3((v).xxx);)"));
 }
 
 TEST_F(HlslASTPrinterTest_Constructor, Type_Vec_SingleScalar_Int) {
diff --git a/src/tint/lang/hlsl/writer/ast_printer/function_test.cc b/src/tint/lang/hlsl/writer/ast_printer/function_test.cc
index 6158a91..2f6822c 100644
--- a/src/tint/lang/hlsl/writer/ast_printer/function_test.cc
+++ b/src/tint/lang/hlsl/writer/ast_printer/function_test.cc
@@ -158,7 +158,7 @@
 }
 
 tint_symbol_2 frag_main(tint_symbol_1 tint_symbol) {
-  const float inner_result = frag_main_inner(tint_symbol.foo);
+  float inner_result = frag_main_inner(tint_symbol.foo);
   tint_symbol_2 wrapper_result = (tint_symbol_2)0;
   wrapper_result.value = inner_result;
   return wrapper_result;
@@ -197,7 +197,7 @@
 }
 
 tint_symbol_2 frag_main(tint_symbol_1 tint_symbol) {
-  const float inner_result = frag_main_inner(float4(tint_symbol.coord.xyz, (1.0f / tint_symbol.coord.w)));
+  float inner_result = frag_main_inner(float4(tint_symbol.coord.xyz, (1.0f / tint_symbol.coord.w)));
   tint_symbol_2 wrapper_result = (tint_symbol_2)0;
   wrapper_result.value = inner_result;
   return wrapper_result;
@@ -256,12 +256,12 @@
 };
 
 Interface vert_main_inner() {
-  const Interface tint_symbol_3 = {(0.0f).xxxx, 0.5f, 0.25f};
+  Interface tint_symbol_3 = {(0.0f).xxxx, 0.5f, 0.25f};
   return tint_symbol_3;
 }
 
 tint_symbol vert_main() {
-  const Interface inner_result = vert_main_inner();
+  Interface inner_result = vert_main_inner();
   tint_symbol wrapper_result = (tint_symbol)0;
   wrapper_result.pos = inner_result.pos;
   wrapper_result.col1 = inner_result.col1;
@@ -276,13 +276,13 @@
 };
 
 void frag_main_inner(Interface inputs) {
-  const float r = inputs.col1;
-  const float g = inputs.col2;
-  const float4 p = inputs.pos;
+  float r = inputs.col1;
+  float g = inputs.col2;
+  float4 p = inputs.pos;
 }
 
 void frag_main(tint_symbol_2 tint_symbol_1) {
-  const Interface tint_symbol_4 = {float4(tint_symbol_1.pos.xyz, (1.0f / tint_symbol_1.pos.w)), tint_symbol_1.col1, tint_symbol_1.col2};
+  Interface tint_symbol_4 = {float4(tint_symbol_1.pos.xyz, (1.0f / tint_symbol_1.pos.w)), tint_symbol_1.col1, tint_symbol_1.col2};
   frag_main_inner(tint_symbol_4);
   return;
 }
@@ -332,7 +332,7 @@
 };
 
 VertexOutput foo(float x) {
-  const VertexOutput tint_symbol_2 = {float4(x, x, x, 1.0f)};
+  VertexOutput tint_symbol_2 = {float4(x, x, x, 1.0f)};
   return tint_symbol_2;
 }
 
@@ -345,7 +345,7 @@
 }
 
 tint_symbol vert_main1() {
-  const VertexOutput inner_result = vert_main1_inner();
+  VertexOutput inner_result = vert_main1_inner();
   tint_symbol wrapper_result = (tint_symbol)0;
   wrapper_result.pos = inner_result.pos;
   return wrapper_result;
@@ -360,7 +360,7 @@
 }
 
 tint_symbol_1 vert_main2() {
-  const VertexOutput inner_result_1 = vert_main2_inner();
+  VertexOutput inner_result_1 = vert_main2_inner();
   tint_symbol_1 wrapper_result_1 = (tint_symbol_1)0;
   wrapper_result_1.pos = inner_result_1.pos;
   return wrapper_result_1;
diff --git a/src/tint/lang/hlsl/writer/ast_printer/member_accessor_test.cc b/src/tint/lang/hlsl/writer/ast_printer/member_accessor_test.cc
index 20ebf39..f06ef68 100644
--- a/src/tint/lang/hlsl/writer/ast_printer/member_accessor_test.cc
+++ b/src/tint/lang/hlsl/writer/ast_printer/member_accessor_test.cc
@@ -1405,7 +1405,7 @@
 RWByteAddressBuffer data : register(u0, space1);
 
 Inner data_load(uint offset) {
-  const Inner tint_symbol = {asint(data.Load((offset + 0u)))};
+  Inner tint_symbol = {asint(data.Load((offset + 0u)))};
   return tint_symbol;
 }
 
@@ -1458,7 +1458,7 @@
 
 Inner data_load(uint offset) {
   const uint scalar_offset = ((offset + 0u)) / 4;
-  const Inner tint_symbol = {asint(data[scalar_offset / 4][scalar_offset % 4])};
+  Inner tint_symbol = {asint(data[scalar_offset / 4][scalar_offset % 4])};
   return tint_symbol;
 }
 
diff --git a/src/tint/lang/hlsl/writer/ast_printer/module_constant_test.cc b/src/tint/lang/hlsl/writer/ast_printer/module_constant_test.cc
index 59f3289..17559d3 100644
--- a/src/tint/lang/hlsl/writer/ast_printer/module_constant_test.cc
+++ b/src/tint/lang/hlsl/writer/ast_printer/module_constant_test.cc
@@ -45,7 +45,7 @@
     ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
 
     EXPECT_EQ(gen.Result(), R"(void f() {
-  const int l = 1;
+  int l = 1;
 }
 )");
 }
@@ -59,7 +59,7 @@
     ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
 
     EXPECT_EQ(gen.Result(), R"(void f() {
-  const float l = 1.0f;
+  float l = 1.0f;
 }
 )");
 }
@@ -73,7 +73,7 @@
     ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
 
     EXPECT_EQ(gen.Result(), R"(void f() {
-  const int l = 1;
+  int l = 1;
 }
 )");
 }
@@ -87,7 +87,7 @@
     ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
 
     EXPECT_EQ(gen.Result(), R"(void f() {
-  const uint l = 1u;
+  uint l = 1u;
 }
 )");
 }
@@ -101,7 +101,7 @@
     ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
 
     EXPECT_EQ(gen.Result(), R"(void f() {
-  const float l = 1.0f;
+  float l = 1.0f;
 }
 )");
 }
@@ -117,7 +117,7 @@
     ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
 
     EXPECT_EQ(gen.Result(), R"(void f() {
-  const float16_t l = float16_t(1.0h);
+  float16_t l = float16_t(1.0h);
 }
 )");
 }
@@ -131,7 +131,7 @@
     ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
 
     EXPECT_EQ(gen.Result(), R"(void f() {
-  const int3 l = int3(1, 2, 3);
+  int3 l = int3(1, 2, 3);
 }
 )");
 }
@@ -145,7 +145,7 @@
     ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
 
     EXPECT_EQ(gen.Result(), R"(void f() {
-  const float3 l = float3(1.0f, 2.0f, 3.0f);
+  float3 l = float3(1.0f, 2.0f, 3.0f);
 }
 )");
 }
@@ -159,7 +159,7 @@
     ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
 
     EXPECT_EQ(gen.Result(), R"(void f() {
-  const float3 l = float3(1.0f, 2.0f, 3.0f);
+  float3 l = float3(1.0f, 2.0f, 3.0f);
 }
 )");
 }
@@ -175,7 +175,7 @@
     ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
 
     EXPECT_EQ(gen.Result(), R"(void f() {
-  const vector<float16_t, 3> l = vector<float16_t, 3>(float16_t(1.0h), float16_t(2.0h), float16_t(3.0h));
+  vector<float16_t, 3> l = vector<float16_t, 3>(float16_t(1.0h), float16_t(2.0h), float16_t(3.0h));
 }
 )");
 }
@@ -189,7 +189,7 @@
     ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
 
     EXPECT_EQ(gen.Result(), R"(void f() {
-  const float2x3 l = float2x3(float3(1.0f, 2.0f, 3.0f), float3(4.0f, 5.0f, 6.0f));
+  float2x3 l = float2x3(float3(1.0f, 2.0f, 3.0f), float3(4.0f, 5.0f, 6.0f));
 }
 )");
 }
@@ -203,7 +203,7 @@
     ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
 
     EXPECT_EQ(gen.Result(), R"(void f() {
-  const float2x3 l = float2x3(float3(1.0f, 2.0f, 3.0f), float3(4.0f, 5.0f, 6.0f));
+  float2x3 l = float2x3(float3(1.0f, 2.0f, 3.0f), float3(4.0f, 5.0f, 6.0f));
 }
 )");
 }
@@ -219,7 +219,7 @@
     ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
 
     EXPECT_EQ(gen.Result(), R"(void f() {
-  const matrix<float16_t, 2, 3> l = matrix<float16_t, 2, 3>(vector<float16_t, 3>(float16_t(1.0h), float16_t(2.0h), float16_t(3.0h)), vector<float16_t, 3>(float16_t(4.0h), float16_t(5.0h), float16_t(6.0h)));
+  matrix<float16_t, 2, 3> l = matrix<float16_t, 2, 3>(vector<float16_t, 3>(float16_t(1.0h), float16_t(2.0h), float16_t(3.0h)), vector<float16_t, 3>(float16_t(4.0h), float16_t(5.0h), float16_t(6.0h)));
 }
 )");
 }
@@ -233,7 +233,7 @@
     ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
 
     EXPECT_EQ(gen.Result(), R"(void f() {
-  const float l[3] = {1.0f, 2.0f, 3.0f};
+  float l[3] = {1.0f, 2.0f, 3.0f};
 }
 )");
 }
@@ -250,7 +250,7 @@
     ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
 
     EXPECT_EQ(gen.Result(), R"(void f() {
-  const bool2 l[3] = {bool2(true, false), bool2(false, true), (true).xx};
+  bool2 l[3] = {bool2(true, false), bool2(false, true), (true).xx};
 }
 )");
 }
diff --git a/src/tint/lang/hlsl/writer/ast_printer/sanitizer_test.cc b/src/tint/lang/hlsl/writer/ast_printer/sanitizer_test.cc
index d9ebaf3..5823771 100644
--- a/src/tint/lang/hlsl/writer/ast_printer/sanitizer_test.cc
+++ b/src/tint/lang/hlsl/writer/ast_printer/sanitizer_test.cc
@@ -61,7 +61,7 @@
 void a_func() {
   uint tint_symbol_1 = 0u;
   b.GetDimensions(tint_symbol_1);
-  const uint tint_symbol_2 = ((tint_symbol_1 - 0u) / 4u);
+  uint tint_symbol_2 = ((tint_symbol_1 - 0u) / 4u);
   uint len = tint_symbol_2;
   return;
 }
@@ -95,7 +95,7 @@
 void a_func() {
   uint tint_symbol_1 = 0u;
   b.GetDimensions(tint_symbol_1);
-  const uint tint_symbol_2 = ((tint_symbol_1 - 4u) / 4u);
+  uint tint_symbol_2 = ((tint_symbol_1 - 4u) / 4u);
   uint len = tint_symbol_2;
   return;
 }
@@ -132,7 +132,7 @@
 void a_func() {
   uint tint_symbol_1 = 0u;
   b.GetDimensions(tint_symbol_1);
-  const uint tint_symbol_2 = ((tint_symbol_1 - 0u) / 4u);
+  uint tint_symbol_2 = ((tint_symbol_1 - 0u) / 4u);
   uint len = tint_symbol_2;
   return;
 }
@@ -175,7 +175,7 @@
 void a_func() {
   uint tint_symbol_3 = 0u;
   b.GetDimensions(tint_symbol_3);
-  const uint tint_symbol_4 = ((tint_symbol_3 - 0u) / 4u);
+  uint tint_symbol_4 = ((tint_symbol_3 - 0u) / 4u);
   uint len = (tint_symbol_4 + ((tint_symbol_1[1].w - 0u) / 4u));
   return;
 }
@@ -202,7 +202,7 @@
     auto got = gen.Result();
     auto* expect = R"(void main() {
   int idx = 3;
-  const int tint_symbol[4] = {1, 2, 3, 4};
+  int tint_symbol[4] = {1, 2, 3, 4};
   int pos = tint_symbol[idx];
   return;
 }
@@ -243,7 +243,7 @@
 
 void main() {
   float runtime_value = 3.0f;
-  const S tint_symbol = {1, float3(2.0f, runtime_value, 4.0f), 4};
+  S tint_symbol = {1, float3(2.0f, runtime_value, 4.0f), 4};
   float3 pos = tint_symbol.b;
   return;
 }
diff --git a/src/tint/lang/hlsl/writer/ast_printer/variable_decl_statement_test.cc b/src/tint/lang/hlsl/writer/ast_printer/variable_decl_statement_test.cc
index 8e52ea8..e546046 100644
--- a/src/tint/lang/hlsl/writer/ast_printer/variable_decl_statement_test.cc
+++ b/src/tint/lang/hlsl/writer/ast_printer/variable_decl_statement_test.cc
@@ -62,7 +62,7 @@
     gen.IncrementIndent();
 
     ASSERT_TRUE(gen.EmitStatement(stmt)) << gen.Diagnostics();
-    EXPECT_EQ(gen.Result(), "  const float a = 0.0f;\n");
+    EXPECT_EQ(gen.Result(), "  float a = 0.0f;\n");
 }
 
 TEST_F(HlslASTPrinterTest_VariableDecl, Emit_VariableDeclStatement_Const) {
@@ -91,7 +91,7 @@
     ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
 
     EXPECT_EQ(gen.Result(), R"(void f() {
-  const int l = 1;
+  int l = 1;
 }
 )");
 }
@@ -109,7 +109,7 @@
     ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
 
     EXPECT_EQ(gen.Result(), R"(void f() {
-  const float l = 1.0f;
+  float l = 1.0f;
 }
 )");
 }
@@ -127,7 +127,7 @@
     ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
 
     EXPECT_EQ(gen.Result(), R"(void f() {
-  const int l = 1;
+  int l = 1;
 }
 )");
 }
@@ -145,7 +145,7 @@
     ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
 
     EXPECT_EQ(gen.Result(), R"(void f() {
-  const uint l = 1u;
+  uint l = 1u;
 }
 )");
 }
@@ -163,7 +163,7 @@
     ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
 
     EXPECT_EQ(gen.Result(), R"(void f() {
-  const float l = 1.0f;
+  float l = 1.0f;
 }
 )");
 }
@@ -183,7 +183,7 @@
     ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
 
     EXPECT_EQ(gen.Result(), R"(void f() {
-  const float16_t l = float16_t(1.0h);
+  float16_t l = float16_t(1.0h);
 }
 )");
 }
@@ -201,7 +201,7 @@
     ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
 
     EXPECT_EQ(gen.Result(), R"(void f() {
-  const int3 l = int3(1, 2, 3);
+  int3 l = int3(1, 2, 3);
 }
 )");
 }
@@ -219,7 +219,7 @@
     ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
 
     EXPECT_EQ(gen.Result(), R"(void f() {
-  const float3 l = float3(1.0f, 2.0f, 3.0f);
+  float3 l = float3(1.0f, 2.0f, 3.0f);
 }
 )");
 }
@@ -237,7 +237,7 @@
     ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
 
     EXPECT_EQ(gen.Result(), R"(void f() {
-  const float3 l = float3(1.0f, 2.0f, 3.0f);
+  float3 l = float3(1.0f, 2.0f, 3.0f);
 }
 )");
 }
@@ -257,7 +257,7 @@
     ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
 
     EXPECT_EQ(gen.Result(), R"(void f() {
-  const vector<float16_t, 3> l = vector<float16_t, 3>(float16_t(1.0h), float16_t(2.0h), float16_t(3.0h));
+  vector<float16_t, 3> l = vector<float16_t, 3>(float16_t(1.0h), float16_t(2.0h), float16_t(3.0h));
 }
 )");
 }
@@ -275,7 +275,7 @@
     ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
 
     EXPECT_EQ(gen.Result(), R"(void f() {
-  const float2x3 l = float2x3(float3(1.0f, 2.0f, 3.0f), float3(4.0f, 5.0f, 6.0f));
+  float2x3 l = float2x3(float3(1.0f, 2.0f, 3.0f), float3(4.0f, 5.0f, 6.0f));
 }
 )");
 }
@@ -293,7 +293,7 @@
     ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
 
     EXPECT_EQ(gen.Result(), R"(void f() {
-  const float2x3 l = float2x3(float3(1.0f, 2.0f, 3.0f), float3(4.0f, 5.0f, 6.0f));
+  float2x3 l = float2x3(float3(1.0f, 2.0f, 3.0f), float3(4.0f, 5.0f, 6.0f));
 }
 )");
 }
@@ -313,7 +313,7 @@
     ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
 
     EXPECT_EQ(gen.Result(), R"(void f() {
-  const matrix<float16_t, 2, 3> l = matrix<float16_t, 2, 3>(vector<float16_t, 3>(float16_t(1.0h), float16_t(2.0h), float16_t(3.0h)), vector<float16_t, 3>(float16_t(4.0h), float16_t(5.0h), float16_t(6.0h)));
+  matrix<float16_t, 2, 3> l = matrix<float16_t, 2, 3>(vector<float16_t, 3>(float16_t(1.0h), float16_t(2.0h), float16_t(3.0h)), vector<float16_t, 3>(float16_t(4.0h), float16_t(5.0h), float16_t(6.0h)));
 }
 )");
 }
@@ -331,7 +331,7 @@
     ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
 
     EXPECT_EQ(gen.Result(), R"(void f() {
-  const float l[3] = {1.0f, 2.0f, 3.0f};
+  float l[3] = {1.0f, 2.0f, 3.0f};
 }
 )");
 }
@@ -352,7 +352,7 @@
     ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
 
     EXPECT_EQ(gen.Result(), R"(void f() {
-  const bool2 l[3] = {bool2(true, false), bool2(false, true), (true).xx};
+  bool2 l[3] = {bool2(true, false), bool2(false, true), (true).xx};
 }
 )");
 }
@@ -456,7 +456,7 @@
     ASSERT_TRUE(gen.Generate()) << gen.Diagnostics();
 
     EXPECT_EQ(gen.Result(), R"(void f() {
-  const float2x3 l = float2x3(float3(1.0f, 2.0f, 3.0f), float3(4.0f, 5.0f, 6.0f));
+  float2x3 l = float2x3(float3(1.0f, 2.0f, 3.0f), float3(4.0f, 5.0f, 6.0f));
 }
 )");
 }
diff --git a/src/tint/lang/msl/writer/ast_printer/ast_printer.cc b/src/tint/lang/msl/writer/ast_printer/ast_printer.cc
index 718770e..d7d744c 100644
--- a/src/tint/lang/msl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/msl/writer/ast_printer/ast_printer.cc
@@ -2798,22 +2798,24 @@
 
         if (auto location = attributes.location) {
             auto& pipeline_stage_uses = str->PipelineStageUses();
-            if (TINT_UNLIKELY(pipeline_stage_uses.size() != 1)) {
+            if (TINT_UNLIKELY(pipeline_stage_uses.Count() != 1)) {
                 TINT_ICE() << "invalid entry point IO struct uses for " << str->Name().NameView();
                 return false;
             }
 
-            if (pipeline_stage_uses.count(core::type::PipelineStageUsage::kVertexInput)) {
+            if (pipeline_stage_uses.Contains(core::type::PipelineStageUsage::kVertexInput)) {
                 out << " [[attribute(" + std::to_string(location.value()) + ")]]";
-            } else if (pipeline_stage_uses.count(core::type::PipelineStageUsage::kVertexOutput)) {
+            } else if (pipeline_stage_uses.Contains(
+                           core::type::PipelineStageUsage::kVertexOutput)) {
                 out << " [[user(locn" + std::to_string(location.value()) + ")]]";
-            } else if (pipeline_stage_uses.count(core::type::PipelineStageUsage::kFragmentInput)) {
+            } else if (pipeline_stage_uses.Contains(
+                           core::type::PipelineStageUsage::kFragmentInput)) {
                 out << " [[user(locn" + std::to_string(location.value()) + ")]]";
-            } else if (TINT_LIKELY(pipeline_stage_uses.count(
+            } else if (TINT_LIKELY(pipeline_stage_uses.Contains(
                            core::type::PipelineStageUsage::kFragmentOutput))) {
-                if (auto index = attributes.index) {
+                if (auto blend_src = attributes.blend_src) {
                     out << " [[color(" + std::to_string(location.value()) + ") index(" +
-                               std::to_string(index.value()) + ")]]";
+                               std::to_string(blend_src.value()) + ")]]";
                 } else {
                     out << " [[color(" + std::to_string(location.value()) + ")]]";
                 }
@@ -3018,16 +3020,17 @@
     return true;
 }
 
-std::string_view ASTPrinter::IsolateUB() {
+std::string ASTPrinter::IsolateUB() {
     if (isolate_ub_macro_name_.empty()) {
         isolate_ub_macro_name_ = UniqueIdentifier("TINT_ISOLATE_UB");
-        auto volatile_true = UniqueIdentifier("tint_volatile_true");
-        Line(&helpers_) << "#define " << isolate_ub_macro_name_ << " \\";
-        Line(&helpers_) << "  if (volatile bool " << volatile_true << " = true; " << volatile_true
-                        << ")";
+        Line(&helpers_) << "#define " << isolate_ub_macro_name_ << "(VOLATILE_NAME) \\";
+        Line(&helpers_) << "  volatile bool VOLATILE_NAME = true; \\";
+        Line(&helpers_) << "  if (VOLATILE_NAME)";
         Line(&helpers_);
     }
-    return isolate_ub_macro_name_;
+    StringStream ss;
+    ss << isolate_ub_macro_name_ << "(" << UniqueIdentifier("tint_volatile_true") << ")";
+    return ss.str();
 }
 
 template <typename F>
diff --git a/src/tint/lang/msl/writer/ast_printer/ast_printer.h b/src/tint/lang/msl/writer/ast_printer/ast_printer.h
index 70269c9..a9e9378 100644
--- a/src/tint/lang/msl/writer/ast_printer/ast_printer.h
+++ b/src/tint/lang/msl/writer/ast_printer/ast_printer.h
@@ -380,8 +380,8 @@
 
     /// Lazilly generates the TINT_ISOLATE_UB macro, used to prevent UB statements from affecting
     /// later logic.
-    /// @return the unique name of the TINT_ISOLATE_UB macro.
-    std::string_view IsolateUB();
+    /// @return the MSL to call the TINT_ISOLATE_UB macro.
+    std::string IsolateUB();
 
     /// Handles generating a builtin name
     /// @param builtin the semantic info for the builtin
diff --git a/src/tint/lang/msl/writer/ast_printer/ast_printer_test.cc b/src/tint/lang/msl/writer/ast_printer/ast_printer_test.cc
index 74d8fdd..28a2d3e 100644
--- a/src/tint/lang/msl/writer/ast_printer/ast_printer_test.cc
+++ b/src/tint/lang/msl/writer/ast_printer/ast_printer_test.cc
@@ -226,15 +226,16 @@
     T elements[N];
 };
 
-#define TINT_ISOLATE_UB \
-  if (volatile bool tint_volatile_true = true; tint_volatile_true)
+#define TINT_ISOLATE_UB(VOLATILE_NAME) \
+  volatile bool VOLATILE_NAME = true; \
+  if (VOLATILE_NAME)
 
 struct tint_symbol_3 {
   tint_array<float2x2, 4> m;
 };
 
 void comp_main_inner(uint local_invocation_index, threadgroup tint_array<float2x2, 4>* const tint_symbol) {
-  TINT_ISOLATE_UB for(uint idx = local_invocation_index; (idx < 4u); idx = (idx + 1u)) {
+  TINT_ISOLATE_UB(tint_volatile_true) for(uint idx = local_invocation_index; (idx < 4u); idx = (idx + 1u)) {
     uint const i = idx;
     (*(tint_symbol))[i] = float2x2(float2(0.0f), float2(0.0f));
   }
diff --git a/src/tint/lang/msl/writer/ast_printer/continue_test.cc b/src/tint/lang/msl/writer/ast_printer/continue_test.cc
index 727f05c..ad68153 100644
--- a/src/tint/lang/msl/writer/ast_printer/continue_test.cc
+++ b/src/tint/lang/msl/writer/ast_printer/continue_test.cc
@@ -44,11 +44,12 @@
 
 using namespace metal;
 
-#define TINT_ISOLATE_UB \
-  if (volatile bool tint_volatile_true = true; tint_volatile_true)
+#define TINT_ISOLATE_UB(VOLATILE_NAME) \
+  volatile bool VOLATILE_NAME = true; \
+  if (VOLATILE_NAME)
 
 kernel void test_function() {
-  TINT_ISOLATE_UB while(true) {
+  TINT_ISOLATE_UB(tint_volatile_true) while(true) {
     if (false) {
       break;
     }
diff --git a/src/tint/lang/msl/writer/ast_printer/loop_test.cc b/src/tint/lang/msl/writer/ast_printer/loop_test.cc
index 7aed227..3d1dd64 100644
--- a/src/tint/lang/msl/writer/ast_printer/loop_test.cc
+++ b/src/tint/lang/msl/writer/ast_printer/loop_test.cc
@@ -51,11 +51,12 @@
 
 using namespace metal;
 
-#define TINT_ISOLATE_UB \
-  if (volatile bool tint_volatile_true = true; tint_volatile_true)
+#define TINT_ISOLATE_UB(VOLATILE_NAME) \
+  volatile bool VOLATILE_NAME = true; \
+  if (VOLATILE_NAME)
 
 fragment void F() {
-  TINT_ISOLATE_UB while(true) {
+  TINT_ISOLATE_UB(tint_volatile_true) while(true) {
     break;
   }
   return;
@@ -80,14 +81,15 @@
 
 using namespace metal;
 
-#define TINT_ISOLATE_UB \
-  if (volatile bool tint_volatile_true = true; tint_volatile_true)
+#define TINT_ISOLATE_UB(VOLATILE_NAME) \
+  volatile bool VOLATILE_NAME = true; \
+  if (VOLATILE_NAME)
 
 void a_statement() {
 }
 
 fragment void F() {
-  TINT_ISOLATE_UB while(true) {
+  TINT_ISOLATE_UB(tint_volatile_true) while(true) {
     break;
     {
       a_statement();
@@ -115,14 +117,15 @@
 
 using namespace metal;
 
-#define TINT_ISOLATE_UB \
-  if (volatile bool tint_volatile_true = true; tint_volatile_true)
+#define TINT_ISOLATE_UB(VOLATILE_NAME) \
+  volatile bool VOLATILE_NAME = true; \
+  if (VOLATILE_NAME)
 
 void a_statement() {
 }
 
 fragment void F() {
-  TINT_ISOLATE_UB while(true) {
+  TINT_ISOLATE_UB(tint_volatile_true) while(true) {
     break;
     {
       a_statement();
@@ -156,8 +159,8 @@
     ASTPrinter& gen = Build();
 
     ASSERT_TRUE(gen.EmitStatement(outer)) << gen.Diagnostics();
-    EXPECT_EQ(gen.Result(), R"(TINT_ISOLATE_UB while(true) {
-  TINT_ISOLATE_UB while(true) {
+    EXPECT_EQ(gen.Result(), R"(TINT_ISOLATE_UB(tint_volatile_true) while(true) {
+  TINT_ISOLATE_UB(tint_volatile_true_1) while(true) {
     break;
     {
       a_statement();
@@ -194,7 +197,7 @@
     ASTPrinter& gen = Build();
 
     ASSERT_TRUE(gen.EmitStatement(outer)) << gen.Diagnostics();
-    EXPECT_EQ(gen.Result(), R"(TINT_ISOLATE_UB while(true) {
+    EXPECT_EQ(gen.Result(), R"(TINT_ISOLATE_UB(tint_volatile_true) while(true) {
   float lhs = 2.5f;
   float other = 0.0f;
   break;
@@ -217,7 +220,7 @@
     ASTPrinter& gen = Build();
 
     ASSERT_TRUE(gen.EmitStatement(f)) << gen.Diagnostics();
-    EXPECT_EQ(gen.Result(), R"(TINT_ISOLATE_UB for(; ; ) {
+    EXPECT_EQ(gen.Result(), R"(TINT_ISOLATE_UB(tint_volatile_true) for(; ; ) {
   return;
 }
 )");
@@ -235,7 +238,7 @@
     ASTPrinter& gen = Build();
 
     ASSERT_TRUE(gen.EmitStatement(f)) << gen.Diagnostics();
-    EXPECT_EQ(gen.Result(), R"(TINT_ISOLATE_UB for(int i = 0; ; ) {
+    EXPECT_EQ(gen.Result(), R"(TINT_ISOLATE_UB(tint_volatile_true) for(int i = 0; ; ) {
   return;
 }
 )");
@@ -266,7 +269,7 @@
     f(1);
     f(2);
   }
-  TINT_ISOLATE_UB for(; ; ) {
+  TINT_ISOLATE_UB(tint_volatile_true) for(; ; ) {
     return;
   }
 }
@@ -285,7 +288,7 @@
     ASTPrinter& gen = Build();
 
     ASSERT_TRUE(gen.EmitStatement(f)) << gen.Diagnostics();
-    EXPECT_EQ(gen.Result(), R"(TINT_ISOLATE_UB for(; true; ) {
+    EXPECT_EQ(gen.Result(), R"(TINT_ISOLATE_UB(tint_volatile_true) for(; true; ) {
   return;
 }
 )");
@@ -304,8 +307,9 @@
     ASTPrinter& gen = Build();
 
     ASSERT_TRUE(gen.EmitStatement(f)) << gen.Diagnostics();
-    EXPECT_EQ(gen.Result(),
-              R"(TINT_ISOLATE_UB for(; ; i = as_type<int>((as_type<uint>(i) + as_type<uint>(1)))) {
+    EXPECT_EQ(
+        gen.Result(),
+        R"(TINT_ISOLATE_UB(tint_volatile_true) for(; ; i = as_type<int>((as_type<uint>(i) + as_type<uint>(1)))) {
   return;
 }
 )");
@@ -331,7 +335,7 @@
     ASTPrinter& gen = Build();
 
     ASSERT_TRUE(gen.EmitStatement(loop)) << gen.Diagnostics();
-    EXPECT_EQ(gen.Result(), R"(TINT_ISOLATE_UB while(true) {
+    EXPECT_EQ(gen.Result(), R"(TINT_ISOLATE_UB(tint_volatile_true) while(true) {
   return;
   {
     f(1);
@@ -357,7 +361,7 @@
     ASSERT_TRUE(gen.EmitStatement(f)) << gen.Diagnostics();
     EXPECT_EQ(
         gen.Result(),
-        R"(TINT_ISOLATE_UB for(int i = 0; true; i = as_type<int>((as_type<uint>(i) + as_type<uint>(1)))) {
+        R"(TINT_ISOLATE_UB(tint_volatile_true) for(int i = 0; true; i = as_type<int>((as_type<uint>(i) + as_type<uint>(1)))) {
   a_statement();
 }
 )");
@@ -389,7 +393,7 @@
     f(1);
     f(2);
   }
-  TINT_ISOLATE_UB while(true) {
+  TINT_ISOLATE_UB(tint_volatile_true) while(true) {
     if (!(true)) { break; }
     return;
     {
@@ -412,7 +416,7 @@
     ASTPrinter& gen = Build();
 
     ASSERT_TRUE(gen.EmitStatement(f)) << gen.Diagnostics();
-    EXPECT_EQ(gen.Result(), R"(TINT_ISOLATE_UB while(true) {
+    EXPECT_EQ(gen.Result(), R"(TINT_ISOLATE_UB(tint_volatile_true) while(true) {
   return;
 }
 )");
@@ -429,7 +433,7 @@
     ASTPrinter& gen = Build();
 
     ASSERT_TRUE(gen.EmitStatement(f)) << gen.Diagnostics();
-    EXPECT_EQ(gen.Result(), R"(TINT_ISOLATE_UB while(true) {
+    EXPECT_EQ(gen.Result(), R"(TINT_ISOLATE_UB(tint_volatile_true) while(true) {
   continue;
 }
 )");
@@ -449,7 +453,7 @@
     ASTPrinter& gen = Build();
 
     ASSERT_TRUE(gen.EmitStatement(f)) << gen.Diagnostics();
-    EXPECT_EQ(gen.Result(), R"(TINT_ISOLATE_UB while((t && false)) {
+    EXPECT_EQ(gen.Result(), R"(TINT_ISOLATE_UB(tint_volatile_true) while((t && false)) {
   return;
 }
 )");
diff --git a/src/tint/lang/msl/writer/printer/printer.cc b/src/tint/lang/msl/writer/printer/printer.cc
index e2950c5..0e8988e 100644
--- a/src/tint/lang/msl/writer/printer/printer.cc
+++ b/src/tint/lang/msl/writer/printer/printer.cc
@@ -1263,20 +1263,20 @@
 
             if (auto location = attributes.location) {
                 auto& pipeline_stage_uses = str->PipelineStageUses();
-                if (TINT_UNLIKELY(pipeline_stage_uses.size() != 1)) {
+                if (TINT_UNLIKELY(pipeline_stage_uses.Count() != 1)) {
                     TINT_IR_ICE(ir_) << "invalid entry point IO struct uses";
                     return;
                 }
 
-                if (pipeline_stage_uses.count(core::type::PipelineStageUsage::kVertexInput)) {
+                if (pipeline_stage_uses.Contains(core::type::PipelineStageUsage::kVertexInput)) {
                     out << " [[attribute(" + std::to_string(location.value()) + ")]]";
-                } else if (pipeline_stage_uses.count(
+                } else if (pipeline_stage_uses.Contains(
                                core::type::PipelineStageUsage::kVertexOutput)) {
                     out << " [[user(locn" + std::to_string(location.value()) + ")]]";
-                } else if (pipeline_stage_uses.count(
+                } else if (pipeline_stage_uses.Contains(
                                core::type::PipelineStageUsage::kFragmentInput)) {
                     out << " [[user(locn" + std::to_string(location.value()) + ")]]";
-                } else if (TINT_LIKELY(pipeline_stage_uses.count(
+                } else if (TINT_LIKELY(pipeline_stage_uses.Contains(
                                core::type::PipelineStageUsage::kFragmentOutput))) {
                     out << " [[color(" + std::to_string(location.value()) + ")]]";
                 } else {
diff --git a/src/tint/lang/spirv/validate/validate.cc b/src/tint/lang/spirv/validate/validate.cc
index 8d85413..c692a53 100644
--- a/src/tint/lang/spirv/validate/validate.cc
+++ b/src/tint/lang/spirv/validate/validate.cc
@@ -44,8 +44,8 @@
         [&](spv_message_level_t level, const char*, const spv_position_t& pos, const char* msg) {
             diag::Diagnostic diag;
             diag.message = msg;
-            diag.source.range.begin.line = pos.line + 1;
-            diag.source.range.begin.column = pos.column + 1;
+            diag.source.range.begin.line = static_cast<uint32_t>(pos.line) + 1;
+            diag.source.range.begin.column = static_cast<uint32_t>(pos.column) + 1;
             diag.source.range.end = diag.source.range.begin;
             switch (level) {
                 case SPV_MSG_FATAL:
diff --git a/src/tint/lang/spirv/writer/ast_printer/ast_printer.cc b/src/tint/lang/spirv/writer/ast_printer/ast_printer.cc
index 5e5055f..346f46d 100644
--- a/src/tint/lang/spirv/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/spirv/writer/ast_printer/ast_printer.cc
@@ -29,7 +29,6 @@
 
 #include <unordered_map>
 
-#include "src/tint/lang/spirv/writer/ast_raise/clamp_frag_depth.h"
 #include "src/tint/lang/spirv/writer/ast_raise/for_loop_to_loop.h"
 #include "src/tint/lang/spirv/writer/ast_raise/merge_return.h"
 #include "src/tint/lang/spirv/writer/ast_raise/var_for_dynamic_index.h"
@@ -41,6 +40,7 @@
 #include "src/tint/lang/wgsl/ast/transform/binding_remapper.h"
 #include "src/tint/lang/wgsl/ast/transform/builtin_polyfill.h"
 #include "src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.h"
+#include "src/tint/lang/wgsl/ast/transform/clamp_frag_depth.h"
 #include "src/tint/lang/wgsl/ast/transform/demote_to_helper.h"
 #include "src/tint/lang/wgsl/ast/transform/direct_variable_access.h"
 #include "src/tint/lang/wgsl/ast/transform/disable_uniformity_analysis.h"
@@ -65,7 +65,7 @@
     ast::transform::DataMap data;
 
     if (options.clamp_frag_depth) {
-        manager.Add<ClampFragDepth>();
+        manager.Add<ast::transform::ClampFragDepth>();
     }
 
     manager.Add<ast::transform::DisableUniformityAnalysis>();
diff --git a/src/tint/lang/spirv/writer/ast_printer/builder.cc b/src/tint/lang/spirv/writer/ast_printer/builder.cc
index 1895d11..143a7ab 100644
--- a/src/tint/lang/spirv/writer/ast_printer/builder.cc
+++ b/src/tint/lang/spirv/writer/ast_printer/builder.cc
@@ -801,7 +801,7 @@
                                    Operand(sem->Attributes().location.value())});
                 return true;
             },
-            [&](const ast::IndexAttribute*) {
+            [&](const ast::BlendSrcAttribute*) {
                 module_.PushAnnot(spv::Op::OpDecorate,
                                   {Operand(var_id), U32Operand(SpvDecorationIndex),
                                    Operand(sem->Attributes().index.value())});
diff --git a/src/tint/lang/spirv/writer/ast_raise/BUILD.bazel b/src/tint/lang/spirv/writer/ast_raise/BUILD.bazel
index e13d946..275f6e5 100644
--- a/src/tint/lang/spirv/writer/ast_raise/BUILD.bazel
+++ b/src/tint/lang/spirv/writer/ast_raise/BUILD.bazel
@@ -39,7 +39,6 @@
 cc_library(
   name = "ast_raise",
   srcs = [
-    "clamp_frag_depth.cc",
     "for_loop_to_loop.cc",
     "merge_return.cc",
     "var_for_dynamic_index.cc",
@@ -47,7 +46,6 @@
     "while_to_loop.cc",
   ],
   hdrs = [
-    "clamp_frag_depth.h",
     "for_loop_to_loop.h",
     "merge_return.h",
     "var_for_dynamic_index.h",
@@ -88,7 +86,6 @@
   name = "test",
   alwayslink = True,
   srcs = [
-    "clamp_frag_depth_test.cc",
     "for_loop_to_loop_test.cc",
     "merge_return_test.cc",
     "var_for_dynamic_index_test.cc",
diff --git a/src/tint/lang/spirv/writer/ast_raise/BUILD.cmake b/src/tint/lang/spirv/writer/ast_raise/BUILD.cmake
index 9cd1d16..409ca99 100644
--- a/src/tint/lang/spirv/writer/ast_raise/BUILD.cmake
+++ b/src/tint/lang/spirv/writer/ast_raise/BUILD.cmake
@@ -41,8 +41,6 @@
 # Condition: TINT_BUILD_SPV_WRITER
 ################################################################################
 tint_add_target(tint_lang_spirv_writer_ast_raise lib
-  lang/spirv/writer/ast_raise/clamp_frag_depth.cc
-  lang/spirv/writer/ast_raise/clamp_frag_depth.h
   lang/spirv/writer/ast_raise/for_loop_to_loop.cc
   lang/spirv/writer/ast_raise/for_loop_to_loop.h
   lang/spirv/writer/ast_raise/merge_return.cc
@@ -91,7 +89,6 @@
 # Condition: TINT_BUILD_SPV_WRITER AND TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER
 ################################################################################
 tint_add_target(tint_lang_spirv_writer_ast_raise_test test
-  lang/spirv/writer/ast_raise/clamp_frag_depth_test.cc
   lang/spirv/writer/ast_raise/for_loop_to_loop_test.cc
   lang/spirv/writer/ast_raise/merge_return_test.cc
   lang/spirv/writer/ast_raise/var_for_dynamic_index_test.cc
diff --git a/src/tint/lang/spirv/writer/ast_raise/BUILD.gn b/src/tint/lang/spirv/writer/ast_raise/BUILD.gn
index b18ddb5..f24dbdd 100644
--- a/src/tint/lang/spirv/writer/ast_raise/BUILD.gn
+++ b/src/tint/lang/spirv/writer/ast_raise/BUILD.gn
@@ -44,8 +44,6 @@
 if (tint_build_spv_writer) {
   libtint_source_set("ast_raise") {
     sources = [
-      "clamp_frag_depth.cc",
-      "clamp_frag_depth.h",
       "for_loop_to_loop.cc",
       "for_loop_to_loop.h",
       "merge_return.cc",
@@ -91,7 +89,6 @@
       tint_build_wgsl_writer) {
     tint_unittests_source_set("unittests") {
       sources = [
-        "clamp_frag_depth_test.cc",
         "for_loop_to_loop_test.cc",
         "merge_return_test.cc",
         "var_for_dynamic_index_test.cc",
diff --git a/src/tint/lang/spirv/writer/printer/printer.cc b/src/tint/lang/spirv/writer/printer/printer.cc
index 8eec980..cc4e35c 100644
--- a/src/tint/lang/spirv/writer/printer/printer.cc
+++ b/src/tint/lang/spirv/writer/printer/printer.cc
@@ -2008,9 +2008,9 @@
             module_.PushAnnot(spv::Op::OpDecorate,
                               {id, U32Operand(SpvDecorationLocation), *attrs.location});
         }
-        if (attrs.index) {
+        if (attrs.blend_src) {
             module_.PushAnnot(spv::Op::OpDecorate,
-                              {id, U32Operand(SpvDecorationIndex), *attrs.index});
+                              {id, U32Operand(SpvDecorationIndex), *attrs.blend_src});
         }
         if (attrs.interpolation) {
             switch (attrs.interpolation->type) {
diff --git a/src/tint/lang/spirv/writer/raise/shader_io.cc b/src/tint/lang/spirv/writer/raise/shader_io.cc
index b4a8be9..8266f23 100644
--- a/src/tint/lang/spirv/writer/raise/shader_io.cc
+++ b/src/tint/lang/spirv/writer/raise/shader_io.cc
@@ -106,8 +106,8 @@
             }
             if (io.attributes.location) {
                 name << "_loc" << io.attributes.location.value();
-                if (io.attributes.index.has_value()) {
-                    name << "_idx" << io.attributes.index.value();
+                if (io.attributes.blend_src.has_value()) {
+                    name << "_idx" << io.attributes.blend_src.value();
                 }
             }
             name << name_suffix;
@@ -117,7 +117,7 @@
             auto* var = b.Var(name.str(), ptr);
             var->SetAttributes(core::ir::IOAttributes{
                 io.attributes.location,
-                io.attributes.index,
+                io.attributes.blend_src,
                 io.attributes.builtin,
                 io.attributes.interpolation,
                 io.attributes.invariant,
diff --git a/src/tint/lang/spirv/writer/raise/shader_io_test.cc b/src/tint/lang/spirv/writer/raise/shader_io_test.cc
index a2c5feb..5738a20 100644
--- a/src/tint/lang/spirv/writer/raise/shader_io_test.cc
+++ b/src/tint/lang/spirv/writer/raise/shader_io_test.cc
@@ -679,8 +679,8 @@
 }
 
 %b1 = block {  # root
-  %foo_loc0_idx0_Output:ptr<__out, f32, write> = var @location(0) @index(0)
-  %foo_loc0_idx1_Output:ptr<__out, f32, write> = var @location(0) @index(1)
+  %foo_loc0_idx0_Output:ptr<__out, f32, write> = var @location(0) @blend_src(0)
+  %foo_loc0_idx1_Output:ptr<__out, f32, write> = var @location(0) @blend_src(1)
 }
 
 %foo_inner = func():Output -> %b2 {
diff --git a/src/tint/lang/wgsl/ast/BUILD.bazel b/src/tint/lang/wgsl/ast/BUILD.bazel
index 6eca7ad..a30fcff 100644
--- a/src/tint/lang/wgsl/ast/BUILD.bazel
+++ b/src/tint/lang/wgsl/ast/BUILD.bazel
@@ -46,6 +46,7 @@
     "binary_expression.cc",
     "binding_attribute.cc",
     "bitcast_expression.cc",
+    "blend_src_attribute.cc",
     "block_statement.cc",
     "bool_literal_expression.cc",
     "break_if_statement.cc",
@@ -81,7 +82,6 @@
     "if_statement.cc",
     "increment_decrement_statement.cc",
     "index_accessor_expression.cc",
-    "index_attribute.cc",
     "int_literal_expression.cc",
     "internal_attribute.cc",
     "interpolate_attribute.cc",
@@ -127,6 +127,7 @@
     "binary_expression.h",
     "binding_attribute.h",
     "bitcast_expression.h",
+    "blend_src_attribute.h",
     "block_statement.h",
     "bool_literal_expression.h",
     "break_if_statement.h",
@@ -162,7 +163,6 @@
     "if_statement.h",
     "increment_decrement_statement.h",
     "index_accessor_expression.h",
-    "index_attribute.h",
     "int_literal_expression.h",
     "internal_attribute.h",
     "interpolate_attribute.h",
@@ -235,6 +235,7 @@
     "binary_expression_test.cc",
     "binding_attribute_test.cc",
     "bitcast_expression_test.cc",
+    "blend_src_attribute_test.cc",
     "block_statement_test.cc",
     "bool_literal_expression_test.cc",
     "break_if_statement_test.cc",
@@ -269,7 +270,6 @@
     "if_statement_test.cc",
     "increment_decrement_statement_test.cc",
     "index_accessor_expression_test.cc",
-    "index_attribute_test.cc",
     "int_literal_expression_test.cc",
     "interpolate_attribute_test.cc",
     "location_attribute_test.cc",
diff --git a/src/tint/lang/wgsl/ast/BUILD.cmake b/src/tint/lang/wgsl/ast/BUILD.cmake
index 767496f..fd20e97 100644
--- a/src/tint/lang/wgsl/ast/BUILD.cmake
+++ b/src/tint/lang/wgsl/ast/BUILD.cmake
@@ -55,6 +55,8 @@
   lang/wgsl/ast/binding_attribute.h
   lang/wgsl/ast/bitcast_expression.cc
   lang/wgsl/ast/bitcast_expression.h
+  lang/wgsl/ast/blend_src_attribute.cc
+  lang/wgsl/ast/blend_src_attribute.h
   lang/wgsl/ast/block_statement.cc
   lang/wgsl/ast/block_statement.h
   lang/wgsl/ast/bool_literal_expression.cc
@@ -125,8 +127,6 @@
   lang/wgsl/ast/increment_decrement_statement.h
   lang/wgsl/ast/index_accessor_expression.cc
   lang/wgsl/ast/index_accessor_expression.h
-  lang/wgsl/ast/index_attribute.cc
-  lang/wgsl/ast/index_attribute.h
   lang/wgsl/ast/int_literal_expression.cc
   lang/wgsl/ast/int_literal_expression.h
   lang/wgsl/ast/internal_attribute.cc
@@ -235,6 +235,7 @@
   lang/wgsl/ast/binary_expression_test.cc
   lang/wgsl/ast/binding_attribute_test.cc
   lang/wgsl/ast/bitcast_expression_test.cc
+  lang/wgsl/ast/blend_src_attribute_test.cc
   lang/wgsl/ast/block_statement_test.cc
   lang/wgsl/ast/bool_literal_expression_test.cc
   lang/wgsl/ast/break_if_statement_test.cc
@@ -269,7 +270,6 @@
   lang/wgsl/ast/if_statement_test.cc
   lang/wgsl/ast/increment_decrement_statement_test.cc
   lang/wgsl/ast/index_accessor_expression_test.cc
-  lang/wgsl/ast/index_attribute_test.cc
   lang/wgsl/ast/int_literal_expression_test.cc
   lang/wgsl/ast/interpolate_attribute_test.cc
   lang/wgsl/ast/location_attribute_test.cc
diff --git a/src/tint/lang/wgsl/ast/BUILD.gn b/src/tint/lang/wgsl/ast/BUILD.gn
index f42bd86..b46a056 100644
--- a/src/tint/lang/wgsl/ast/BUILD.gn
+++ b/src/tint/lang/wgsl/ast/BUILD.gn
@@ -58,6 +58,8 @@
     "binding_attribute.h",
     "bitcast_expression.cc",
     "bitcast_expression.h",
+    "blend_src_attribute.cc",
+    "blend_src_attribute.h",
     "block_statement.cc",
     "block_statement.h",
     "bool_literal_expression.cc",
@@ -128,8 +130,6 @@
     "increment_decrement_statement.h",
     "index_accessor_expression.cc",
     "index_accessor_expression.h",
-    "index_attribute.cc",
-    "index_attribute.h",
     "int_literal_expression.cc",
     "int_literal_expression.h",
     "internal_attribute.cc",
@@ -235,6 +235,7 @@
       "binary_expression_test.cc",
       "binding_attribute_test.cc",
       "bitcast_expression_test.cc",
+      "blend_src_attribute_test.cc",
       "block_statement_test.cc",
       "bool_literal_expression_test.cc",
       "break_if_statement_test.cc",
@@ -269,7 +270,6 @@
       "if_statement_test.cc",
       "increment_decrement_statement_test.cc",
       "index_accessor_expression_test.cc",
-      "index_attribute_test.cc",
       "int_literal_expression_test.cc",
       "interpolate_attribute_test.cc",
       "location_attribute_test.cc",
diff --git a/src/tint/lang/wgsl/ast/index_attribute.cc b/src/tint/lang/wgsl/ast/blend_src_attribute.cc
similarity index 82%
rename from src/tint/lang/wgsl/ast/index_attribute.cc
rename to src/tint/lang/wgsl/ast/blend_src_attribute.cc
index 6650066..089ce1e 100644
--- a/src/tint/lang/wgsl/ast/index_attribute.cc
+++ b/src/tint/lang/wgsl/ast/blend_src_attribute.cc
@@ -25,34 +25,34 @@
 // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-#include "src/tint/lang/wgsl/ast/index_attribute.h"
+#include "src/tint/lang/wgsl/ast/blend_src_attribute.h"
 
 #include <string>
 
 #include "src/tint/lang/wgsl/ast/builder.h"
 #include "src/tint/lang/wgsl/ast/clone_context.h"
 
-TINT_INSTANTIATE_TYPEINFO(tint::ast::IndexAttribute);
+TINT_INSTANTIATE_TYPEINFO(tint::ast::BlendSrcAttribute);
 
 namespace tint::ast {
 
-IndexAttribute::IndexAttribute(GenerationID pid,
+BlendSrcAttribute::BlendSrcAttribute(GenerationID pid,
                                NodeID nid,
                                const Source& src,
                                const Expression* exp)
     : Base(pid, nid, src), expr(exp) {}
 
-IndexAttribute::~IndexAttribute() = default;
+BlendSrcAttribute::~BlendSrcAttribute() = default;
 
-std::string IndexAttribute::Name() const {
-    return "index";
+std::string BlendSrcAttribute::Name() const {
+    return "blend_src";
 }
 
-const IndexAttribute* IndexAttribute::Clone(CloneContext& ctx) const {
+const BlendSrcAttribute* BlendSrcAttribute::Clone(CloneContext& ctx) const {
     // Clone arguments outside of create() call to have deterministic ordering
     auto src = ctx.Clone(source);
     auto* expr_ = ctx.Clone(expr);
-    return ctx.dst->create<IndexAttribute>(src, expr_);
+    return ctx.dst->create<BlendSrcAttribute>(src, expr_);
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/index_attribute.h b/src/tint/lang/wgsl/ast/blend_src_attribute.h
similarity index 79%
rename from src/tint/lang/wgsl/ast/index_attribute.h
rename to src/tint/lang/wgsl/ast/blend_src_attribute.h
index 215ffe9..9133182 100644
--- a/src/tint/lang/wgsl/ast/index_attribute.h
+++ b/src/tint/lang/wgsl/ast/blend_src_attribute.h
@@ -25,8 +25,8 @@
 // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-#ifndef SRC_TINT_LANG_WGSL_AST_INDEX_ATTRIBUTE_H_
-#define SRC_TINT_LANG_WGSL_AST_INDEX_ATTRIBUTE_H_
+#ifndef SRC_TINT_LANG_WGSL_AST_BLEND_SRC_ATTRIBUTE_H_
+#define SRC_TINT_LANG_WGSL_AST_BLEND_SRC_ATTRIBUTE_H_
 
 #include <string>
 
@@ -35,16 +35,16 @@
 
 namespace tint::ast {
 
-/// An id attribute for pipeline-overridable constants
-class IndexAttribute final : public Castable<IndexAttribute, Attribute> {
+/// An blend_src attribute for shader IO.
+class BlendSrcAttribute final : public Castable<BlendSrcAttribute, Attribute> {
   public:
-    /// Create an index attribute.
+    /// Create a blend_src ttribute.
     /// @param pid the identifier of the program that owns this node
     /// @param nid the unique node identifier
     /// @param src the source of this node
     /// @param expr the numeric id expression
-    IndexAttribute(GenerationID pid, NodeID nid, const Source& src, const Expression* expr);
-    ~IndexAttribute() override;
+    BlendSrcAttribute(GenerationID pid, NodeID nid, const Source& src, const Expression* expr);
+    ~BlendSrcAttribute() override;
 
     /// @returns the WGSL name for the attribute
     std::string Name() const override;
@@ -53,12 +53,12 @@
     /// `ctx`.
     /// @param ctx the clone context
     /// @return the newly cloned node
-    const IndexAttribute* Clone(CloneContext& ctx) const override;
+    const BlendSrcAttribute* Clone(CloneContext& ctx) const override;
 
-    /// The id expression
+    /// The blend_src expression
     const Expression* const expr;
 };
 
 }  // namespace tint::ast
 
-#endif  // SRC_TINT_LANG_WGSL_AST_INDEX_ATTRIBUTE_H_
+#endif  // SRC_TINT_LANG_WGSL_AST_BLEND_SRC_ATTRIBUTE_H_
diff --git a/src/tint/lang/wgsl/ast/index_attribute_test.cc b/src/tint/lang/wgsl/ast/blend_src_attribute_test.cc
similarity index 91%
rename from src/tint/lang/wgsl/ast/index_attribute_test.cc
rename to src/tint/lang/wgsl/ast/blend_src_attribute_test.cc
index 38cd550..f954dc0 100644
--- a/src/tint/lang/wgsl/ast/index_attribute_test.cc
+++ b/src/tint/lang/wgsl/ast/blend_src_attribute_test.cc
@@ -25,7 +25,7 @@
 // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-#include "src/tint/lang/wgsl/ast/id_attribute.h"
+#include "src/tint/lang/wgsl/ast/blend_src_attribute.h"
 
 #include "src/tint/lang/wgsl/ast/helper_test.h"
 
@@ -33,10 +33,10 @@
 namespace {
 
 using namespace tint::core::number_suffixes;  // NOLINT
-using IndexAttributeTest = TestHelper;
+using BlendSrcAttributeTest = TestHelper;
 
-TEST_F(IndexAttributeTest, Creation) {
-    auto* d = Index(1_a);
+TEST_F(BlendSrcAttributeTest, Creation) {
+    auto* d = BlendSrc(1_a);
     EXPECT_TRUE(d->expr->Is<IntLiteralExpression>());
 }
 
diff --git a/src/tint/lang/wgsl/ast/builder.h b/src/tint/lang/wgsl/ast/builder.h
index 7bdaf1e..b037caa 100644
--- a/src/tint/lang/wgsl/ast/builder.h
+++ b/src/tint/lang/wgsl/ast/builder.h
@@ -61,6 +61,7 @@
 #include "src/tint/lang/wgsl/ast/binary_expression.h"
 #include "src/tint/lang/wgsl/ast/binding_attribute.h"
 #include "src/tint/lang/wgsl/ast/bitcast_expression.h"
+#include "src/tint/lang/wgsl/ast/blend_src_attribute.h"
 #include "src/tint/lang/wgsl/ast/bool_literal_expression.h"
 #include "src/tint/lang/wgsl/ast/break_if_statement.h"
 #include "src/tint/lang/wgsl/ast/break_statement.h"
@@ -86,7 +87,6 @@
 #include "src/tint/lang/wgsl/ast/if_statement.h"
 #include "src/tint/lang/wgsl/ast/increment_decrement_statement.h"
 #include "src/tint/lang/wgsl/ast/index_accessor_expression.h"
-#include "src/tint/lang/wgsl/ast/index_attribute.h"
 #include "src/tint/lang/wgsl/ast/int_literal_expression.h"
 #include "src/tint/lang/wgsl/ast/interpolate_attribute.h"
 #include "src/tint/lang/wgsl/ast/invariant_attribute.h"
@@ -3193,21 +3193,21 @@
         return create<ast::LocationAttribute>(source_, Expr(std::forward<EXPR>(location)));
     }
 
-    /// Creates an ast::IndexAttribute
+    /// Creates an ast::BlendSrcAttribute
     /// @param source the source information
-    /// @param index the index value expression
-    /// @returns the index attribute pointer
+    /// @param blend_src the blend_src value expression
+    /// @returns the blend_src attribute pointer
     template <typename EXPR>
-    const ast::IndexAttribute* Index(const Source& source, EXPR&& index) {
-        return create<ast::IndexAttribute>(source, Expr(std::forward<EXPR>(index)));
+    const ast::BlendSrcAttribute* BlendSrc(const Source& source, EXPR&& blend_src) {
+        return create<ast::BlendSrcAttribute>(source, Expr(std::forward<EXPR>(blend_src)));
     }
 
-    /// Creates an ast::IndexAttribute
-    /// @param index the index value expression
-    /// @returns the index attribute pointer
+    /// Creates an ast::BlendSrcAttribute
+    /// @param blend_src the blend_src value expression
+    /// @returns the blend_src attribute pointer
     template <typename EXPR>
-    const ast::IndexAttribute* Index(EXPR&& index) {
-        return create<ast::IndexAttribute>(source_, Expr(std::forward<EXPR>(index)));
+    const ast::BlendSrcAttribute* BlendSrc(EXPR&& blend_src) {
+        return create<ast::BlendSrcAttribute>(source_, Expr(std::forward<EXPR>(blend_src)));
     }
 
     /// Creates an ast::IdAttribute
diff --git a/src/tint/lang/wgsl/ast/node_id.h b/src/tint/lang/wgsl/ast/node_id.h
index 556f6fe..36096c3 100644
--- a/src/tint/lang/wgsl/ast/node_id.h
+++ b/src/tint/lang/wgsl/ast/node_id.h
@@ -29,6 +29,7 @@
 #define SRC_TINT_LANG_WGSL_AST_NODE_ID_H_
 
 #include <stddef.h>
+#include <stdint.h>
 
 namespace tint::ast {
 
@@ -46,7 +47,7 @@
     bool operator<(NodeID other) const { return value < other.value; }
 
     /// The numerical value for the node identifier
-    size_t value = 0;
+    uint32_t value = 0;
 };
 
 }  // namespace tint::ast
diff --git a/src/tint/lang/wgsl/ast/transform/BUILD.bazel b/src/tint/lang/wgsl/ast/transform/BUILD.bazel
index cb9c9b1..6229a94 100644
--- a/src/tint/lang/wgsl/ast/transform/BUILD.bazel
+++ b/src/tint/lang/wgsl/ast/transform/BUILD.bazel
@@ -45,6 +45,7 @@
     "binding_remapper.cc",
     "builtin_polyfill.cc",
     "canonicalize_entry_point_io.cc",
+    "clamp_frag_depth.cc",
     "data.cc",
     "demote_to_helper.cc",
     "direct_variable_access.cc",
@@ -55,6 +56,7 @@
     "hoist_to_decl_before.cc",
     "manager.cc",
     "multiplanar_external_texture.cc",
+    "offset_first_index.cc",
     "preserve_padding.cc",
     "promote_initializers_to_let.cc",
     "promote_side_effects_to_decl.cc",
@@ -79,6 +81,7 @@
     "binding_remapper.h",
     "builtin_polyfill.h",
     "canonicalize_entry_point_io.h",
+    "clamp_frag_depth.h",
     "data.h",
     "demote_to_helper.h",
     "direct_variable_access.h",
@@ -89,6 +92,7 @@
     "hoist_to_decl_before.h",
     "manager.h",
     "multiplanar_external_texture.h",
+    "offset_first_index.h",
     "preserve_padding.h",
     "promote_initializers_to_let.h",
     "promote_side_effects_to_decl.h",
@@ -146,6 +150,7 @@
     "binding_remapper_test.cc",
     "builtin_polyfill_test.cc",
     "canonicalize_entry_point_io_test.cc",
+    "clamp_frag_depth_test.cc",
     "demote_to_helper_test.cc",
     "direct_variable_access_test.cc",
     "disable_uniformity_analysis_test.cc",
@@ -156,6 +161,7 @@
     "hoist_to_decl_before_test.cc",
     "manager_test.cc",
     "multiplanar_external_texture_test.cc",
+    "offset_first_index_test.cc",
     "preserve_padding_test.cc",
     "promote_initializers_to_let_test.cc",
     "promote_side_effects_to_decl_test.cc",
diff --git a/src/tint/lang/wgsl/ast/transform/BUILD.cmake b/src/tint/lang/wgsl/ast/transform/BUILD.cmake
index c432f09..068369c 100644
--- a/src/tint/lang/wgsl/ast/transform/BUILD.cmake
+++ b/src/tint/lang/wgsl/ast/transform/BUILD.cmake
@@ -51,6 +51,8 @@
   lang/wgsl/ast/transform/builtin_polyfill.h
   lang/wgsl/ast/transform/canonicalize_entry_point_io.cc
   lang/wgsl/ast/transform/canonicalize_entry_point_io.h
+  lang/wgsl/ast/transform/clamp_frag_depth.cc
+  lang/wgsl/ast/transform/clamp_frag_depth.h
   lang/wgsl/ast/transform/data.cc
   lang/wgsl/ast/transform/data.h
   lang/wgsl/ast/transform/demote_to_helper.cc
@@ -71,6 +73,8 @@
   lang/wgsl/ast/transform/manager.h
   lang/wgsl/ast/transform/multiplanar_external_texture.cc
   lang/wgsl/ast/transform/multiplanar_external_texture.h
+  lang/wgsl/ast/transform/offset_first_index.cc
+  lang/wgsl/ast/transform/offset_first_index.h
   lang/wgsl/ast/transform/preserve_padding.cc
   lang/wgsl/ast/transform/preserve_padding.h
   lang/wgsl/ast/transform/promote_initializers_to_let.cc
@@ -146,6 +150,7 @@
   lang/wgsl/ast/transform/binding_remapper_test.cc
   lang/wgsl/ast/transform/builtin_polyfill_test.cc
   lang/wgsl/ast/transform/canonicalize_entry_point_io_test.cc
+  lang/wgsl/ast/transform/clamp_frag_depth_test.cc
   lang/wgsl/ast/transform/demote_to_helper_test.cc
   lang/wgsl/ast/transform/direct_variable_access_test.cc
   lang/wgsl/ast/transform/disable_uniformity_analysis_test.cc
@@ -156,6 +161,7 @@
   lang/wgsl/ast/transform/hoist_to_decl_before_test.cc
   lang/wgsl/ast/transform/manager_test.cc
   lang/wgsl/ast/transform/multiplanar_external_texture_test.cc
+  lang/wgsl/ast/transform/offset_first_index_test.cc
   lang/wgsl/ast/transform/preserve_padding_test.cc
   lang/wgsl/ast/transform/promote_initializers_to_let_test.cc
   lang/wgsl/ast/transform/promote_side_effects_to_decl_test.cc
diff --git a/src/tint/lang/wgsl/ast/transform/BUILD.gn b/src/tint/lang/wgsl/ast/transform/BUILD.gn
index 1214968..b94227e 100644
--- a/src/tint/lang/wgsl/ast/transform/BUILD.gn
+++ b/src/tint/lang/wgsl/ast/transform/BUILD.gn
@@ -56,6 +56,8 @@
     "builtin_polyfill.h",
     "canonicalize_entry_point_io.cc",
     "canonicalize_entry_point_io.h",
+    "clamp_frag_depth.cc",
+    "clamp_frag_depth.h",
     "data.cc",
     "data.h",
     "demote_to_helper.cc",
@@ -76,6 +78,8 @@
     "manager.h",
     "multiplanar_external_texture.cc",
     "multiplanar_external_texture.h",
+    "offset_first_index.cc",
+    "offset_first_index.h",
     "preserve_padding.cc",
     "preserve_padding.h",
     "promote_initializers_to_let.cc",
@@ -147,6 +151,7 @@
         "binding_remapper_test.cc",
         "builtin_polyfill_test.cc",
         "canonicalize_entry_point_io_test.cc",
+        "clamp_frag_depth_test.cc",
         "demote_to_helper_test.cc",
         "direct_variable_access_test.cc",
         "disable_uniformity_analysis_test.cc",
@@ -157,6 +162,7 @@
         "hoist_to_decl_before_test.cc",
         "manager_test.cc",
         "multiplanar_external_texture_test.cc",
+        "offset_first_index_test.cc",
         "preserve_padding_test.cc",
         "promote_initializers_to_let_test.cc",
         "promote_side_effects_to_decl_test.cc",
diff --git a/src/tint/lang/wgsl/ast/transform/add_block_attribute.cc b/src/tint/lang/wgsl/ast/transform/add_block_attribute.cc
index 607bf54..82e0716 100644
--- a/src/tint/lang/wgsl/ast/transform/add_block_attribute.cc
+++ b/src/tint/lang/wgsl/ast/transform/add_block_attribute.cc
@@ -39,7 +39,6 @@
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::AddBlockAttribute);
 TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::AddBlockAttribute::BlockAttribute);
-TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::AddBlockAttribute::Config);
 
 namespace tint::ast::transform {
 
@@ -48,13 +47,12 @@
 AddBlockAttribute::~AddBlockAttribute() = default;
 
 Transform::ApplyResult AddBlockAttribute::Apply(const Program& src,
-                                                const DataMap& inputs,
+                                                const DataMap&,
                                                 DataMap&) const {
     ProgramBuilder b;
     program::CloneContext ctx{&b, &src, /* auto_clone_symbols */ true};
 
     auto& sem = src.Sem();
-    auto* cfg = inputs.Get<Config>();
 
     // A map from a type in the source program to a block-decorated wrapper that contains it in the
     // destination program.
@@ -82,11 +80,6 @@
         bool needs_wrapping = !str ||                    // Type is not a structure
                               str->HasFixedFootprint();  // Struct has a fixed footprint
 
-        if (cfg && cfg->skip_push_constants &&
-            var->AddressSpace() == core::AddressSpace::kPushConstant) {
-            continue;
-        }
-
         if (needs_wrapping) {
             const char* kMemberName = "inner";
 
@@ -136,8 +129,4 @@
                                                                          ctx.dst->AllocateNodeID());
 }
 
-AddBlockAttribute::Config::Config(bool skip_push_consts) : skip_push_constants(skip_push_consts) {}
-
-AddBlockAttribute::Config::~Config() = default;
-
 }  // namespace tint::ast::transform
diff --git a/src/tint/lang/wgsl/ast/transform/add_block_attribute.h b/src/tint/lang/wgsl/ast/transform/add_block_attribute.h
index 61a884d..544e1ee 100644
--- a/src/tint/lang/wgsl/ast/transform/add_block_attribute.h
+++ b/src/tint/lang/wgsl/ast/transform/add_block_attribute.h
@@ -66,19 +66,6 @@
     /// Destructor
     ~AddBlockAttribute() override;
 
-    /// Transform configuration options
-    struct Config final : public Castable<Config, ast::transform::Data> {
-        /// Constructor
-        /// @param skip_push_const whether to skip push constants
-        explicit Config(bool skip_push_const);
-
-        /// Destructor
-        ~Config() override;
-
-        /// Whether to skip push constants
-        bool skip_push_constants;
-    };
-
     /// @copydoc Transform::Apply
     ApplyResult Apply(const Program& program,
                       const DataMap& inputs,
diff --git a/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.cc b/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.cc
index 9bfa463..b056a6a 100644
--- a/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.cc
+++ b/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.cc
@@ -109,7 +109,7 @@
 // Returns true if `attr` is a shader IO attribute.
 bool IsShaderIOAttribute(const Attribute* attr) {
     return attr->IsAnyOf<BuiltinAttribute, InterpolateAttribute, InvariantAttribute,
-                         LocationAttribute, ColorAttribute, IndexAttribute>();
+                         LocationAttribute, ColorAttribute, BlendSrcAttribute>();
 }
 
 }  // namespace
@@ -128,8 +128,8 @@
         const Expression* value;
         /// The output location.
         std::optional<uint32_t> location;
-        /// The output index.
-        std::optional<uint32_t> index;
+        /// The output blend_src.
+        std::optional<uint32_t> blend_src;
     };
 
     /// The clone context.
@@ -374,13 +374,13 @@
     /// @param name the name of the shader output
     /// @param type the type of the shader output
     /// @param location the location if provided
-    /// @param index the index if provided
+    /// @param blend_src the blend_src if provided
     /// @param attrs the attributes to apply to the shader output
     /// @param value the value of the shader output
     void AddOutput(std::string name,
                    const core::type::Type* type,
                    std::optional<uint32_t> location,
-                   std::optional<uint32_t> index,
+                   std::optional<uint32_t> blend_src,
                    tint::Vector<const Attribute*, 8> attrs,
                    const Expression* value) {
         auto builtin_attr = BuiltinOf(attrs);
@@ -412,7 +412,7 @@
         output.attributes = std::move(attrs);
         output.value = value;
         output.location = location;
-        output.index = index;
+        output.blend_src = blend_src;
         wrapper_output_values.Push(output);
     }
 
@@ -510,7 +510,7 @@
 
                 // Extract the original structure member.
                 AddOutput(name, member->Type(), member->Attributes().location,
-                          member->Attributes().index, std::move(attributes),
+                          member->Attributes().blend_src, std::move(attributes),
                           b.MemberAccessor(original_result, name));
             }
         } else if (!inner_ret_type->Is<core::type::Void>()) {
@@ -658,7 +658,7 @@
             wrapper_struct_output_members.Push({
                 /* member */ b.Member(name, outval.type, std::move(outval.attributes)),
                 /* location */ outval.location,
-                /* index */ outval.index,
+                /* blend_src */ outval.blend_src,
                 /* color */ std::nullopt,
             });
             assignments.Push(b.Assign(b.MemberAccessor(wrapper_result, name), outval.value));
diff --git a/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io_test.cc b/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io_test.cc
index 468bb82..7c33b29 100644
--- a/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io_test.cc
+++ b/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io_test.cc
@@ -4134,8 +4134,8 @@
 enable chromium_internal_dual_source_blending;
 
 struct FragOutput {
-  @location(0) @index(0) color : vec4<f32>,
-  @location(0) @index(1) blend : vec4<f32>,
+  @location(0) @blend_src(0) color : vec4<f32>,
+  @location(0) @blend_src(1) blend : vec4<f32>,
   @builtin(frag_depth) depth : f32,
   @builtin(sample_mask) mask : u32,
 };
@@ -4154,9 +4154,9 @@
     auto* expect = R"(
 enable chromium_internal_dual_source_blending;
 
-@location(0) @index(0) @internal(disable_validation__ignore_address_space) var<__out> color_1 : vec4<f32>;
+@location(0) @blend_src(0) @internal(disable_validation__ignore_address_space) var<__out> color_1 : vec4<f32>;
 
-@location(0) @index(1) @internal(disable_validation__ignore_address_space) var<__out> blend_1 : vec4<f32>;
+@location(0) @blend_src(1) @internal(disable_validation__ignore_address_space) var<__out> blend_1 : vec4<f32>;
 
 @builtin(frag_depth) @internal(disable_validation__ignore_address_space) var<__out> depth_1 : f32;
 
@@ -4200,8 +4200,8 @@
 enable chromium_internal_dual_source_blending;
 
 struct FragOutput {
-  @location(0) @index(0) color : vec4<f32>,
-  @location(0) @index(1) blend : vec4<f32>,
+  @location(0) @blend_src(0) color : vec4<f32>,
+  @location(0) @blend_src(1) blend : vec4<f32>,
   @builtin(frag_depth) depth : f32,
   @builtin(sample_mask) mask : u32,
 };
@@ -4228,9 +4228,9 @@
 }
 
 struct tint_symbol {
-  @location(0) @index(0)
+  @location(0) @blend_src(0)
   color : vec4<f32>,
-  @location(0) @index(1)
+  @location(0) @blend_src(1)
   blend : vec4<f32>,
   @builtin(frag_depth)
   depth : f32,
@@ -4271,8 +4271,8 @@
 enable chromium_internal_dual_source_blending;
 
 struct FragOutput {
-  @location(0) @index(0) color : vec4<f32>,
-  @location(0) @index(1) blend : vec4<f32>,
+  @location(0) @blend_src(0) color : vec4<f32>,
+  @location(0) @blend_src(1) blend : vec4<f32>,
   @builtin(frag_depth) depth : f32,
   @builtin(sample_mask) mask : u32,
 };
@@ -4299,9 +4299,9 @@
 }
 
 struct tint_symbol {
-  @location(0) @index(0)
+  @location(0) @blend_src(0)
   color : vec4<f32>,
-  @location(0) @index(1)
+  @location(0) @blend_src(1)
   blend : vec4<f32>,
   @builtin(frag_depth)
   depth : f32,
diff --git a/src/tint/lang/spirv/writer/ast_raise/clamp_frag_depth.cc b/src/tint/lang/wgsl/ast/transform/clamp_frag_depth.cc
similarity index 97%
rename from src/tint/lang/spirv/writer/ast_raise/clamp_frag_depth.cc
rename to src/tint/lang/wgsl/ast/transform/clamp_frag_depth.cc
index 6683f18..c87fa15 100644
--- a/src/tint/lang/spirv/writer/ast_raise/clamp_frag_depth.cc
+++ b/src/tint/lang/wgsl/ast/transform/clamp_frag_depth.cc
@@ -25,7 +25,7 @@
 // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-#include "src/tint/lang/spirv/writer/ast_raise/clamp_frag_depth.h"
+#include "src/tint/lang/wgsl/ast/transform/clamp_frag_depth.h"
 
 #include <utility>
 
@@ -44,9 +44,9 @@
 #include "src/tint/utils/containers/vector.h"
 #include "src/tint/utils/macros/scoped_assignment.h"
 
-TINT_INSTANTIATE_TYPEINFO(tint::spirv::writer::ClampFragDepth);
+TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::ClampFragDepth);
 
-namespace tint::spirv::writer {
+namespace tint::ast::transform {
 
 /// PIMPL state for the transform
 struct ClampFragDepth::State {
@@ -242,4 +242,4 @@
     return State{src}.Run();
 }
 
-}  // namespace tint::spirv::writer
+}  // namespace tint::ast::transform
diff --git a/src/tint/lang/spirv/writer/ast_raise/clamp_frag_depth.h b/src/tint/lang/wgsl/ast/transform/clamp_frag_depth.h
similarity index 91%
rename from src/tint/lang/spirv/writer/ast_raise/clamp_frag_depth.h
rename to src/tint/lang/wgsl/ast/transform/clamp_frag_depth.h
index 521ba2e..5bcd3ea 100644
--- a/src/tint/lang/spirv/writer/ast_raise/clamp_frag_depth.h
+++ b/src/tint/lang/wgsl/ast/transform/clamp_frag_depth.h
@@ -25,12 +25,12 @@
 // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-#ifndef SRC_TINT_LANG_SPIRV_WRITER_AST_RAISE_CLAMP_FRAG_DEPTH_H_
-#define SRC_TINT_LANG_SPIRV_WRITER_AST_RAISE_CLAMP_FRAG_DEPTH_H_
+#ifndef SRC_TINT_LANG_WGSL_AST_TRANSFORM_CLAMP_FRAG_DEPTH_H_
+#define SRC_TINT_LANG_WGSL_AST_TRANSFORM_CLAMP_FRAG_DEPTH_H_
 
 #include "src/tint/lang/wgsl/ast/transform/transform.h"
 
-namespace tint::spirv::writer {
+namespace tint::ast::transform {
 
 /// Add clamping of the `@builtin(frag_depth)` output of fragment shaders using two push constants
 /// provided by the outside environment. For example the following code:
@@ -78,6 +78,6 @@
     struct State;
 };
 
-}  // namespace tint::spirv::writer
+}  // namespace tint::ast::transform
 
-#endif  // SRC_TINT_LANG_SPIRV_WRITER_AST_RAISE_CLAMP_FRAG_DEPTH_H_
+#endif  // SRC_TINT_LANG_WGSL_AST_TRANSFORM_CLAMP_FRAG_DEPTH_H_
diff --git a/src/tint/lang/spirv/writer/ast_raise/clamp_frag_depth_test.cc b/src/tint/lang/wgsl/ast/transform/clamp_frag_depth_test.cc
similarity index 98%
rename from src/tint/lang/spirv/writer/ast_raise/clamp_frag_depth_test.cc
rename to src/tint/lang/wgsl/ast/transform/clamp_frag_depth_test.cc
index 9a71665..cc09c6f 100644
--- a/src/tint/lang/spirv/writer/ast_raise/clamp_frag_depth_test.cc
+++ b/src/tint/lang/wgsl/ast/transform/clamp_frag_depth_test.cc
@@ -25,11 +25,11 @@
 // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-#include "src/tint/lang/spirv/writer/ast_raise/clamp_frag_depth.h"
+#include "src/tint/lang/wgsl/ast/transform/clamp_frag_depth.h"
 
 #include "src/tint/lang/wgsl/ast/transform/helper_test.h"
 
-namespace tint::spirv::writer {
+namespace tint::ast::transform {
 namespace {
 
 using ClampFragDepthTest = ast::transform::TransformTest;
@@ -391,4 +391,4 @@
 }
 
 }  // namespace
-}  // namespace tint::spirv::writer
+}  // namespace tint::ast::transform
diff --git a/src/tint/lang/wgsl/ast/transform/offset_first_index.cc b/src/tint/lang/wgsl/ast/transform/offset_first_index.cc
new file mode 100644
index 0000000..eb64f60
--- /dev/null
+++ b/src/tint/lang/wgsl/ast/transform/offset_first_index.cc
@@ -0,0 +1,201 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/wgsl/ast/transform/offset_first_index.h"
+
+#include <memory>
+#include <unordered_map>
+#include <utility>
+
+#include "src/tint/lang/core/builtin_value.h"
+#include "src/tint/lang/core/fluent_types.h"
+#include "src/tint/lang/wgsl/program/clone_context.h"
+#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
+#include "src/tint/lang/wgsl/sem/function.h"
+#include "src/tint/lang/wgsl/sem/member_accessor_expression.h"
+#include "src/tint/lang/wgsl/sem/struct.h"
+#include "src/tint/lang/wgsl/sem/variable.h"
+
+using namespace tint::core::fluent_types;  // NOLINT
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::OffsetFirstIndex);
+TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::OffsetFirstIndex::Config);
+
+namespace tint::ast::transform {
+namespace {
+
+// Push constant names
+constexpr char kFirstVertexName[] = "first_vertex";
+constexpr char kFirstInstanceName[] = "first_instance";
+
+bool ShouldRun(const Program& program) {
+    for (auto* fn : program.AST().Functions()) {
+        if (fn->PipelineStage() == PipelineStage::kVertex) {
+            return true;
+        }
+    }
+    return false;
+}
+
+}  // namespace
+
+OffsetFirstIndex::OffsetFirstIndex() = default;
+OffsetFirstIndex::~OffsetFirstIndex() = default;
+
+Transform::ApplyResult OffsetFirstIndex::Apply(const Program& src,
+                                               const DataMap& inputs,
+                                               DataMap&) const {
+    if (!ShouldRun(src)) {
+        return SkipTransform;
+    }
+
+    const Config* cfg = inputs.Get<Config>();
+    if (!cfg) {
+        return SkipTransform;
+    }
+
+    ProgramBuilder b;
+    program::CloneContext ctx{&b, &src, /* auto_clone_symbols */ true};
+
+    // Map of builtin usages
+    std::unordered_map<const sem::Variable*, const char*> builtin_vars;
+    std::unordered_map<const core::type::StructMember*, const char*> builtin_members;
+
+    bool has_vertex_index = false;
+    bool has_instance_index = false;
+
+    // Traverse the AST scanning for builtin accesses via variables (includes
+    // parameters) or structure member accesses.
+    for (auto* node : ctx.src->ASTNodes().Objects()) {
+        if (auto* var = node->As<Variable>()) {
+            for (auto* attr : var->attributes) {
+                if (auto* builtin_attr = attr->As<BuiltinAttribute>()) {
+                    core::BuiltinValue builtin = src.Sem().Get(builtin_attr)->Value();
+                    if (builtin == core::BuiltinValue::kVertexIndex && cfg &&
+                        cfg->first_vertex_offset.has_value()) {
+                        auto* sem_var = ctx.src->Sem().Get(var);
+                        builtin_vars.emplace(sem_var, kFirstVertexName);
+                        has_vertex_index = true;
+                    }
+                    if (builtin == core::BuiltinValue::kInstanceIndex && cfg &&
+                        cfg->first_instance_offset.has_value()) {
+                        auto* sem_var = ctx.src->Sem().Get(var);
+                        builtin_vars.emplace(sem_var, kFirstInstanceName);
+                        has_instance_index = true;
+                    }
+                }
+            }
+        }
+        if (auto* member = node->As<StructMember>()) {
+            for (auto* attr : member->attributes) {
+                if (auto* builtin_attr = attr->As<BuiltinAttribute>()) {
+                    core::BuiltinValue builtin = src.Sem().Get(builtin_attr)->Value();
+                    if (builtin == core::BuiltinValue::kVertexIndex && cfg &&
+                        cfg->first_vertex_offset.has_value()) {
+                        auto* sem_mem = ctx.src->Sem().Get(member);
+                        builtin_members.emplace(sem_mem, kFirstVertexName);
+                        has_vertex_index = true;
+                    }
+                    if (builtin == core::BuiltinValue::kInstanceIndex && cfg &&
+                        cfg->first_instance_offset.has_value()) {
+                        auto* sem_mem = ctx.src->Sem().Get(member);
+                        builtin_members.emplace(sem_mem, kFirstInstanceName);
+                        has_instance_index = true;
+                    }
+                }
+            }
+        }
+    }
+
+    if (!has_vertex_index && !has_instance_index) {
+        return SkipTransform;
+    }
+
+    // Abort on any use of push constants in the module.
+    for (auto* global : src.AST().GlobalVariables()) {
+        if (auto* var = global->As<ast::Var>()) {
+            auto* v = src.Sem().Get(var);
+            if (TINT_UNLIKELY(v->AddressSpace() == core::AddressSpace::kPushConstant)) {
+                TINT_ICE()
+                    << "OffsetFirstIndex doesn't know how to handle module that already use push "
+                       "constants (yet)";
+                return resolver::Resolve(b);
+            }
+        }
+    }
+
+    b.Enable(wgsl::Extension::kChromiumExperimentalPushConstant);
+
+    // Add push constant members and calculate byte offsets
+    tint::Vector<const StructMember*, 8> members;
+    if (has_vertex_index) {
+        members.Push(b.Member(kFirstVertexName, b.ty.u32(),
+                              Vector{b.MemberOffset(AInt(*cfg->first_vertex_offset))}));
+    }
+    if (has_instance_index) {
+        members.Push(b.Member(kFirstInstanceName, b.ty.u32(),
+                              Vector{b.MemberOffset(AInt(*cfg->first_instance_offset))}));
+    }
+    auto struct_ = b.Structure(b.Symbols().New("PushConstants"), std::move(members));
+    // Create a global to hold the uniform buffer
+    Symbol buffer_name = b.Symbols().New("push_constants");
+    b.GlobalVar(buffer_name, b.ty.Of(struct_), core::AddressSpace::kPushConstant);
+
+    // Fix up all references to the builtins with the offsets
+    ctx.ReplaceAll([&](const Expression* expr) -> const Expression* {
+        if (auto* sem = ctx.src->Sem().GetVal(expr)) {
+            if (auto* user = sem->UnwrapLoad()->As<sem::VariableUser>()) {
+                auto it = builtin_vars.find(user->Variable());
+                if (it != builtin_vars.end()) {
+                    return ctx.dst->Add(ctx.CloneWithoutTransform(expr),
+                                        ctx.dst->MemberAccessor(buffer_name, it->second));
+                }
+            }
+            if (auto* access = sem->As<sem::StructMemberAccess>()) {
+                auto it = builtin_members.find(access->Member());
+                if (it != builtin_members.end()) {
+                    return ctx.dst->Add(ctx.CloneWithoutTransform(expr),
+                                        ctx.dst->MemberAccessor(buffer_name, it->second));
+                }
+            }
+        }
+        // Not interested in this expression. Just clone.
+        return nullptr;
+    });
+
+    ctx.Clone();
+    return resolver::Resolve(b);
+}
+
+OffsetFirstIndex::Config::Config(std::optional<int32_t> first_vertex_off,
+                                 std::optional<int32_t> first_instance_off)
+    : first_vertex_offset(first_vertex_off), first_instance_offset(first_instance_off) {}
+
+OffsetFirstIndex::Config::~Config() = default;
+
+}  // namespace tint::ast::transform
diff --git a/src/tint/lang/wgsl/ast/transform/offset_first_index.h b/src/tint/lang/wgsl/ast/transform/offset_first_index.h
new file mode 100644
index 0000000..aa37559
--- /dev/null
+++ b/src/tint/lang/wgsl/ast/transform/offset_first_index.h
@@ -0,0 +1,111 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_TINT_LANG_WGSL_AST_TRANSFORM_OFFSET_FIRST_INDEX_H_
+#define SRC_TINT_LANG_WGSL_AST_TRANSFORM_OFFSET_FIRST_INDEX_H_
+
+#include "src/tint/lang/wgsl/ast/transform/transform.h"
+
+namespace tint::ast::transform {
+
+/// Adds firstVertex/Instance (injected via push constants) to
+/// vertex/instance index builtins.
+///
+/// This transform assumes that Name transform has been run before.
+///
+/// Some shading languages start vertex and instance numbering at 0,
+/// regardless of the firstVertex/firstInstance value specified. This transform
+/// adds the value of firstVertex/firstInstance to each builtin. This action is
+/// performed by adding a new push constant equal to original builtin +
+/// firstVertex/firstInstance to each function that references one of
+/// these builtins.
+///
+/// For D3D, this affects both firstVertex and firstInstance. For OpenGL,
+/// it applies to only firstInstance. For this reason, the first_vertex_offset
+/// and first_instance_offset are optional, where std::nullopt indicates that
+/// no subsitution is to be performed.
+///
+/// Before:
+/// ```
+///   @builtin(vertex_index) var<in> vert_idx : u32;
+///   @builtin(instance_index) var<in> inst_idx : u32;
+///   fn func() -> u32 {
+///     return vert_idx * inst_idx;
+///   }
+/// ```
+///
+/// After:
+/// ```
+/// struct PushConstants {
+///   first_vertex : u32,
+///   first_instance : u32,
+/// }
+///
+/// var<push_constant> push_constants : PushConstants;
+///
+///   @builtin(vertex_index) var<in> vert_idx : u32;
+///   @builtin(instance_index) var<in> inst_idx : u32;
+///   fn func() -> u32 {
+///     return (vert_idx + push_constants.first_vertex) * (inst_idx +
+///     push_constants.first_instance);
+///   }
+/// ```
+///
+class OffsetFirstIndex final : public Castable<OffsetFirstIndex, Transform> {
+  public:
+    /// Transform configuration options
+    struct Config final : public Castable<Config, ast::transform::Data> {
+        /// Constructor
+        /// @param first_vertex_off Offset of the firstVertex push constant
+        /// @param first_instance_off Location of the firstInstance push constant
+        Config(std::optional<int32_t> first_vertex_off, std::optional<int32_t> first_instance_off);
+
+        /// Destructor
+        ~Config() override;
+
+        /// Offset of the firstVertex push constant
+        const std::optional<uint32_t> first_vertex_offset;
+
+        /// Offset of the firstInstance push constant
+        const std::optional<uint32_t> first_instance_offset;
+    };
+
+    /// Constructor
+    OffsetFirstIndex();
+
+    /// Destructor
+    ~OffsetFirstIndex() override;
+
+    /// @copydoc Transform::Apply
+    ApplyResult Apply(const Program& program,
+                      const DataMap& inputs,
+                      DataMap& outputs) const override;
+};
+
+}  // namespace tint::ast::transform
+
+#endif  // SRC_TINT_LANG_WGSL_AST_TRANSFORM_OFFSET_FIRST_INDEX_H_
diff --git a/src/tint/lang/wgsl/ast/transform/offset_first_index_test.cc b/src/tint/lang/wgsl/ast/transform/offset_first_index_test.cc
new file mode 100644
index 0000000..37a79a9
--- /dev/null
+++ b/src/tint/lang/wgsl/ast/transform/offset_first_index_test.cc
@@ -0,0 +1,856 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/wgsl/ast/transform/offset_first_index.h"
+
+#include <memory>
+#include <utility>
+#include <vector>
+
+#include "src/tint/lang/wgsl/ast/transform/helper_test.h"
+
+namespace tint::ast::transform {
+namespace {
+
+using OffsetFirstIndexTest = TransformTest;
+
+TEST_F(OffsetFirstIndexTest, ShouldRunEmptyModule) {
+    auto* src = R"()";
+
+    DataMap config;
+    config.Add<OffsetFirstIndex::Config>(0, 4);
+    EXPECT_FALSE(ShouldRun<OffsetFirstIndex>(src, config));
+}
+
+TEST_F(OffsetFirstIndexTest, ShouldRunFragmentStage) {
+    auto* src = R"(
+@fragment
+fn entry() {
+  return;
+}
+)";
+
+    DataMap config;
+    config.Add<OffsetFirstIndex::Config>(0, 4);
+    EXPECT_FALSE(ShouldRun<OffsetFirstIndex>(src, config));
+}
+
+TEST_F(OffsetFirstIndexTest, ShouldRunVertexStage) {
+    auto* src = R"(
+@vertex
+fn entry() -> @builtin(position) vec4<f32> {
+  return vec4<f32>();
+}
+)";
+
+    DataMap config;
+    config.Add<OffsetFirstIndex::Config>(0, 4);
+    EXPECT_FALSE(ShouldRun<OffsetFirstIndex>(src, config));
+}
+
+TEST_F(OffsetFirstIndexTest, ShouldRunVertexStageWithVertexIndex) {
+    auto* src = R"(
+@vertex
+fn entry(@builtin(vertex_index) vert_idx : u32) -> @builtin(position) vec4<f32> {
+  return vec4<f32>();
+}
+)";
+
+    DataMap config;
+    config.Add<OffsetFirstIndex::Config>(0, 4);
+    EXPECT_TRUE(ShouldRun<OffsetFirstIndex>(src, config));
+}
+
+TEST_F(OffsetFirstIndexTest, ShouldRunVertexStageWithInstanceIndex) {
+    auto* src = R"(
+@vertex
+fn entry(@builtin(instance_index) inst_idx : u32) -> @builtin(position) vec4<f32> {
+  return vec4<f32>();
+}
+)";
+
+    DataMap config;
+    config.Add<OffsetFirstIndex::Config>(0, 4);
+    EXPECT_TRUE(ShouldRun<OffsetFirstIndex>(src, config));
+}
+
+TEST_F(OffsetFirstIndexTest, EmptyModule) {
+    auto* src = "";
+    auto* expect = "";
+
+    DataMap config;
+    config.Add<OffsetFirstIndex::Config>(0, 4);
+    auto got = Run<OffsetFirstIndex>(src, std::move(config));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(OffsetFirstIndexTest, BasicVertexShader) {
+    auto* src = R"(
+@vertex
+fn entry() -> @builtin(position) vec4<f32> {
+  return vec4<f32>();
+}
+)";
+    auto* expect = src;
+
+    DataMap config;
+    config.Add<OffsetFirstIndex::Config>(0, 4);
+    auto got = Run<OffsetFirstIndex>(src, std::move(config));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(OffsetFirstIndexTest, BasicModuleVertexIndex) {
+    auto* src = R"(
+fn test(vert_idx : u32) -> u32 {
+  return vert_idx;
+}
+
+@vertex
+fn entry(@builtin(vertex_index) vert_idx : u32) -> @builtin(position) vec4<f32> {
+  test(vert_idx);
+  return vec4<f32>();
+}
+)";
+
+    auto* expect = R"(
+enable chromium_experimental_push_constant;
+
+struct PushConstants {
+  /* @offset(0) */
+  first_vertex : u32,
+}
+
+var<push_constant> push_constants : PushConstants;
+
+fn test(vert_idx : u32) -> u32 {
+  return vert_idx;
+}
+
+@vertex
+fn entry(@builtin(vertex_index) vert_idx : u32) -> @builtin(position) vec4<f32> {
+  test((vert_idx + push_constants.first_vertex));
+  return vec4<f32>();
+}
+)";
+
+    DataMap config;
+    config.Add<OffsetFirstIndex::Config>(0, 4);
+    auto got = Run<OffsetFirstIndex>(src, std::move(config));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(OffsetFirstIndexTest, BasicModuleVertexIndex_OutOfOrder) {
+    auto* src = R"(
+@vertex
+fn entry(@builtin(vertex_index) vert_idx : u32) -> @builtin(position) vec4<f32> {
+  test(vert_idx);
+  return vec4<f32>();
+}
+
+fn test(vert_idx : u32) -> u32 {
+  return vert_idx;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_experimental_push_constant;
+
+struct PushConstants {
+  /* @offset(0) */
+  first_vertex : u32,
+}
+
+var<push_constant> push_constants : PushConstants;
+
+@vertex
+fn entry(@builtin(vertex_index) vert_idx : u32) -> @builtin(position) vec4<f32> {
+  test((vert_idx + push_constants.first_vertex));
+  return vec4<f32>();
+}
+
+fn test(vert_idx : u32) -> u32 {
+  return vert_idx;
+}
+)";
+
+    DataMap config;
+    config.Add<OffsetFirstIndex::Config>(0, 4);
+    auto got = Run<OffsetFirstIndex>(src, std::move(config));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(OffsetFirstIndexTest, BasicModuleInstanceIndex) {
+    auto* src = R"(
+fn test(inst_idx : u32) -> u32 {
+  return inst_idx;
+}
+
+@vertex
+fn entry(@builtin(instance_index) inst_idx : u32) -> @builtin(position) vec4<f32> {
+  test(inst_idx);
+  return vec4<f32>();
+}
+)";
+
+    auto* expect = R"(
+enable chromium_experimental_push_constant;
+
+struct PushConstants {
+  @size(4)
+  padding_0 : u32,
+  /* @offset(4) */
+  first_instance : u32,
+}
+
+var<push_constant> push_constants : PushConstants;
+
+fn test(inst_idx : u32) -> u32 {
+  return inst_idx;
+}
+
+@vertex
+fn entry(@builtin(instance_index) inst_idx : u32) -> @builtin(position) vec4<f32> {
+  test((inst_idx + push_constants.first_instance));
+  return vec4<f32>();
+}
+)";
+
+    DataMap config;
+    config.Add<OffsetFirstIndex::Config>(0, 4);
+    auto got = Run<OffsetFirstIndex>(src, std::move(config));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(OffsetFirstIndexTest, BasicModuleInstanceIndex_OutOfOrder) {
+    auto* src = R"(
+@vertex
+fn entry(@builtin(instance_index) inst_idx : u32) -> @builtin(position) vec4<f32> {
+  test(inst_idx);
+  return vec4<f32>();
+}
+
+fn test(inst_idx : u32) -> u32 {
+  return inst_idx;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_experimental_push_constant;
+
+struct PushConstants {
+  @size(4)
+  padding_0 : u32,
+  /* @offset(4) */
+  first_instance : u32,
+}
+
+var<push_constant> push_constants : PushConstants;
+
+@vertex
+fn entry(@builtin(instance_index) inst_idx : u32) -> @builtin(position) vec4<f32> {
+  test((inst_idx + push_constants.first_instance));
+  return vec4<f32>();
+}
+
+fn test(inst_idx : u32) -> u32 {
+  return inst_idx;
+}
+)";
+
+    DataMap config;
+    config.Add<OffsetFirstIndex::Config>(0, 4);
+    auto got = Run<OffsetFirstIndex>(src, std::move(config));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(OffsetFirstIndexTest, BasicModuleBothIndexes) {
+    auto* src = R"(
+fn test(instance_idx : u32, vert_idx : u32) -> u32 {
+  return instance_idx + vert_idx;
+}
+
+struct Inputs {
+  @builtin(instance_index) instance_idx : u32,
+  @builtin(vertex_index) vert_idx : u32,
+};
+
+@vertex
+fn entry(inputs : Inputs) -> @builtin(position) vec4<f32> {
+  test(inputs.instance_idx, inputs.vert_idx);
+  return vec4<f32>();
+}
+)";
+
+    auto* expect = R"(
+enable chromium_experimental_push_constant;
+
+struct PushConstants {
+  /* @offset(0) */
+  first_vertex : u32,
+  /* @offset(4) */
+  first_instance : u32,
+}
+
+var<push_constant> push_constants : PushConstants;
+
+fn test(instance_idx : u32, vert_idx : u32) -> u32 {
+  return (instance_idx + vert_idx);
+}
+
+struct Inputs {
+  @builtin(instance_index)
+  instance_idx : u32,
+  @builtin(vertex_index)
+  vert_idx : u32,
+}
+
+@vertex
+fn entry(inputs : Inputs) -> @builtin(position) vec4<f32> {
+  test((inputs.instance_idx + push_constants.first_instance), (inputs.vert_idx + push_constants.first_vertex));
+  return vec4<f32>();
+}
+)";
+
+    DataMap config;
+    config.Add<OffsetFirstIndex::Config>(0, 4);
+    auto got = Run<OffsetFirstIndex>(src, std::move(config));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(OffsetFirstIndexTest, BasicModuleBothIndexes_OutOfOrder) {
+    auto* src = R"(
+@vertex
+fn entry(inputs : Inputs) -> @builtin(position) vec4<f32> {
+  test(inputs.instance_idx, inputs.vert_idx);
+  return vec4<f32>();
+}
+
+struct Inputs {
+  @builtin(instance_index) instance_idx : u32,
+  @builtin(vertex_index) vert_idx : u32,
+};
+
+fn test(instance_idx : u32, vert_idx : u32) -> u32 {
+  return instance_idx + vert_idx;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_experimental_push_constant;
+
+struct PushConstants {
+  /* @offset(0) */
+  first_vertex : u32,
+  /* @offset(4) */
+  first_instance : u32,
+}
+
+var<push_constant> push_constants : PushConstants;
+
+@vertex
+fn entry(inputs : Inputs) -> @builtin(position) vec4<f32> {
+  test((inputs.instance_idx + push_constants.first_instance), (inputs.vert_idx + push_constants.first_vertex));
+  return vec4<f32>();
+}
+
+struct Inputs {
+  @builtin(instance_index)
+  instance_idx : u32,
+  @builtin(vertex_index)
+  vert_idx : u32,
+}
+
+fn test(instance_idx : u32, vert_idx : u32) -> u32 {
+  return (instance_idx + vert_idx);
+}
+)";
+
+    DataMap config;
+    config.Add<OffsetFirstIndex::Config>(0, 4);
+    auto got = Run<OffsetFirstIndex>(src, std::move(config));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(OffsetFirstIndexTest, BasicModuleBothIndexesVertexDisabled) {
+    auto* src = R"(
+fn test(instance_idx : u32, vert_idx : u32) -> u32 {
+  return instance_idx + vert_idx;
+}
+
+struct Inputs {
+  @builtin(instance_index) instance_idx : u32,
+  @builtin(vertex_index) vert_idx : u32,
+};
+
+@vertex
+fn entry(inputs : Inputs) -> @builtin(position) vec4<f32> {
+  test(inputs.instance_idx, inputs.vert_idx);
+  return vec4<f32>();
+}
+)";
+
+    auto* expect = R"(
+enable chromium_experimental_push_constant;
+
+struct PushConstants {
+  /* @offset(0) */
+  first_instance : u32,
+}
+
+var<push_constant> push_constants : PushConstants;
+
+fn test(instance_idx : u32, vert_idx : u32) -> u32 {
+  return (instance_idx + vert_idx);
+}
+
+struct Inputs {
+  @builtin(instance_index)
+  instance_idx : u32,
+  @builtin(vertex_index)
+  vert_idx : u32,
+}
+
+@vertex
+fn entry(inputs : Inputs) -> @builtin(position) vec4<f32> {
+  test((inputs.instance_idx + push_constants.first_instance), inputs.vert_idx);
+  return vec4<f32>();
+}
+)";
+
+    DataMap config;
+    config.Add<OffsetFirstIndex::Config>(std::nullopt, 0);
+    auto got = Run<OffsetFirstIndex>(src, std::move(config));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(OffsetFirstIndexTest, BasicModuleBothIndexesInstanceDisabled) {
+    auto* src = R"(
+fn test(instance_idx : u32, vert_idx : u32) -> u32 {
+  return instance_idx + vert_idx;
+}
+
+struct Inputs {
+  @builtin(instance_index) instance_idx : u32,
+  @builtin(vertex_index) vert_idx : u32,
+};
+
+@vertex
+fn entry(inputs : Inputs) -> @builtin(position) vec4<f32> {
+  test(inputs.instance_idx, inputs.vert_idx);
+  return vec4<f32>();
+}
+)";
+
+    auto* expect = R"(
+enable chromium_experimental_push_constant;
+
+struct PushConstants {
+  /* @offset(0) */
+  first_vertex : u32,
+}
+
+var<push_constant> push_constants : PushConstants;
+
+fn test(instance_idx : u32, vert_idx : u32) -> u32 {
+  return (instance_idx + vert_idx);
+}
+
+struct Inputs {
+  @builtin(instance_index)
+  instance_idx : u32,
+  @builtin(vertex_index)
+  vert_idx : u32,
+}
+
+@vertex
+fn entry(inputs : Inputs) -> @builtin(position) vec4<f32> {
+  test(inputs.instance_idx, (inputs.vert_idx + push_constants.first_vertex));
+  return vec4<f32>();
+}
+)";
+
+    DataMap config;
+    config.Add<OffsetFirstIndex::Config>(0, std::nullopt);
+    auto got = Run<OffsetFirstIndex>(src, std::move(config));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(OffsetFirstIndexTest, BasicModuleBothIndexesBothDisabled) {
+    auto* src = R"(
+fn test(instance_idx : u32, vert_idx : u32) -> u32 {
+  return (instance_idx + vert_idx);
+}
+
+struct Inputs {
+  @builtin(instance_index)
+  instance_idx : u32,
+  @builtin(vertex_index)
+  vert_idx : u32,
+}
+
+@vertex
+fn entry(inputs : Inputs) -> @builtin(position) vec4<f32> {
+  test(inputs.instance_idx, inputs.vert_idx);
+  return vec4<f32>();
+}
+)";
+
+    auto* expect = src;
+
+    DataMap config;
+    config.Add<OffsetFirstIndex::Config>(std::nullopt, std::nullopt);
+    auto got = Run<OffsetFirstIndex>(src, std::move(config));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+// Test a shader with a user-declared struct called PushConstants, to force
+// renaming of the Tint-provided PushConstants struct.
+TEST_F(OffsetFirstIndexTest, ForceRenamingPushConstantsStruct) {
+    auto* src = R"(
+struct PushConstants {
+  f : f32,
+}
+
+@group(0) @binding(0) var<uniform> p : PushConstants;
+
+@vertex
+fn entry(@builtin(vertex_index) vert_idx : u32) -> @builtin(position) vec4<f32> {
+  return vec4<f32>(f32(vert_idx) + p.f);
+}
+)";
+
+    auto* expect = R"(
+enable chromium_experimental_push_constant;
+
+struct PushConstants_1 {
+  /* @offset(0) */
+  first_vertex : u32,
+}
+
+var<push_constant> push_constants : PushConstants_1;
+
+struct PushConstants {
+  f : f32,
+}
+
+@group(0) @binding(0) var<uniform> p : PushConstants;
+
+@vertex
+fn entry(@builtin(vertex_index) vert_idx : u32) -> @builtin(position) vec4<f32> {
+  return vec4<f32>((f32((vert_idx + push_constants.first_vertex)) + p.f));
+}
+)";
+
+    DataMap config;
+    config.Add<OffsetFirstIndex::Config>(0, 4);
+    auto got = Run<OffsetFirstIndex>(src, std::move(config));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+// Test a shader with a user-declared variable called push_constants, to force
+// renaming of the Tint-provided push_constants variable.
+TEST_F(OffsetFirstIndexTest, ForceRenamingPushConstantsVar) {
+    auto* src = R"(
+@group(0) @binding(0) var<uniform> push_constants : u32;
+
+@vertex
+fn entry(@builtin(vertex_index) vert_idx : u32) -> @builtin(position) vec4<f32> {
+  return vec4<f32>(f32(vert_idx));
+}
+)";
+
+    auto* expect = R"(
+enable chromium_experimental_push_constant;
+
+struct PushConstants {
+  /* @offset(0) */
+  first_vertex : u32,
+}
+
+var<push_constant> push_constants_1 : PushConstants;
+
+@group(0) @binding(0) var<uniform> push_constants : u32;
+
+@vertex
+fn entry(@builtin(vertex_index) vert_idx : u32) -> @builtin(position) vec4<f32> {
+  return vec4<f32>(f32((vert_idx + push_constants_1.first_vertex)));
+}
+)";
+
+    DataMap config;
+    config.Add<OffsetFirstIndex::Config>(0, 4);
+    auto got = Run<OffsetFirstIndex>(src, std::move(config));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(OffsetFirstIndexTest, NestedCalls) {
+    auto* src = R"(
+fn func1(vert_idx : u32) -> u32 {
+  return vert_idx;
+}
+
+fn func2(vert_idx : u32) -> u32 {
+  return func1(vert_idx);
+}
+
+@vertex
+fn entry(@builtin(vertex_index) vert_idx : u32) -> @builtin(position) vec4<f32> {
+  func2(vert_idx);
+  return vec4<f32>();
+}
+)";
+
+    auto* expect = R"(
+enable chromium_experimental_push_constant;
+
+struct PushConstants {
+  /* @offset(0) */
+  first_vertex : u32,
+}
+
+var<push_constant> push_constants : PushConstants;
+
+fn func1(vert_idx : u32) -> u32 {
+  return vert_idx;
+}
+
+fn func2(vert_idx : u32) -> u32 {
+  return func1(vert_idx);
+}
+
+@vertex
+fn entry(@builtin(vertex_index) vert_idx : u32) -> @builtin(position) vec4<f32> {
+  func2((vert_idx + push_constants.first_vertex));
+  return vec4<f32>();
+}
+)";
+
+    DataMap config;
+    config.Add<OffsetFirstIndex::Config>(0, 4);
+    auto got = Run<OffsetFirstIndex>(src, std::move(config));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(OffsetFirstIndexTest, NestedCalls_OutOfOrder) {
+    auto* src = R"(
+@vertex
+fn entry(@builtin(vertex_index) vert_idx : u32) -> @builtin(position) vec4<f32> {
+  func2(vert_idx);
+  return vec4<f32>();
+}
+
+fn func2(vert_idx : u32) -> u32 {
+  return func1(vert_idx);
+}
+
+fn func1(vert_idx : u32) -> u32 {
+  return vert_idx;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_experimental_push_constant;
+
+struct PushConstants {
+  /* @offset(0) */
+  first_vertex : u32,
+}
+
+var<push_constant> push_constants : PushConstants;
+
+@vertex
+fn entry(@builtin(vertex_index) vert_idx : u32) -> @builtin(position) vec4<f32> {
+  func2((vert_idx + push_constants.first_vertex));
+  return vec4<f32>();
+}
+
+fn func2(vert_idx : u32) -> u32 {
+  return func1(vert_idx);
+}
+
+fn func1(vert_idx : u32) -> u32 {
+  return vert_idx;
+}
+)";
+
+    DataMap config;
+    config.Add<OffsetFirstIndex::Config>(0, 4);
+    auto got = Run<OffsetFirstIndex>(src, std::move(config));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(OffsetFirstIndexTest, MultipleEntryPoints) {
+    auto* src = R"(
+fn func(i : u32) -> u32 {
+  return i;
+}
+
+@vertex
+fn entry_a(@builtin(vertex_index) vert_idx : u32) -> @builtin(position) vec4<f32> {
+  func(vert_idx);
+  return vec4<f32>();
+}
+
+@vertex
+fn entry_b(@builtin(vertex_index) vert_idx : u32, @builtin(instance_index) inst_idx : u32) -> @builtin(position) vec4<f32> {
+  func(vert_idx + inst_idx);
+  return vec4<f32>();
+}
+
+@vertex
+fn entry_c(@builtin(instance_index) inst_idx : u32) -> @builtin(position) vec4<f32> {
+  func(inst_idx);
+  return vec4<f32>();
+}
+)";
+
+    auto* expect = R"(
+enable chromium_experimental_push_constant;
+
+struct PushConstants {
+  /* @offset(0) */
+  first_vertex : u32,
+  /* @offset(4) */
+  first_instance : u32,
+}
+
+var<push_constant> push_constants : PushConstants;
+
+fn func(i : u32) -> u32 {
+  return i;
+}
+
+@vertex
+fn entry_a(@builtin(vertex_index) vert_idx : u32) -> @builtin(position) vec4<f32> {
+  func((vert_idx + push_constants.first_vertex));
+  return vec4<f32>();
+}
+
+@vertex
+fn entry_b(@builtin(vertex_index) vert_idx : u32, @builtin(instance_index) inst_idx : u32) -> @builtin(position) vec4<f32> {
+  func(((vert_idx + push_constants.first_vertex) + (inst_idx + push_constants.first_instance)));
+  return vec4<f32>();
+}
+
+@vertex
+fn entry_c(@builtin(instance_index) inst_idx : u32) -> @builtin(position) vec4<f32> {
+  func((inst_idx + push_constants.first_instance));
+  return vec4<f32>();
+}
+)";
+
+    DataMap config;
+    config.Add<OffsetFirstIndex::Config>(0, 4);
+    auto got = Run<OffsetFirstIndex>(src, std::move(config));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(OffsetFirstIndexTest, MultipleEntryPoints_OutOfOrder) {
+    auto* src = R"(
+@vertex
+fn entry_a(@builtin(vertex_index) vert_idx : u32) -> @builtin(position) vec4<f32> {
+  func(vert_idx);
+  return vec4<f32>();
+}
+
+@vertex
+fn entry_b(@builtin(vertex_index) vert_idx : u32, @builtin(instance_index) inst_idx : u32) -> @builtin(position) vec4<f32> {
+  func(vert_idx + inst_idx);
+  return vec4<f32>();
+}
+
+@vertex
+fn entry_c(@builtin(instance_index) inst_idx : u32) -> @builtin(position) vec4<f32> {
+  func(inst_idx);
+  return vec4<f32>();
+}
+
+fn func(i : u32) -> u32 {
+  return i;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_experimental_push_constant;
+
+struct PushConstants {
+  /* @offset(0) */
+  first_vertex : u32,
+  /* @offset(4) */
+  first_instance : u32,
+}
+
+var<push_constant> push_constants : PushConstants;
+
+@vertex
+fn entry_a(@builtin(vertex_index) vert_idx : u32) -> @builtin(position) vec4<f32> {
+  func((vert_idx + push_constants.first_vertex));
+  return vec4<f32>();
+}
+
+@vertex
+fn entry_b(@builtin(vertex_index) vert_idx : u32, @builtin(instance_index) inst_idx : u32) -> @builtin(position) vec4<f32> {
+  func(((vert_idx + push_constants.first_vertex) + (inst_idx + push_constants.first_instance)));
+  return vec4<f32>();
+}
+
+@vertex
+fn entry_c(@builtin(instance_index) inst_idx : u32) -> @builtin(position) vec4<f32> {
+  func((inst_idx + push_constants.first_instance));
+  return vec4<f32>();
+}
+
+fn func(i : u32) -> u32 {
+  return i;
+}
+)";
+
+    DataMap config;
+    config.Add<OffsetFirstIndex::Config>(0, 4);
+    auto got = Run<OffsetFirstIndex>(src, std::move(config));
+
+    EXPECT_EQ(expect, str(got));
+}
+
+}  // namespace
+}  // namespace tint::ast::transform
diff --git a/src/tint/lang/wgsl/diagnostic_severity.h b/src/tint/lang/wgsl/diagnostic_severity.h
index dfccb5d..88eba28 100644
--- a/src/tint/lang/wgsl/diagnostic_severity.h
+++ b/src/tint/lang/wgsl/diagnostic_severity.h
@@ -38,9 +38,9 @@
 #define SRC_TINT_LANG_WGSL_DIAGNOSTIC_SEVERITY_H_
 
 #include <string>
-#include <unordered_map>
 
 #include "src/tint/lang/wgsl/diagnostic_rule.h"
+#include "src/tint/utils/containers/hashmap.h"
 #include "src/tint/utils/diagnostic/diagnostic.h"
 #include "src/tint/utils/traits/traits.h"
 
@@ -83,7 +83,7 @@
 diag::Severity ToSeverity(DiagnosticSeverity sc);
 
 /// DiagnosticRuleSeverities is a map from diagnostic rule to diagnostic severity.
-using DiagnosticRuleSeverities = std::unordered_map<DiagnosticRule, DiagnosticSeverity>;
+using DiagnosticRuleSeverities = Hashmap<DiagnosticRule, DiagnosticSeverity, 1>;
 
 }  // namespace tint::wgsl
 
diff --git a/src/tint/lang/wgsl/diagnostic_severity.h.tmpl b/src/tint/lang/wgsl/diagnostic_severity.h.tmpl
index 045591f..40714cb 100644
--- a/src/tint/lang/wgsl/diagnostic_severity.h.tmpl
+++ b/src/tint/lang/wgsl/diagnostic_severity.h.tmpl
@@ -15,11 +15,11 @@
 #define SRC_TINT_LANG_WGSL_DIAGNOSTIC_SEVERITY_H_
 
 #include <string>
-#include <unordered_map>
 
-#include "src/tint/utils/traits/traits.h"
 #include "src/tint/lang/wgsl/diagnostic_rule.h"
+#include "src/tint/utils/containers/hashmap.h"
 #include "src/tint/utils/diagnostic/diagnostic.h"
+#include "src/tint/utils/traits/traits.h"
 
 namespace tint::wgsl {
 
@@ -30,7 +30,7 @@
 diag::Severity ToSeverity(DiagnosticSeverity sc);
 
 /// DiagnosticRuleSeverities is a map from diagnostic rule to diagnostic severity.
-using DiagnosticRuleSeverities = std::unordered_map<DiagnosticRule, DiagnosticSeverity>;
+using DiagnosticRuleSeverities = Hashmap<DiagnosticRule, DiagnosticSeverity, 1>;
 
 }  // namespace tint::wgsl
 
diff --git a/src/tint/lang/wgsl/features/status.cc b/src/tint/lang/wgsl/features/status.cc
index 2fe2f02..334c1b6 100644
--- a/src/tint/lang/wgsl/features/status.cc
+++ b/src/tint/lang/wgsl/features/status.cc
@@ -33,14 +33,23 @@
 
 FeatureStatus GetLanguageFeatureStatus(LanguageFeature f) {
     switch (f) {
+            ////////////////////////////////////////////////////////////////////
+            // Experimental features
+            ///////////////////////////////////////////////////////////////////
         case LanguageFeature::kPacked4X8IntegerDotProduct:
         case LanguageFeature::kPointerCompositeAccess:
         case LanguageFeature::kReadonlyAndReadwriteStorageTextures:
-        case LanguageFeature::kUnrestrictedPointerParameters:
             return FeatureStatus::kExperimental;
-        case LanguageFeature::kUndefined:
-            return FeatureStatus::kUnknown;
 
+            ////////////////////////////////////////////////////////////////////
+            // Enabled features
+            ////////////////////////////////////////////////////////////////////
+        case LanguageFeature::kUnrestrictedPointerParameters:
+            return FeatureStatus::kShippedWithKillswitch;
+
+            ////////////////////////////////////////////////////////////////////
+            // Testing / special cases
+            ////////////////////////////////////////////////////////////////////
         case LanguageFeature::kChromiumTestingUnimplemented:
             return FeatureStatus::kUnimplemented;
         case LanguageFeature::kChromiumTestingUnsafeExperimental:
@@ -51,6 +60,8 @@
             return FeatureStatus::kShippedWithKillswitch;
         case LanguageFeature::kChromiumTestingShipped:
             return FeatureStatus::kShipped;
+        case LanguageFeature::kUndefined:
+            return FeatureStatus::kUnknown;
     }
 
     return FeatureStatus::kUnknown;
diff --git a/src/tint/lang/wgsl/inspector/inspector.cc b/src/tint/lang/wgsl/inspector/inspector.cc
index d7fe564..6c5d935 100644
--- a/src/tint/lang/wgsl/inspector/inspector.cc
+++ b/src/tint/lang/wgsl/inspector/inspector.cc
@@ -814,14 +814,14 @@
         }
 
         auto* call_func = call->Stmt()->Function();
-        std::vector<const sem::Function*> entry_points;
+        Vector<const sem::Function*, 4> entry_points;
         if (call_func->Declaration()->IsEntryPoint()) {
             entry_points = {call_func};
         } else {
             entry_points = call_func->AncestorEntryPoints();
         }
 
-        if (entry_points.empty()) {
+        if (entry_points.IsEmpty()) {
             continue;
         }
 
@@ -974,7 +974,7 @@
             globals[i] = global;
         } else if (auto* param = root_ident->As<sem::Parameter>()) {
             auto* func = tint::As<sem::Function>(param->Owner());
-            if (func->CallSites().empty()) {
+            if (func->CallSites().IsEmpty()) {
                 // One or more of the expressions is a parameter, but this function
                 // is not called. Ignore.
                 return;
@@ -1141,6 +1141,7 @@
                 });
         }
     }
+
     return res;
 }
 
diff --git a/src/tint/lang/wgsl/ir_roundtrip_test.cc b/src/tint/lang/wgsl/ir_roundtrip_test.cc
index d3497d0..9b7b522 100644
--- a/src/tint/lang/wgsl/ir_roundtrip_test.cc
+++ b/src/tint/lang/wgsl/ir_roundtrip_test.cc
@@ -256,7 +256,7 @@
 
 struct S {
   a : i32,
-  @location(0u) @index(0u)
+  @location(0u) @blend_src(0u)
   b : u32,
   c : f32,
 }
diff --git a/src/tint/lang/wgsl/reader/parser/const_literal_test.cc b/src/tint/lang/wgsl/reader/parser/const_literal_test.cc
index 4cb239f..eff472c 100644
--- a/src/tint/lang/wgsl/reader/parser/const_literal_test.cc
+++ b/src/tint/lang/wgsl/reader/parser/const_literal_test.cc
@@ -147,7 +147,8 @@
         EXPECT_EQ(c->As<ast::FloatLiteralExpression>()->suffix,
                   ast::FloatLiteralExpression::Suffix::kNone);
     }
-    EXPECT_EQ(c->source.range, (Source::Range{{1u, 1u}, {1u, 1u + params.input.size()}}));
+    EXPECT_EQ(c->source.range,
+              (Source::Range{{1u, 1u}, {1u, 1u + static_cast<uint32_t>(params.input.size())}}));
 }
 using FloatLiteralTestCaseList = std::vector<FloatLiteralTestCase>;
 
diff --git a/src/tint/lang/wgsl/reader/parser/error_resync_test.cc b/src/tint/lang/wgsl/reader/parser/error_resync_test.cc
index a38fd87..d818893 100644
--- a/src/tint/lang/wgsl/reader/parser/error_resync_test.cc
+++ b/src/tint/lang/wgsl/reader/parser/error_resync_test.cc
@@ -67,7 +67,7 @@
      ^
 
 test.wgsl:4:2 error: expected attribute
-Possible values: 'align', 'binding', 'builtin', 'color', 'compute', 'diagnostic', 'fragment', 'group', 'id', 'index', 'interpolate', 'invariant', 'location', 'must_use', 'size', 'vertex', 'workgroup_size'
+Possible values: 'align', 'binding', 'blend_src', 'builtin', 'color', 'compute', 'diagnostic', 'fragment', 'group', 'id', 'interpolate', 'invariant', 'location', 'must_use', 'size', 'vertex', 'workgroup_size'
 @_ fn -> {}
  ^
 )");
@@ -135,7 +135,7 @@
          ^^^^
 
 test.wgsl:7:6 error: expected attribute
-Possible values: 'align', 'binding', 'builtin', 'color', 'compute', 'diagnostic', 'fragment', 'group', 'id', 'index', 'interpolate', 'invariant', 'location', 'must_use', 'size', 'vertex', 'workgroup_size'
+Possible values: 'align', 'binding', 'blend_src', 'builtin', 'color', 'compute', 'diagnostic', 'fragment', 'group', 'id', 'interpolate', 'invariant', 'location', 'must_use', 'size', 'vertex', 'workgroup_size'
     @- x : i32,
      ^
 )");
diff --git a/src/tint/lang/wgsl/reader/parser/function_attribute_list_test.cc b/src/tint/lang/wgsl/reader/parser/function_attribute_list_test.cc
index 4fae501..3e66246 100644
--- a/src/tint/lang/wgsl/reader/parser/function_attribute_list_test.cc
+++ b/src/tint/lang/wgsl/reader/parser/function_attribute_list_test.cc
@@ -31,7 +31,7 @@
 namespace tint::wgsl::reader {
 namespace {
 
-TEST_F(WGSLParserTest, AttributeList_Parses) {
+TEST_F(WGSLParserTest, FunctionAttributeList_Parses) {
     auto p = parser("@workgroup_size(2) @compute");
     auto attrs = p->attribute_list();
     EXPECT_FALSE(p->has_error()) << p->error();
@@ -57,7 +57,7 @@
     EXPECT_EQ(attr_1->As<ast::StageAttribute>()->stage, ast::PipelineStage::kCompute);
 }
 
-TEST_F(WGSLParserTest, AttributeList_Invalid) {
+TEST_F(WGSLParserTest, FunctionAttributeList_Invalid) {
     auto p = parser("@invalid");
     auto attrs = p->attribute_list();
     EXPECT_TRUE(p->has_error());
@@ -66,7 +66,7 @@
     EXPECT_TRUE(attrs.value.IsEmpty());
     EXPECT_EQ(p->error(), R"(1:2: expected attribute
 Did you mean 'invariant'?
-Possible values: 'align', 'binding', 'builtin', 'color', 'compute', 'diagnostic', 'fragment', 'group', 'id', 'index', 'interpolate', 'invariant', 'location', 'must_use', 'size', 'vertex', 'workgroup_size')");
+Possible values: 'align', 'binding', 'blend_src', 'builtin', 'color', 'compute', 'diagnostic', 'fragment', 'group', 'id', 'interpolate', 'invariant', 'location', 'must_use', 'size', 'vertex', 'workgroup_size')");
 }
 
 }  // namespace
diff --git a/src/tint/lang/wgsl/reader/parser/lexer.cc b/src/tint/lang/wgsl/reader/parser/lexer.cc
index a133cb6..1ee2bcb 100644
--- a/src/tint/lang/wgsl/reader/parser/lexer.cc
+++ b/src/tint/lang/wgsl/reader/parser/lexer.cc
@@ -58,7 +58,10 @@
 // programs and being a bit bigger then those need (atan2-const-eval is the outlier here).
 static constexpr size_t kDefaultListSize = 4092;
 
-bool read_blankspace(std::string_view str, size_t i, bool* is_blankspace, size_t* blankspace_size) {
+bool read_blankspace(std::string_view str,
+                     size_t i,
+                     bool* is_blankspace,
+                     uint32_t* blankspace_size) {
     // See https://www.w3.org/TR/WGSL/#blankspace
 
     auto* utf8 = reinterpret_cast<const uint8_t*>(&str[i]);
@@ -75,7 +78,7 @@
 
     if (cp == kSpace || cp == kHTab || cp == kL2R || cp == kR2L) {
         *is_blankspace = true;
-        *blankspace_size = n;
+        *blankspace_size = static_cast<uint32_t>(n);
         return true;
     }
 
@@ -139,15 +142,15 @@
     return file_->content.lines[location_.line - 1];
 }
 
-size_t Lexer::pos() const {
+uint32_t Lexer::pos() const {
     return location_.column - 1;
 }
 
-size_t Lexer::length() const {
-    return line().size();
+uint32_t Lexer::length() const {
+    return static_cast<uint32_t>(line().size());
 }
 
-const char& Lexer::at(size_t pos) const {
+const char& Lexer::at(uint32_t pos) const {
     auto l = line();
     // Unlike for std::string, if pos == l.size(), indexing `l[pos]` is UB for
     // std::string_view.
@@ -158,15 +161,15 @@
     return l[pos];
 }
 
-std::string_view Lexer::substr(size_t offset, size_t count) {
+std::string_view Lexer::substr(uint32_t offset, uint32_t count) {
     return line().substr(offset, count);
 }
 
-void Lexer::advance(size_t offset) {
+void Lexer::advance(uint32_t offset) {
     location_.column += offset;
 }
 
-void Lexer::set_pos(size_t pos) {
+void Lexer::set_pos(uint32_t pos) {
     location_.column = pos + 1;
 }
 
@@ -235,19 +238,18 @@
 bool Lexer::is_digit(char ch) const {
     return std::isdigit(static_cast<unsigned char>(ch));
 }
-
 bool Lexer::is_hex(char ch) const {
     return std::isxdigit(static_cast<unsigned char>(ch));
 }
 
-bool Lexer::matches(size_t pos, std::string_view sub_string) {
+bool Lexer::matches(uint32_t pos, std::string_view sub_string) {
     if (pos >= length()) {
         return false;
     }
-    return substr(pos, sub_string.size()) == sub_string;
+    return substr(pos, static_cast<uint32_t>(sub_string.size())) == sub_string;
 }
 
-bool Lexer::matches(size_t pos, char ch) {
+bool Lexer::matches(uint32_t pos, char ch) {
     if (pos >= length()) {
         return false;
     }
@@ -264,7 +266,7 @@
             }
 
             bool is_blankspace;
-            size_t blankspace_size;
+            uint32_t blankspace_size;
             if (!read_blankspace(line(), pos(), &is_blankspace, &blankspace_size)) {
                 return Token{Token::Type::kError, begin_source(), "invalid UTF-8"};
             }
@@ -383,7 +385,7 @@
     }
 
     // Parse the exponent if one exists
-    std::optional<size_t> exponent_value_position;
+    std::optional<uint32_t> exponent_value_position;
     bool negative_exponent = false;
     if (end < length() && (matches(end, 'e') || matches(end, 'E'))) {
         end++;
@@ -893,8 +895,8 @@
 }
 
 Token Lexer::build_token_from_int_if_possible(Source source,
-                                              size_t start,
-                                              size_t prefix_count,
+                                              uint32_t start,
+                                              uint32_t prefix_count,
                                               int32_t base) {
     const char* start_ptr = &at(start);
     // The call to `from_chars` will return the pointer to just after the last parsed character.
@@ -907,7 +909,7 @@
     int64_t value = 0;
     auto res = std::from_chars(start_ptr, end_ptr, value, base);
     const bool overflow = res.ec != std::errc();
-    advance(static_cast<size_t>(res.ptr - start_ptr) + prefix_count);
+    advance(static_cast<uint32_t>(res.ptr - start_ptr) + prefix_count);
 
     if (matches(pos(), 'u')) {
         if (!overflow && core::CheckedConvert<u32>(AInt(value)) == Success) {
@@ -993,7 +995,7 @@
             return {};
         }
         // Consume start codepoint
-        advance(n);
+        advance(static_cast<uint32_t>(n));
     }
 
     while (!is_eol()) {
@@ -1009,7 +1011,7 @@
         }
 
         // Consume continuing codepoint
-        advance(n);
+        advance(static_cast<uint32_t>(n));
 
         if (pos() - start == 2 && substr(start, 2) == "__") {
             // Identifiers prefixed with two or more underscores are not allowed.
diff --git a/src/tint/lang/wgsl/reader/parser/lexer.h b/src/tint/lang/wgsl/reader/parser/lexer.h
index 6e97551..50cce07 100644
--- a/src/tint/lang/wgsl/reader/parser/lexer.h
+++ b/src/tint/lang/wgsl/reader/parser/lexer.h
@@ -61,8 +61,8 @@
     std::optional<Token> skip_comment();
 
     Token build_token_from_int_if_possible(Source source,
-                                           size_t start,
-                                           size_t prefix_count,
+                                           uint32_t start,
+                                           uint32_t prefix_count,
                                            int32_t base);
 
     std::optional<Token::Type> parse_keyword(std::string_view);
@@ -88,17 +88,17 @@
     /// @returns view of current line
     std::string_view line() const;
     /// @returns position in current line
-    size_t pos() const;
+    uint32_t pos() const;
     /// @returns length of current line
-    size_t length() const;
+    uint32_t length() const;
     /// @returns reference to character at `pos` within current line
-    const char& at(size_t pos) const;
+    const char& at(uint32_t pos) const;
     /// @returns substring view at `offset` within current line of length `count`
-    std::string_view substr(size_t offset, size_t count);
+    std::string_view substr(uint32_t offset, uint32_t count);
     /// advances current position by `offset` within current line
-    void advance(size_t offset = 1);
+    void advance(uint32_t offset = 1);
     /// sets current position to `pos` within current line
-    void set_pos(size_t pos);
+    void set_pos(uint32_t pos);
     /// advances current position to next line
     void advance_line();
     /// @returns true if the end of the input has been reached.
@@ -115,9 +115,9 @@
     /// @returns true if 'ch' is a hexadecimal digit
     bool is_hex(char ch) const;
     /// @returns true if string at `pos` matches `substr`
-    bool matches(size_t pos, std::string_view substr);
+    bool matches(uint32_t pos, std::string_view substr);
     /// @returns true if char at `pos` matches `ch`
-    bool matches(size_t pos, char ch);
+    bool matches(uint32_t pos, char ch);
     /// The source file content
     Source::File const* const file_;
     /// The current location within the input
diff --git a/src/tint/lang/wgsl/reader/parser/parser.cc b/src/tint/lang/wgsl/reader/parser/parser.cc
index 104f37d..b00c274 100644
--- a/src/tint/lang/wgsl/reader/parser/parser.cc
+++ b/src/tint/lang/wgsl/reader/parser/parser.cc
@@ -3099,6 +3099,8 @@
             return create<ast::StructMemberAlignAttribute>(t.source(), args[0]);
         case core::Attribute::kBinding:
             return create<ast::BindingAttribute>(t.source(), args[0]);
+        case core::Attribute::kBlendSrc:
+            return create<ast::BlendSrcAttribute>(t.source(), args[0]);
         case core::Attribute::kBuiltin:
             return create<ast::BuiltinAttribute>(t.source(), args[0]);
         case core::Attribute::kColor:
@@ -3111,8 +3113,6 @@
             return create<ast::GroupAttribute>(t.source(), args[0]);
         case core::Attribute::kId:
             return create<ast::IdAttribute>(t.source(), args[0]);
-        case core::Attribute::kIndex:
-            return create<ast::IndexAttribute>(t.source(), args[0]);
         case core::Attribute::kInterpolate:
             return create<ast::InterpolateAttribute>(t.source(), args[0],
                                                      args.Length() == 2 ? args[1] : nullptr);
diff --git a/src/tint/lang/wgsl/reader/parser/struct_member_attribute_test.cc b/src/tint/lang/wgsl/reader/parser/struct_member_attribute_test.cc
index f4169ac..2c93c6e 100644
--- a/src/tint/lang/wgsl/reader/parser/struct_member_attribute_test.cc
+++ b/src/tint/lang/wgsl/reader/parser/struct_member_attribute_test.cc
@@ -240,8 +240,8 @@
 )");
 }
 
-TEST_F(WGSLParserTest, Attribute_Index) {
-    auto p = parser("index(1)");
+TEST_F(WGSLParserTest, Attribute_BlendSrc) {
+    auto p = parser("blend_src(1)");
     auto attr = p->attribute();
     EXPECT_TRUE(attr.matched);
     EXPECT_FALSE(attr.errored);
@@ -250,9 +250,9 @@
 
     auto* member_attr = attr.value->As<ast::Attribute>();
     ASSERT_NE(member_attr, nullptr);
-    ASSERT_TRUE(member_attr->Is<ast::IndexAttribute>());
+    ASSERT_TRUE(member_attr->Is<ast::BlendSrcAttribute>());
 
-    auto* o = member_attr->As<ast::IndexAttribute>();
+    auto* o = member_attr->As<ast::BlendSrcAttribute>();
     ASSERT_TRUE(o->expr->Is<ast::IntLiteralExpression>());
     EXPECT_EQ(o->expr->As<ast::IntLiteralExpression>()->value, 1);
 }
diff --git a/src/tint/lang/wgsl/reader/parser/variable_attribute_list_test.cc b/src/tint/lang/wgsl/reader/parser/variable_attribute_list_test.cc
index 417ab1f..7b82086 100644
--- a/src/tint/lang/wgsl/reader/parser/variable_attribute_list_test.cc
+++ b/src/tint/lang/wgsl/reader/parser/variable_attribute_list_test.cc
@@ -31,7 +31,7 @@
 namespace tint::wgsl::reader {
 namespace {
 
-TEST_F(WGSLParserTest, AttributeList_Parses) {
+TEST_F(WGSLParserTest, VariableAttributeList_Parses) {
     auto p = parser(R"(@location(4) @builtin(position))");
     auto attrs = p->attribute_list();
     ASSERT_FALSE(p->has_error()) << p->error();
@@ -55,7 +55,7 @@
     ast::CheckIdentifier(attr_1->As<ast::BuiltinAttribute>()->builtin, "position");
 }
 
-TEST_F(WGSLParserTest, AttributeList_Invalid) {
+TEST_F(WGSLParserTest, VariableAttributeList_Invalid) {
     auto p = parser(R"(@invalid)");
     auto attrs = p->attribute_list();
     EXPECT_TRUE(p->has_error());
@@ -64,7 +64,7 @@
     EXPECT_TRUE(attrs.value.IsEmpty());
     EXPECT_EQ(p->error(), R"(1:2: expected attribute
 Did you mean 'invariant'?
-Possible values: 'align', 'binding', 'builtin', 'color', 'compute', 'diagnostic', 'fragment', 'group', 'id', 'index', 'interpolate', 'invariant', 'location', 'must_use', 'size', 'vertex', 'workgroup_size')");
+Possible values: 'align', 'binding', 'blend_src', 'builtin', 'color', 'compute', 'diagnostic', 'fragment', 'group', 'id', 'interpolate', 'invariant', 'location', 'must_use', 'size', 'vertex', 'workgroup_size')");
 }
 
 }  // namespace
diff --git a/src/tint/lang/wgsl/reader/reader.cc b/src/tint/lang/wgsl/reader/reader.cc
index 3ddc2ae..77289d1 100644
--- a/src/tint/lang/wgsl/reader/reader.cc
+++ b/src/tint/lang/wgsl/reader/reader.cc
@@ -27,6 +27,7 @@
 
 #include "src/tint/lang/wgsl/reader/reader.h"
 
+#include <limits>
 #include <utility>
 
 #include "src/tint/lang/wgsl/reader/lower/lower.h"
@@ -37,6 +38,13 @@
 namespace tint::wgsl::reader {
 
 Program Parse(const Source::File* file, const Options& options) {
+    if (TINT_UNLIKELY(file->content.data.size() >
+                      static_cast<size_t>(std::numeric_limits<uint32_t>::max()))) {
+        ProgramBuilder b;
+        b.Diagnostics().add_error(tint::diag::System::Reader,
+                                  "WGSL source must be 0xffffffff bytes or fewer");
+        return Program(std::move(b));
+    }
     Parser parser(file);
     parser.Parse();
     return resolver::Resolve(parser.builder(), options.allowed_features);
diff --git a/src/tint/lang/wgsl/resolver/alias_analysis_test.cc b/src/tint/lang/wgsl/resolver/alias_analysis_test.cc
index 0d9e0bb..e7bd78f 100644
--- a/src/tint/lang/wgsl/resolver/alias_analysis_test.cc
+++ b/src/tint/lang/wgsl/resolver/alias_analysis_test.cc
@@ -50,8 +50,8 @@
 //   target(&v1, aliased ? &v1 : &v2);
 // }
 struct TwoPointerConfig {
-    core::AddressSpace address_space;  // The address space for the pointers.
-    bool aliased;                      // Whether the pointers alias or not.
+    const core::AddressSpace address_space;  // The address space for the pointers.
+    const bool aliased;                      // Whether the pointers alias or not.
 };
 class TwoPointers : public ResolverTestWithParam<TwoPointerConfig> {
   protected:
@@ -179,7 +179,7 @@
     // f1(p1);
     // f2(p2);
     Func("f1",
-         Vector<const ast::Parameter*, 4>{
+         Vector{
              Param("p1", ty.ptr<i32>(GetParam().address_space)),
          },
          ty.void_(),
@@ -187,7 +187,7 @@
              Assign(Phony(), Deref("p1")),
          });
     Func("f2",
-         Vector<const ast::Parameter*, 4>{
+         Vector{
              Param("p2", ty.ptr<i32>(GetParam().address_space)),
          },
          ty.void_(),
@@ -240,7 +240,7 @@
 
     void Run(Vector<const ast::Statement*, 4>&& body, const char* err = nullptr) {
         Func("target",
-             Vector<const ast::Parameter*, 4>{
+             Vector{
                  Param("p1", ty.ptr<i32>(core::AddressSpace::kPrivate)),
              },
              ty.void_(), std::move(body));
@@ -309,7 +309,7 @@
     //
     // f1(p1);
     Func("f2",
-         Vector<const ast::Parameter*, 4>{
+         Vector{
              Param("p1", ty.ptr<i32>(core::AddressSpace::kPrivate)),
          },
          ty.void_(),
@@ -317,7 +317,7 @@
              Assign(Deref("p1"), 42_a),
          });
     Func("f1",
-         Vector<const ast::Parameter*, 4>{
+         Vector{
              Param("p1", ty.ptr<i32>(core::AddressSpace::kPrivate)),
          },
          ty.void_(),
@@ -344,7 +344,7 @@
     //
     // f1(p1);
     Func("f2",
-         Vector<const ast::Parameter*, 4>{
+         Vector{
              Param("p1", ty.ptr<i32>(core::AddressSpace::kPrivate)),
          },
          ty.void_(),
@@ -353,7 +353,7 @@
              Assign(Expr(Source{{56, 78}}, "global_1"), 42_a),
          });
     Func("f1",
-         Vector<const ast::Parameter*, 4>{
+         Vector{
              Param("p1", ty.ptr<i32>(core::AddressSpace::kPrivate)),
          },
          ty.void_(),
@@ -379,7 +379,7 @@
     //
     // f1(p1);
     Func("f2",
-         Vector<const ast::Parameter*, 4>{
+         Vector{
              Param("p1", ty.ptr<i32>(core::AddressSpace::kPrivate)),
          },
          ty.void_(),
@@ -387,7 +387,7 @@
              Assign(Phony(), Deref("p1")),
          });
     Func("f1",
-         Vector<const ast::Parameter*, 4>{
+         Vector{
              Param("p1", ty.ptr<i32>(core::AddressSpace::kPrivate)),
          },
          ty.void_(),
@@ -939,5 +939,433 @@
     EXPECT_TRUE(r()->Resolve()) << r()->error();
 }
 
+////////////////////////////////////////////////////////////////////////////////
+// Atomics
+////////////////////////////////////////////////////////////////////////////////
+class AtomicPointers
+    : public ResolverTestWithParam<
+          std::tuple<wgsl::BuiltinFn, wgsl::BuiltinFn, core::AddressSpace, bool /* aliased */>> {
+  protected:
+    static constexpr std::string_view kPass = "<PASS>";
+
+    ast::Type Ptr() {
+        auto address_space = std::get<2>(GetParam());
+        if (address_space == storage) {
+            return ty.ptr<storage, atomic<i32>, read_write>();
+        } else {
+            return ty.ptr<atomic<i32>>(address_space);
+        }
+    }
+
+    void SetUp() override {
+        auto address_space = std::get<2>(GetParam());
+        if (address_space == storage) {
+            GlobalVar("v1", address_space, read_write, ty.Of<atomic<i32>>(),  //
+                      Binding(0_a), Group(0_a));
+            GlobalVar("v2", address_space, read_write, ty.Of<atomic<i32>>(),  //
+                      Binding(1_a), Group(0_a));
+        } else {
+            GlobalVar("v1", address_space, ty.Of<atomic<i32>>());
+            GlobalVar("v2", address_space, ty.Of<atomic<i32>>());
+        }
+    }
+
+    bool IsWrite(wgsl::BuiltinFn fn) const {
+        switch (fn) {
+            case wgsl::BuiltinFn::kAtomicStore:
+            case wgsl::BuiltinFn::kAtomicAdd:
+            case wgsl::BuiltinFn::kAtomicSub:
+            case wgsl::BuiltinFn::kAtomicMax:
+            case wgsl::BuiltinFn::kAtomicMin:
+            case wgsl::BuiltinFn::kAtomicAnd:
+            case wgsl::BuiltinFn::kAtomicOr:
+            case wgsl::BuiltinFn::kAtomicXor:
+            case wgsl::BuiltinFn::kAtomicExchange:
+            case wgsl::BuiltinFn::kAtomicCompareExchangeWeak:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    bool ShouldPass() const {
+        auto [builtin_a, builtin_b, space, aliased] = GetParam();
+        bool fail = aliased && (IsWrite(builtin_a) || IsWrite(builtin_b));
+        return !fail;
+    }
+
+    const ast::Statement* CallBuiltin(wgsl::BuiltinFn fn, std::string_view ptr) {
+        switch (fn) {
+            case wgsl::BuiltinFn::kAtomicLoad:
+                return CallStmt(Call(fn, ptr));
+            case wgsl::BuiltinFn::kAtomicStore:
+            case wgsl::BuiltinFn::kAtomicAdd:
+            case wgsl::BuiltinFn::kAtomicSub:
+            case wgsl::BuiltinFn::kAtomicMax:
+            case wgsl::BuiltinFn::kAtomicMin:
+            case wgsl::BuiltinFn::kAtomicAnd:
+            case wgsl::BuiltinFn::kAtomicOr:
+            case wgsl::BuiltinFn::kAtomicXor:
+            case wgsl::BuiltinFn::kAtomicExchange:
+                return CallStmt(Call(fn, ptr, 42_a));
+            case wgsl::BuiltinFn::kAtomicCompareExchangeWeak:
+                return CallStmt(Call(fn, ptr, 10_a, 42_a));
+            default:
+                TINT_UNIMPLEMENTED() << fn;
+                return nullptr;
+        }
+    }
+
+    std::string Run() {
+        if (r()->Resolve()) {
+            return std::string(kPass);
+        }
+        return r()->error();
+    }
+};
+
+TEST_P(AtomicPointers, CallDirect) {
+    // var<ADDRESS_SPACE> v1 : atomic<i32>;
+    // var<ADDRESS_SPACE> v2 : atomic<i32>;
+    //
+    // fn caller() {
+    //    callee(&v1, aliased ? &v1 : &v2);
+    // }
+    //
+    // fn callee(p1 : PTR, p2 : PTR) {
+    //   <builtin-a>(p1);
+    //   <builtin-b>(p2);
+    // }
+    auto [builtin_a, builtin_b, space, aliased] = GetParam();
+
+    Func("caller", tint::Empty, ty.void_(),
+         Vector{
+             CallStmt(Call("callee",  //
+                           AddressOf(Source{{12, 34}}, "v1"),
+                           AddressOf(Source{{56, 78}}, aliased ? "v1" : "v2"))),
+         });
+
+    Func("callee", Vector{Param("p1", Ptr()), Param("p2", Ptr())}, ty.void_(),
+         Vector{
+             CallBuiltin(builtin_a, "p1"),
+             CallBuiltin(builtin_b, "p2"),
+         });
+
+    EXPECT_EQ(Run(), ShouldPass() ? kPass : R"(56:78 error: invalid aliased pointer argument
+12:34 note: aliases with another argument passed here)");
+}
+
+TEST_P(AtomicPointers, CallThroughChain) {
+    // var<ADDRESS_SPACE> v1 : atomic<i32>;
+    // var<ADDRESS_SPACE> v2 : atomic<i32>;
+    //
+    // fn caller() {
+    //    callee(&v1, aliased ? &v1 : &v2);
+    // }
+    //
+    // fn f2(p1 : PTR, p2 : PTR) {
+    //    f1(p1, p2);
+    // }
+    //
+    // fn f1(p1 : PTR, p2 : PTR) {
+    //    callee(p1, p2);
+    // }
+    //
+    // fn callee(p1 : PTR, p2 : PTR) {
+    //   <builtin-a>(p1);
+    //   <builtin-b>(p2);
+    // }
+    auto [builtin_a, builtin_b, space, aliased] = GetParam();
+
+    Func("caller", tint::Empty, ty.void_(),
+         Vector{
+             CallStmt(Call("callee",  //
+                           AddressOf(Source{{12, 34}}, "v1"),
+                           AddressOf(Source{{56, 78}}, aliased ? "v1" : "v2"))),
+         });
+
+    Func("f2", Vector{Param("p1", Ptr()), Param("p2", Ptr())}, ty.void_(),
+         Vector{
+             CallStmt(Call("f1", "p1", "p2")),
+         });
+
+    Func("f1", Vector{Param("p1", Ptr()), Param("p2", Ptr())}, ty.void_(),
+         Vector{
+             CallStmt(Call("callee", "p1", "p2")),
+         });
+
+    Func("callee", Vector{Param("p1", Ptr()), Param("p2", Ptr())}, ty.void_(),
+         Vector{
+             CallBuiltin(builtin_a, "p1"),
+             CallBuiltin(builtin_b, "p2"),
+         });
+
+    EXPECT_EQ(Run(), ShouldPass() ? kPass : R"(56:78 error: invalid aliased pointer argument
+12:34 note: aliases with another argument passed here)");
+}
+
+TEST_P(AtomicPointers, ReadWriteAcrossDifferentFunctions) {
+    // var<ADDRESS_SPACE> v1 : atomic<i32>;
+    // var<ADDRESS_SPACE> v2 : atomic<i32>;
+    //
+    // fn caller() {
+    //   f(&v1, aliased ? &v1 : &v2);
+    // }
+    //
+    // fn f(p1 : PTR, p2 : PTR) {
+    //    f1(p1);
+    //    f2(p2);
+    // }
+    //
+    // fn f1(p : PTR) {
+    //   <builtin-a>(p);
+    // }
+    //
+    // fn f2(p : PTR) {
+    //   <builtin-b>(p);
+    // }
+    auto [builtin_a, builtin_b, space, aliased] = GetParam();
+
+    Func("caller", tint::Empty, ty.void_(),
+         Vector{
+             CallStmt(Call("f",  //
+                           AddressOf(Source{{12, 34}}, "v1"),
+                           AddressOf(Source{{56, 78}}, aliased ? "v1" : "v2"))),
+         });
+
+    Func("f", Vector{Param("p1", Ptr()), Param("p2", Ptr())}, ty.void_(),
+         Vector{
+             CallStmt(Call("f1", "p1")),
+             CallStmt(Call("f2", "p2")),
+         });
+
+    Func("f1", Vector{Param("p", Ptr())}, ty.void_(),
+         Vector{
+             CallBuiltin(builtin_a, "p"),
+         });
+
+    Func("f2", Vector{Param("p", Ptr())}, ty.void_(),
+         Vector{
+             CallBuiltin(builtin_b, "p"),
+         });
+
+    EXPECT_EQ(Run(), ShouldPass() ? kPass : R"(56:78 error: invalid aliased pointer argument
+12:34 note: aliases with another argument passed here)");
+}
+
+std::array kAtomicFns{
+    wgsl::BuiltinFn::kAtomicLoad,
+    wgsl::BuiltinFn::kAtomicStore,
+    wgsl::BuiltinFn::kAtomicAdd,
+    wgsl::BuiltinFn::kAtomicSub,
+    wgsl::BuiltinFn::kAtomicMax,
+    wgsl::BuiltinFn::kAtomicMin,
+    wgsl::BuiltinFn::kAtomicAnd,
+    wgsl::BuiltinFn::kAtomicOr,
+    wgsl::BuiltinFn::kAtomicXor,
+    wgsl::BuiltinFn::kAtomicExchange,
+    wgsl::BuiltinFn::kAtomicCompareExchangeWeak,
+};
+
+INSTANTIATE_TEST_SUITE_P(ResolverAliasAnalysisTest,
+                         AtomicPointers,
+                         ::testing::Combine(::testing::ValuesIn(kAtomicFns),
+                                            ::testing::ValuesIn(kAtomicFns),
+                                            ::testing::Values(core::AddressSpace::kWorkgroup,
+                                                              core::AddressSpace::kStorage),
+                                            ::testing::Values(true, false)));
+
+////////////////////////////////////////////////////////////////////////////////
+// WorkgroupUniformLoad
+////////////////////////////////////////////////////////////////////////////////
+enum class WorkgroupUniformLoadAction {
+    kRead,
+    kWrite,
+    kWorkgroupUniformLoad,
+};
+
+std::array kWorkgroupUniformLoadActions{
+    WorkgroupUniformLoadAction::kRead,
+    WorkgroupUniformLoadAction::kWrite,
+    WorkgroupUniformLoadAction::kWorkgroupUniformLoad,
+};
+
+class WorkgroupUniformLoad
+    : public ResolverTestWithParam<
+          std::tuple<WorkgroupUniformLoadAction, WorkgroupUniformLoadAction, bool>> {
+  protected:
+    static constexpr std::string_view kPass = "<PASS>";
+
+    void SetUp() override {
+        GlobalVar("v1", workgroup, ty.i32());
+        GlobalVar("v2", workgroup, ty.i32());
+    }
+
+    const ast::Statement* Do(WorkgroupUniformLoadAction action, std::string_view ptr) {
+        switch (action) {
+            case WorkgroupUniformLoadAction::kRead:
+                return Assign(Phony(), Deref(ptr));
+            case WorkgroupUniformLoadAction::kWrite:
+                return Assign(Deref(ptr), 42_a);
+            case WorkgroupUniformLoadAction::kWorkgroupUniformLoad:
+                return Assign(Phony(), Call(wgsl::BuiltinFn::kWorkgroupUniformLoad, ptr));
+        }
+        return nullptr;
+    }
+
+    bool IsWrite(WorkgroupUniformLoadAction action) const {
+        return action == WorkgroupUniformLoadAction::kWrite;
+    }
+
+    bool ShouldPass() const {
+        auto [action_a, action_b, aliased] = GetParam();
+        bool fail = aliased && (IsWrite(action_a) || IsWrite(action_b));
+        return !fail;
+    }
+
+    std::string Run() {
+        if (r()->Resolve()) {
+            return std::string(kPass);
+        }
+        return r()->error();
+    }
+};
+
+TEST_P(WorkgroupUniformLoad, CallDirect) {
+    // var<workgroup> v1 : i32;
+    // var<workgroup> v2 : i32;
+    //
+    // fn caller() {
+    //    callee(&v1, aliased ? &v1 : &v2);
+    // }
+    //
+    // fn callee(p1 : PTR, p2 : PTR) {
+    //   <action-a>(p1);
+    //   <action-b>(p2);
+    // }
+    auto [action_a, action_b, aliased] = GetParam();
+
+    Func("caller", tint::Empty, ty.void_(),
+         Vector{
+             CallStmt(Call("callee",  //
+                           AddressOf(Source{{12, 34}}, "v1"),
+                           AddressOf(Source{{56, 78}}, aliased ? "v1" : "v2"))),
+         });
+
+    Func("callee",
+         Vector{Param("p1", ty.ptr<workgroup, i32>()), Param("p2", ty.ptr<workgroup, i32>())},
+         ty.void_(),
+         Vector{
+             Do(action_a, "p1"),
+             Do(action_b, "p2"),
+         });
+
+    EXPECT_EQ(Run(), ShouldPass() ? kPass : R"(56:78 error: invalid aliased pointer argument
+12:34 note: aliases with another argument passed here)");
+}
+
+TEST_P(WorkgroupUniformLoad, CallThroughChain) {
+    // var<workgroup> v1 : i32;
+    // var<workgroup> v2 : i32;
+    //
+    // fn caller() {
+    //    callee(&v1, aliased ? &v1 : &v2);
+    // }
+    //
+    // fn f2(p1 : PTR, p2 : PTR) {
+    //    f1(p1, p2);
+    // }
+    //
+    // fn f1(p1 : PTR, p2 : PTR) {
+    //    callee(p1, p2);
+    // }
+    //
+    // fn callee(p1 : PTR, p2 : PTR) {
+    //   <action-a>(p1);
+    //   <action-b>(p2);
+    // }
+    auto [action_a, action_b, aliased] = GetParam();
+
+    Func("caller", tint::Empty, ty.void_(),
+         Vector{
+             CallStmt(Call("callee",  //
+                           AddressOf(Source{{12, 34}}, "v1"),
+                           AddressOf(Source{{56, 78}}, aliased ? "v1" : "v2"))),
+         });
+
+    Func("f2", Vector{Param("p1", ty.ptr<workgroup, i32>()), Param("p2", ty.ptr<workgroup, i32>())},
+         ty.void_(),
+         Vector{
+             CallStmt(Call("f1", "p1", "p2")),
+         });
+
+    Func("f1", Vector{Param("p1", ty.ptr<workgroup, i32>()), Param("p2", ty.ptr<workgroup, i32>())},
+         ty.void_(),
+         Vector{
+             CallStmt(Call("callee", "p1", "p2")),
+         });
+
+    Func("callee",
+         Vector{Param("p1", ty.ptr<workgroup, i32>()), Param("p2", ty.ptr<workgroup, i32>())},
+         ty.void_(),
+         Vector{
+             Do(action_a, "p1"),
+             Do(action_b, "p2"),
+         });
+
+    EXPECT_EQ(Run(), ShouldPass() ? kPass : R"(56:78 error: invalid aliased pointer argument
+12:34 note: aliases with another argument passed here)");
+}
+
+TEST_P(WorkgroupUniformLoad, ReadWriteAcrossDifferentFunctions) {
+    // var<workgroup> v1 : i32;
+    // var<workgroup> v2 : i32;
+    //
+    // fn caller() {
+    //   f(&v1, aliased ? &v1 : &v2);
+    // }
+    //
+    // fn f(p1 : PTR, p2 : PTR) {
+    //    f1(p1);
+    //    f2(p2);
+    // }
+    //
+    // fn f1(p : PTR) {
+    //   <action-a>(p);
+    // }
+    //
+    // fn f2(p : PTR) {
+    //   <action-b>(p);
+    // }
+    auto [action_a, action_b, aliased] = GetParam();
+
+    Func("caller", tint::Empty, ty.void_(),
+         Vector{
+             CallStmt(Call("f",  //
+                           AddressOf(Source{{12, 34}}, "v1"),
+                           AddressOf(Source{{56, 78}}, aliased ? "v1" : "v2"))),
+         });
+
+    Func("f", Vector{Param("p1", ty.ptr<workgroup, i32>()), Param("p2", ty.ptr<workgroup, i32>())},
+         ty.void_(),
+         Vector{
+             CallStmt(Call("f1", "p1")),
+             CallStmt(Call("f2", "p2")),
+         });
+
+    Func("f1", Vector{Param("p", ty.ptr<workgroup, i32>())}, ty.void_(), Vector{Do(action_a, "p")});
+
+    Func("f2", Vector{Param("p", ty.ptr<workgroup, i32>())}, ty.void_(), Vector{Do(action_b, "p")});
+
+    EXPECT_EQ(Run(), ShouldPass() ? kPass : R"(56:78 error: invalid aliased pointer argument
+12:34 note: aliases with another argument passed here)");
+}
+
+INSTANTIATE_TEST_SUITE_P(ResolverAliasAnalysisTest,
+                         WorkgroupUniformLoad,
+                         ::testing::Combine(::testing::ValuesIn(kWorkgroupUniformLoadActions),
+                                            ::testing::ValuesIn(kWorkgroupUniformLoadActions),
+                                            ::testing::Values(true, false)));
+
 }  // namespace
 }  // namespace tint::resolver
diff --git a/src/tint/lang/wgsl/resolver/attribute_validation_test.cc b/src/tint/lang/wgsl/resolver/attribute_validation_test.cc
index c359dd7..33cccae 100644
--- a/src/tint/lang/wgsl/resolver/attribute_validation_test.cc
+++ b/src/tint/lang/wgsl/resolver/attribute_validation_test.cc
@@ -59,12 +59,12 @@
 enum class AttributeKind {
     kAlign,
     kBinding,
+    kBlendSrc,
     kBuiltinPosition,
     kColor,
     kDiagnostic,
     kGroup,
     kId,
-    kIndex,
     kInterpolate,
     kInvariant,
     kLocation,
@@ -81,6 +81,8 @@
             return o << "@align";
         case AttributeKind::kBinding:
             return o << "@binding";
+        case AttributeKind::kBlendSrc:
+            return o << "@blend_src";
         case AttributeKind::kBuiltinPosition:
             return o << "@builtin(position)";
         case AttributeKind::kColor:
@@ -91,8 +93,6 @@
             return o << "@group";
         case AttributeKind::kId:
             return o << "@id";
-        case AttributeKind::kIndex:
-            return o << "@index";
         case AttributeKind::kInterpolate:
             return o << "@interpolate";
         case AttributeKind::kInvariant:
@@ -143,6 +143,10 @@
                 "1:2 error: @binding is not valid for " + thing,
             },
             TestParams{
+                {AttributeKind::kBlendSrc},
+                "1:2 error: @blend_src is not valid for " + thing,
+            },
+            TestParams{
                 {AttributeKind::kBuiltinPosition},
                 "1:2 error: @builtin is not valid for " + thing,
             },
@@ -163,10 +167,6 @@
                 "1:2 error: @id is not valid for " + thing,
             },
             TestParams{
-                {AttributeKind::kIndex},
-                "1:2 error: @index is not valid for " + thing,
-            },
-            TestParams{
                 {AttributeKind::kInterpolate},
                 "1:2 error: @interpolate is not valid for " + thing,
             },
@@ -231,8 +231,8 @@
             return builder.Group(source, 1_a);
         case AttributeKind::kId:
             return builder.Id(source, 0_a);
-        case AttributeKind::kIndex:
-            return builder.Index(source, 0_a);
+        case AttributeKind::kBlendSrc:
+            return builder.BlendSrc(source, 0_a);
         case AttributeKind::kInterpolate:
             return builder.Interpolate(source, core::InterpolationType::kLinear,
                                        core::InterpolationSampling::kCenter);
@@ -263,7 +263,7 @@
             case AttributeKind::kColor:
                 Enable(wgsl::Extension::kChromiumExperimentalFramebufferFetch);
                 break;
-            case AttributeKind::kIndex:
+            case AttributeKind::kBlendSrc:
                 Enable(wgsl::Extension::kChromiumInternalDualSourceBlending);
                 break;
             default:
@@ -323,6 +323,10 @@
             R"(1:2 error: @binding is not valid for functions)",
         },
         TestParams{
+            {AttributeKind::kBlendSrc},
+            R"(1:2 error: @blend_src is not valid for functions)",
+        },
+        TestParams{
             {AttributeKind::kBuiltinPosition},
             R"(1:2 error: @builtin is not valid for functions)",
         },
@@ -343,10 +347,6 @@
             R"(1:2 error: @id is not valid for functions)",
         },
         TestParams{
-            {AttributeKind::kIndex},
-            R"(1:2 error: @index is not valid for functions)",
-        },
-        TestParams{
             {AttributeKind::kInterpolate},
             R"(1:2 error: @interpolate is not valid for functions)",
         },
@@ -407,6 +407,10 @@
                                  R"(1:2 error: @binding is not valid for functions)",
                              },
                              TestParams{
+                                 {AttributeKind::kBlendSrc},
+                                 R"(1:2 error: @blend_src is not valid for functions)",
+                             },
+                             TestParams{
                                  {AttributeKind::kBuiltinPosition},
                                  R"(1:2 error: @builtin is not valid for functions)",
                              },
@@ -427,10 +431,6 @@
                                  R"(1:2 error: @id is not valid for functions)",
                              },
                              TestParams{
-                                 {AttributeKind::kIndex},
-                                 R"(1:2 error: @index is not valid for functions)",
-                             },
-                             TestParams{
                                  {AttributeKind::kInterpolate},
                                  R"(1:2 error: @interpolate is not valid for functions)",
                              },
@@ -498,6 +498,10 @@
             R"(1:2 error: @binding is not valid for function parameters)",
         },
         TestParams{
+            {AttributeKind::kBlendSrc},
+            R"(1:2 error: @blend_src is not valid for function parameters)",
+        },
+        TestParams{
             {AttributeKind::kBuiltinPosition},
             R"(1:2 error: @builtin is not valid for non-entry point function parameters)",
         },
@@ -518,10 +522,6 @@
             R"(1:2 error: @id is not valid for function parameters)",
         },
         TestParams{
-            {AttributeKind::kIndex},
-            R"(1:2 error: @index is not valid for function parameters)",
-        },
-        TestParams{
             {AttributeKind::kInterpolate},
             R"(1:2 error: @interpolate is not valid for non-entry point function parameters)",
         },
@@ -583,6 +583,10 @@
             R"(1:2 error: @binding is not valid for non-entry point function return types)",
         },
         TestParams{
+            {AttributeKind::kBlendSrc},
+            R"(1:2 error: @blend_src is not valid for non-entry point function return types)",
+        },
+        TestParams{
             {AttributeKind::kBuiltinPosition},
             R"(1:2 error: @builtin is not valid for non-entry point function return types)",
         },
@@ -603,10 +607,6 @@
             R"(1:2 error: @id is not valid for non-entry point function return types)",
         },
         TestParams{
-            {AttributeKind::kIndex},
-            R"(1:2 error: @index is not valid for non-entry point function return types)",
-        },
-        TestParams{
             {AttributeKind::kInterpolate},
             R"(1:2 error: @interpolate is not valid for non-entry point function return types)",
         },
@@ -673,6 +673,10 @@
             R"(1:2 error: @binding is not valid for function parameters)",
         },
         TestParams{
+            {AttributeKind::kBlendSrc},
+            R"(1:2 error: @blend_src is not valid for function parameters)",
+        },
+        TestParams{
             {AttributeKind::kBuiltinPosition},
             R"(1:2 error: @builtin(position) cannot be used for compute shader input)",
         },
@@ -693,10 +697,6 @@
             R"(1:2 error: @id is not valid for function parameters)",
         },
         TestParams{
-            {AttributeKind::kIndex},
-            R"(1:2 error: @index is not valid for function parameters)",
-        },
-        TestParams{
             {AttributeKind::kInterpolate},
             R"(1:2 error: @interpolate cannot be used by compute shaders)",
         },
@@ -757,6 +757,10 @@
             R"(1:2 error: @binding is not valid for function parameters)",
         },
         TestParams{
+            {AttributeKind::kBlendSrc},
+            R"(1:2 error: @blend_src is not valid for function parameters)",
+        },
+        TestParams{
             {AttributeKind::kBuiltinPosition},
             Pass,
         },
@@ -782,10 +786,6 @@
             R"(1:2 error: @id is not valid for function parameters)",
         },
         TestParams{
-            {AttributeKind::kIndex},
-            R"(1:2 error: @index is not valid for function parameters)",
-        },
-        TestParams{
             {AttributeKind::kInterpolate},
             R"(9:9 error: missing entry point IO attribute on parameter)",
         },
@@ -865,6 +865,10 @@
             R"(1:2 error: @binding is not valid for function parameters)",
         },
         TestParams{
+            {AttributeKind::kBlendSrc},
+            R"(1:2 error: @blend_src is not valid for function parameters)",
+        },
+        TestParams{
             {AttributeKind::kBuiltinPosition},
             R"(1:2 error: @builtin(position) cannot be used for vertex shader input)",
         },
@@ -885,10 +889,6 @@
             R"(1:2 error: @id is not valid for function parameters)",
         },
         TestParams{
-            {AttributeKind::kIndex},
-            R"(1:2 error: @index is not valid for function parameters)",
-        },
-        TestParams{
             {AttributeKind::kInterpolate},
             R"(9:9 error: missing entry point IO attribute on parameter)",
         },
@@ -970,6 +970,10 @@
             R"(1:2 error: @binding is not valid for entry point return types)",
         },
         TestParams{
+            {AttributeKind::kBlendSrc},
+            R"(1:2 error: @blend_src can only be used for fragment shader output)",
+        },
+        TestParams{
             {AttributeKind::kBuiltinPosition},
             R"(1:2 error: @builtin(position) cannot be used for compute shader output)",
         },
@@ -990,10 +994,6 @@
             R"(1:2 error: @id is not valid for entry point return types)",
         },
         TestParams{
-            {AttributeKind::kIndex},
-            R"(1:2 error: @index can only be used for fragment shader output)",
-        },
-        TestParams{
             {AttributeKind::kInterpolate},
             R"(1:2 error: @interpolate cannot be used by compute shaders)",
         },
@@ -1056,6 +1056,14 @@
             R"(1:2 error: @binding is not valid for entry point return types)",
         },
         TestParams{
+            {AttributeKind::kBlendSrc},
+            R"(9:9 error: missing entry point IO attribute on return type)",
+        },
+        TestParams{
+            {AttributeKind::kBlendSrc, AttributeKind::kLocation},
+            Pass,
+        },
+        TestParams{
             {AttributeKind::kBuiltinPosition},
             R"(1:2 error: @builtin(position) cannot be used for fragment shader output)",
         },
@@ -1076,14 +1084,6 @@
             R"(1:2 error: @id is not valid for entry point return types)",
         },
         TestParams{
-            {AttributeKind::kIndex},
-            R"(9:9 error: missing entry point IO attribute on return type)",
-        },
-        TestParams{
-            {AttributeKind::kIndex, AttributeKind::kLocation},
-            Pass,
-        },
-        TestParams{
             {AttributeKind::kInterpolate},
             R"(9:9 error: missing entry point IO attribute on return type)",
         },
@@ -1132,7 +1132,7 @@
             R"(1:2 error: @binding is not valid for entry point return types)",
         },
         TestParams{
-            {AttributeKind::kIndex, AttributeKind::kLocation},
+            {AttributeKind::kBlendSrc, AttributeKind::kLocation},
             Pass,
         }));
 
@@ -1168,6 +1168,10 @@
             R"(1:2 error: @binding is not valid for entry point return types)",
         },
         TestParams{
+            {AttributeKind::kBlendSrc},
+            R"(1:2 error: @blend_src can only be used for fragment shader output)",
+        },
+        TestParams{
             {AttributeKind::kBuiltinPosition},
             Pass,
         },
@@ -1188,10 +1192,6 @@
             R"(1:2 error: @id is not valid for entry point return types)",
         },
         TestParams{
-            {AttributeKind::kIndex},
-            R"(1:2 error: @index can only be used for fragment shader output)",
-        },
-        TestParams{
             {AttributeKind::kInterpolate},
             R"(1:2 error: @interpolate can only be used with @location)",
         },
@@ -1293,6 +1293,10 @@
             R"(1:2 error: @binding is not valid for struct declarations)",
         },
         TestParams{
+            {AttributeKind::kBlendSrc},
+            R"(1:2 error: @blend_src is not valid for struct declarations)",
+        },
+        TestParams{
             {AttributeKind::kBuiltinPosition},
             R"(1:2 error: @builtin is not valid for struct declarations)",
         },
@@ -1313,10 +1317,6 @@
             R"(1:2 error: @id is not valid for struct declarations)",
         },
         TestParams{
-            {AttributeKind::kIndex},
-            R"(1:2 error: @index is not valid for struct declarations)",
-        },
-        TestParams{
             {AttributeKind::kInterpolate},
             R"(1:2 error: @interpolate is not valid for struct declarations)",
         },
@@ -1376,6 +1376,10 @@
                                  R"(1:2 error: @binding is not valid for struct members)",
                              },
                              TestParams{
+                                 {AttributeKind::kBlendSrc},
+                                 R"(1:2 error: @blend_src can only be used with @location(0))",
+                             },
+                             TestParams{
                                  {AttributeKind::kBuiltinPosition},
                                  Pass,
                              },
@@ -1396,10 +1400,6 @@
                                  R"(1:2 error: @id is not valid for struct members)",
                              },
                              TestParams{
-                                 {AttributeKind::kIndex},
-                                 R"(1:2 error: @index can only be used with @location(0))",
-                             },
-                             TestParams{
                                  {AttributeKind::kInterpolate},
                                  R"(1:2 error: @interpolate can only be used with @location)",
                              },
@@ -1648,6 +1648,10 @@
                                  R"(1:2 error: @binding is not valid for array types)",
                              },
                              TestParams{
+                                 {AttributeKind::kBlendSrc},
+                                 R"(1:2 error: @blend_src is not valid for array types)",
+                             },
+                             TestParams{
                                  {AttributeKind::kBuiltinPosition},
                                  R"(1:2 error: @builtin is not valid for array types)",
                              },
@@ -1664,10 +1668,6 @@
                                  R"(1:2 error: @id is not valid for array types)",
                              },
                              TestParams{
-                                 {AttributeKind::kIndex},
-                                 R"(1:2 error: @index is not valid for array types)",
-                             },
-                             TestParams{
                                  {AttributeKind::kInterpolate},
                                  R"(1:2 error: @interpolate is not valid for array types)",
                              },
@@ -1739,6 +1739,10 @@
             R"(9:9 error: resource variables require @group and @binding attributes)",
         },
         TestParams{
+            {AttributeKind::kBlendSrc},
+            R"(1:2 error: @blend_src is not valid for module-scope 'var')",
+        },
+        TestParams{
             {AttributeKind::kBuiltinPosition},
             R"(1:2 error: @builtin is not valid for module-scope 'var')",
         },
@@ -1755,10 +1759,6 @@
             R"(1:2 error: @id is not valid for module-scope 'var')",
         },
         TestParams{
-            {AttributeKind::kIndex},
-            R"(1:2 error: @index is not valid for module-scope 'var')",
-        },
-        TestParams{
             {AttributeKind::kInterpolate},
             R"(1:2 error: @interpolate is not valid for module-scope 'var')",
         },
@@ -1843,6 +1843,10 @@
             R"(1:2 error: @binding is not valid for 'const' declaration)",
         },
         TestParams{
+            {AttributeKind::kBlendSrc},
+            R"(1:2 error: @blend_src is not valid for 'const' declaration)",
+        },
+        TestParams{
             {AttributeKind::kBuiltinPosition},
             R"(1:2 error: @builtin is not valid for 'const' declaration)",
         },
@@ -1859,10 +1863,6 @@
             R"(1:2 error: @id is not valid for 'const' declaration)",
         },
         TestParams{
-            {AttributeKind::kIndex},
-            R"(1:2 error: @index is not valid for 'const' declaration)",
-        },
-        TestParams{
             {AttributeKind::kInterpolate},
             R"(1:2 error: @interpolate is not valid for 'const' declaration)",
         },
@@ -1924,6 +1924,10 @@
             R"(1:2 error: @binding is not valid for 'override' declaration)",
         },
         TestParams{
+            {AttributeKind::kBlendSrc},
+            R"(1:2 error: @blend_src is not valid for 'override' declaration)",
+        },
+        TestParams{
             {AttributeKind::kBuiltinPosition},
             R"(1:2 error: @builtin is not valid for 'override' declaration)",
         },
@@ -1936,10 +1940,6 @@
             R"(1:2 error: @group is not valid for 'override' declaration)",
         },
         TestParams{
-            {AttributeKind::kIndex},
-            R"(1:2 error: @index is not valid for 'override' declaration)",
-        },
-        TestParams{
             {AttributeKind::kId},
             Pass,
         },
diff --git a/src/tint/lang/wgsl/resolver/dependency_graph.cc b/src/tint/lang/wgsl/resolver/dependency_graph.cc
index bb07be0..890d024 100644
--- a/src/tint/lang/wgsl/resolver/dependency_graph.cc
+++ b/src/tint/lang/wgsl/resolver/dependency_graph.cc
@@ -36,6 +36,7 @@
 #include "src/tint/lang/core/builtin_value.h"
 #include "src/tint/lang/wgsl/ast/alias.h"
 #include "src/tint/lang/wgsl/ast/assignment_statement.h"
+#include "src/tint/lang/wgsl/ast/blend_src_attribute.h"
 #include "src/tint/lang/wgsl/ast/block_statement.h"
 #include "src/tint/lang/wgsl/ast/break_if_statement.h"
 #include "src/tint/lang/wgsl/ast/break_statement.h"
@@ -51,7 +52,6 @@
 #include "src/tint/lang/wgsl/ast/identifier.h"
 #include "src/tint/lang/wgsl/ast/if_statement.h"
 #include "src/tint/lang/wgsl/ast/increment_decrement_statement.h"
-#include "src/tint/lang/wgsl/ast/index_attribute.h"
 #include "src/tint/lang/wgsl/ast/internal_attribute.h"
 #include "src/tint/lang/wgsl/ast/interpolate_attribute.h"
 #include "src/tint/lang/wgsl/ast/invariant_attribute.h"
@@ -390,7 +390,7 @@
             [&](const ast::ColorAttribute* color) { TraverseExpression(color->expr); },
             [&](const ast::GroupAttribute* group) { TraverseExpression(group->expr); },
             [&](const ast::IdAttribute* id) { TraverseExpression(id->expr); },
-            [&](const ast::IndexAttribute* index) { TraverseExpression(index->expr); },
+            [&](const ast::BlendSrcAttribute* index) { TraverseExpression(index->expr); },
             [&](const ast::InterpolateAttribute* interpolate) {
                 TraverseExpression(interpolate->type);
                 TraverseExpression(interpolate->sampling);
diff --git a/src/tint/lang/wgsl/resolver/dual_source_blending_extension_test.cc b/src/tint/lang/wgsl/resolver/dual_source_blending_extension_test.cc
index 223744a..67a3aab 100644
--- a/src/tint/lang/wgsl/resolver/dual_source_blending_extension_test.cc
+++ b/src/tint/lang/wgsl/resolver/dual_source_blending_extension_test.cc
@@ -39,17 +39,18 @@
 
 using DualSourceBlendingExtensionTest = ResolverTest;
 
-// Using the @index attribute without chromium_internal_dual_source_blending enabled should fail.
-TEST_F(DualSourceBlendingExtensionTest, UseIndexAttribWithoutExtensionError) {
-    Structure("Output",
-              Vector{
-                  Member("a", ty.vec4<f32>(), Vector{Location(0_a), Index(Source{{12, 34}}, 0_a)}),
-              });
+// Using the @blend_src attribute without chromium_internal_dual_source_blending enabled should
+// fail.
+TEST_F(DualSourceBlendingExtensionTest, UseBlendSrcAttribWithoutExtensionError) {
+    Structure("Output", Vector{
+                            Member("a", ty.vec4<f32>(),
+                                   Vector{Location(0_a), BlendSrc(Source{{12, 34}}, 0_a)}),
+                        });
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(
         r()->error(),
-        R"(12:34 error: use of @index requires enabling extension 'chromium_internal_dual_source_blending')");
+        R"(12:34 error: use of @blend_src requires enabling extension 'chromium_internal_dual_source_blending')");
 }
 
 class DualSourceBlendingExtensionTests : public ResolverTest {
@@ -60,10 +61,10 @@
 };
 
 // Using an F32 as an index value should fail.
-TEST_F(DualSourceBlendingExtensionTests, IndexF32Error) {
+TEST_F(DualSourceBlendingExtensionTests, BlendSrcF32Error) {
     Structure("Output", Vector{
                             Member(Source{{12, 34}}, "a", ty.vec4<f32>(),
-                                   Vector{Location(0_a), Index(Source{{12, 34}}, 0_f)}),
+                                   Vector{Location(0_a), BlendSrc(Source{{12, 34}}, 0_f)}),
                         });
 
     EXPECT_FALSE(r()->Resolve());
@@ -71,111 +72,111 @@
 }
 
 // Using a floating point number as an index value should fail.
-TEST_F(DualSourceBlendingExtensionTests, IndexFloatValueError) {
+TEST_F(DualSourceBlendingExtensionTests, BlendSrcFloatValueError) {
     Structure("Output", Vector{
                             Member(Source{{12, 34}}, "a", ty.vec4<f32>(),
-                                   Vector{Location(0_a), Index(Source{{12, 34}}, 1.0_a)}),
+                                   Vector{Location(0_a), BlendSrc(Source{{12, 34}}, 1.0_a)}),
                         });
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(), "12:34 error: @location must be an i32 or u32 value");
 }
 
 // Using a number less than zero as an index value should fail.
-TEST_F(DualSourceBlendingExtensionTests, IndexNegativeValue) {
+TEST_F(DualSourceBlendingExtensionTests, BlendSrcNegativeValue) {
     Structure("Output", Vector{
                             Member(Source{{12, 34}}, "a", ty.vec4<f32>(),
-                                   Vector{Location(0_a), Index(Source{{12, 34}}, -1_a)}),
+                                   Vector{Location(0_a), BlendSrc(Source{{12, 34}}, -1_a)}),
                         });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: @index value must be zero or one");
+    EXPECT_EQ(r()->error(), "12:34 error: @blend_src value must be zero or one");
 }
 
 // Using a number greater than one as an index value should fail.
-TEST_F(DualSourceBlendingExtensionTests, IndexValueAboveOne) {
+TEST_F(DualSourceBlendingExtensionTests, BlendSrcValueAboveOne) {
     Structure("Output", Vector{
                             Member(Source{{12, 34}}, "a", ty.vec4<f32>(),
-                                   Vector{Location(0_a), Index(Source{{12, 34}}, 2_a)}),
+                                   Vector{Location(0_a), BlendSrc(Source{{12, 34}}, 2_a)}),
                         });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: @index value must be zero or one");
+    EXPECT_EQ(r()->error(), "12:34 error: @blend_src value must be zero or one");
 }
 
 // Using an index value at the same location multiple times should fail.
-TEST_F(DualSourceBlendingExtensionTests, DuplicateIndexes) {
+TEST_F(DualSourceBlendingExtensionTests, DuplicateBlendSrces) {
     Structure("Output", Vector{
-                            Member("a", ty.vec4<f32>(), Vector{Location(0_a), Index(0_a)}),
+                            Member("a", ty.vec4<f32>(), Vector{Location(0_a), BlendSrc(0_a)}),
                             Member(Source{{12, 34}}, "b", ty.vec4<f32>(),
-                                   Vector{Location(Source{{12, 34}}, 0_a), Index(0_a)}),
+                                   Vector{Location(Source{{12, 34}}, 0_a), BlendSrc(0_a)}),
                         });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: @location(0) @index(0) appears multiple times");
+    EXPECT_EQ(r()->error(), "12:34 error: @location(0) @blend_src(0) appears multiple times");
 }
 
 // Using the index attribute without a location attribute should fail.
-TEST_F(DualSourceBlendingExtensionTests, IndexWithMissingLocationAttribute_Struct) {
+TEST_F(DualSourceBlendingExtensionTests, BlendSrcWithMissingLocationAttribute_Struct) {
     Structure("Output", Vector{
                             Member(Source{{12, 34}}, "a", ty.vec4<f32>(),
-                                   Vector{Index(Source{{12, 34}}, 1_a)}),
+                                   Vector{BlendSrc(Source{{12, 34}}, 1_a)}),
                         });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: @index can only be used with @location(0)");
+    EXPECT_EQ(r()->error(), "12:34 error: @blend_src can only be used with @location(0)");
 }
 
 // Using the index attribute without a location attribute should fail.
-TEST_F(DualSourceBlendingExtensionTests, IndexWithMissingLocationAttribute_ReturnValue) {
+TEST_F(DualSourceBlendingExtensionTests, BlendSrcWithMissingLocationAttribute_ReturnValue) {
     Func("F", Empty, ty.vec4<f32>(),
          Vector{
              Return(Call<vec4<f32>>()),
          },
          Vector{Stage(ast::PipelineStage::kFragment)},
          Vector{
-             Index(Source{{12, 34}}, 1_a),
+             BlendSrc(Source{{12, 34}}, 1_a),
              Builtin(core::BuiltinValue::kPointSize),
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: @index can only be used with @location(0)");
+    EXPECT_EQ(r()->error(), "12:34 error: @blend_src can only be used with @location(0)");
 }
 
 // Using an index attribute on a struct member should pass.
-TEST_F(DualSourceBlendingExtensionTests, StructMemberIndexAttribute) {
-    Structure("Output",
-              Vector{
-                  Member("a", ty.vec4<f32>(), Vector{Location(0_a), Index(Source{{12, 34}}, 0_a)}),
-              });
+TEST_F(DualSourceBlendingExtensionTests, StructMemberBlendSrcAttribute) {
+    Structure("Output", Vector{
+                            Member("a", ty.vec4<f32>(),
+                                   Vector{Location(0_a), BlendSrc(Source{{12, 34}}, 0_a)}),
+                        });
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
 }
 
 // Using an index attribute on a global variable should pass. This is needed internally when using
-// @index with the canonicalize_entry_point transform. This test uses an internal attribute to
+// @blend_src with the canonicalize_entry_point transform. This test uses an internal attribute to
 // ignore address space, which is how it is used with the canonicalize_entry_point transform.
-TEST_F(DualSourceBlendingExtensionTests, GlobalVariableIndexAttribute) {
+TEST_F(DualSourceBlendingExtensionTests, GlobalVariableBlendSrcAttribute) {
     GlobalVar(
         "var", ty.vec4<f32>(),
-        Vector{Location(0_a), Index(0_a), Disable(ast::DisabledValidation::kIgnoreAddressSpace)},
+        Vector{Location(0_a), BlendSrc(0_a), Disable(ast::DisabledValidation::kIgnoreAddressSpace)},
         core::AddressSpace::kOut);
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
 }
 
 // Using the index attribute with a non-zero location should fail.
-TEST_F(DualSourceBlendingExtensionTests, IndexWithNonZeroLocation_Struct) {
-    Structure("Output",
-              Vector{
-                  Member("a", ty.vec4<f32>(), Vector{Location(1_a), Index(Source{{12, 34}}, 0_a)}),
-              });
+TEST_F(DualSourceBlendingExtensionTests, BlendSrcWithNonZeroLocation_Struct) {
+    Structure("Output", Vector{
+                            Member("a", ty.vec4<f32>(),
+                                   Vector{Location(1_a), BlendSrc(Source{{12, 34}}, 0_a)}),
+                        });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: @index can only be used with @location(0)");
+    EXPECT_EQ(r()->error(), "12:34 error: @blend_src can only be used with @location(0)");
 }
 
 // Using the index attribute with a non-zero location should fail.
-TEST_F(DualSourceBlendingExtensionTests, IndexWithNonZeroLocation_ReturnValue) {
+TEST_F(DualSourceBlendingExtensionTests, BlendSrcWithNonZeroLocation_ReturnValue) {
     Func("F", Empty, ty.vec4<f32>(),
          Vector{
              Return(Call<vec4<f32>>()),
@@ -183,31 +184,32 @@
          Vector{Stage(ast::PipelineStage::kFragment)},
          Vector{
              Location(1_a),
-             Index(Source{{12, 34}}, 1_a),
+             BlendSrc(Source{{12, 34}}, 1_a),
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: @index can only be used with @location(0)");
+    EXPECT_EQ(r()->error(), "12:34 error: @blend_src can only be used with @location(0)");
 }
 
 TEST_F(DualSourceBlendingExtensionTests, NoNonZeroCollisionsBetweenInAndOut) {
     // struct NonZeroLocation {
     //   @location(1) a : vec4<f32>,
     // };
-    // struct NonZeroIndex {
-    //   @location(0) @index(1) a : vec4<f32>,
+    // struct NonZeroBlendSrc {
+    //   @location(0) @blend_src(1) a : vec4<f32>,
     // };
-    // fn X(in : NonZeroLocation) -> NonZeroIndex { return NonZeroIndex(); }
-    // fn Y(in : NonZeroIndex) -> NonZeroLocation { return NonZeroLocation(); }
+    // fn X(in : NonZeroLocation) -> NonZeroBlendSrc { return NonZeroBlendSrc(); }
+    // fn Y(in : NonZeroBlendSrc) -> NonZeroLocation { return NonZeroLocation(); }
     Structure("NonZeroLocation", Vector{
                                      Member("a", ty.vec4<f32>(), Vector{Location(1_a)}),
                                  });
-    Structure("NonZeroIndex", Vector{
-                                  Member("a", ty.vec4<f32>(), Vector{Location(0_a), Index(1_a)}),
-                              });
-    Func("X", Vector{Param("in", ty("NonZeroLocation"))}, ty("NonZeroIndex"),
-         Vector{Return(Call("NonZeroIndex"))}, Vector{Stage(ast::PipelineStage::kFragment)});
-    Func("Y", Vector{Param("in", ty("NonZeroIndex"))}, ty("NonZeroLocation"),
+    Structure("NonZeroBlendSrc",
+              Vector{
+                  Member("a", ty.vec4<f32>(), Vector{Location(0_a), BlendSrc(1_a)}),
+              });
+    Func("X", Vector{Param("in", ty("NonZeroLocation"))}, ty("NonZeroBlendSrc"),
+         Vector{Return(Call("NonZeroBlendSrc"))}, Vector{Stage(ast::PipelineStage::kFragment)});
+    Func("Y", Vector{Param("in", ty("NonZeroBlendSrc"))}, ty("NonZeroLocation"),
          Vector{Return(Call("NonZeroLocation"))}, Vector{Stage(ast::PipelineStage::kFragment)});
 
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -222,17 +224,17 @@
 
 // Rendering to multiple render targets while using dual source blending should fail.
 TEST_P(DualSourceBlendingExtensionTestWithParams,
-       MultipleRenderTargetsNotAllowed_IndexThenNonZeroLocation) {
+       MultipleRenderTargetsNotAllowed_BlendSrcThenNonZeroLocation) {
     // struct S {
-    //   @location(0) @index(0) a : vec4<f32>,
-    //   @location(0) @index(1) b : vec4<f32>,
+    //   @location(0) @blend_src(0) a : vec4<f32>,
+    //   @location(0) @blend_src(1) b : vec4<f32>,
     //   @location(n)           c : vec4<f32>,
     // };
     // fn F() -> S { return S(); }
     Structure("S",
               Vector{
-                  Member("a", ty.vec4<f32>(), Vector{Location(0_a), Index(0_a)}),
-                  Member("b", ty.vec4<f32>(), Vector{Location(0_a), Index(Source{{1, 2}}, 1_a)}),
+                  Member("a", ty.vec4<f32>(), Vector{Location(0_a), BlendSrc(0_a)}),
+                  Member("b", ty.vec4<f32>(), Vector{Location(0_a), BlendSrc(Source{{1, 2}}, 1_a)}),
                   Member("c", ty.vec4<f32>(), Vector{Location(Source{{3, 4}}, AInt(GetParam()))}),
               });
     Func("F", Empty, ty("S"), Vector{Return(Call("S"))},
@@ -240,31 +242,31 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(1:2 error: pipeline cannot use both non-zero @index and non-zero @location
+              R"(1:2 error: pipeline cannot use both non-zero @blend_src and non-zero @location
 3:4 note: non-zero @location declared here
 note: while analyzing entry point 'F')");
 }
 
 TEST_P(DualSourceBlendingExtensionTestWithParams,
-       MultipleRenderTargetsNotAllowed_NonZeroLocationThenIndex) {
+       MultipleRenderTargetsNotAllowed_NonZeroLocationThenBlendSrc) {
     // struct S {
     //   @location(n)           a : vec4<f32>,
-    //   @location(0) @index(0) b : vec4<f32>,
-    //   @location(0) @index(1) c : vec4<f32>,
+    //   @location(0) @blend_src(0) b : vec4<f32>,
+    //   @location(0) @blend_src(1) c : vec4<f32>,
     // };
     // fn F() -> S { return S(); }
     Structure("S",
               Vector{
                   Member("a", ty.vec4<f32>(), Vector{Location(Source{{1, 2}}, AInt(GetParam()))}),
-                  Member("b", ty.vec4<f32>(), Vector{Location(0_a), Index(0_a)}),
-                  Member("c", ty.vec4<f32>(), Vector{Location(0_a), Index(Source{{3, 4}}, 1_a)}),
+                  Member("b", ty.vec4<f32>(), Vector{Location(0_a), BlendSrc(0_a)}),
+                  Member("c", ty.vec4<f32>(), Vector{Location(0_a), BlendSrc(Source{{3, 4}}, 1_a)}),
               });
     Func(Source{{5, 6}}, "F", Empty, ty("S"), Vector{Return(Call("S"))},
          Vector{Stage(ast::PipelineStage::kFragment)});
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(3:4 error: pipeline cannot use both non-zero @index and non-zero @location
+              R"(3:4 error: pipeline cannot use both non-zero @blend_src and non-zero @location
 1:2 note: non-zero @location declared here
 5:6 note: while analyzing entry point 'F')");
 }
diff --git a/src/tint/lang/wgsl/resolver/resolver.cc b/src/tint/lang/wgsl/resolver/resolver.cc
index 454e928..e8b9bcc 100644
--- a/src/tint/lang/wgsl/resolver/resolver.cc
+++ b/src/tint/lang/wgsl/resolver/resolver.cc
@@ -634,11 +634,11 @@
                     global->Attributes().location = value.Get();
                     return kSuccess;
                 },
-                [&](const ast::IndexAttribute* attr) {
+                [&](const ast::BlendSrcAttribute* attr) {
                     if (!has_io_address_space) {
                         return kInvalid;
                     }
-                    auto value = IndexAttribute(attr);
+                    auto value = BlendSrcAttribute(attr);
                     if (value != Success) {
                         return kErrored;
                     }
@@ -1080,8 +1080,8 @@
                     func->SetReturnLocation(value.Get());
                     return kSuccess;
                 },
-                [&](const ast::IndexAttribute* attr) {
-                    auto value = IndexAttribute(attr);
+                [&](const ast::BlendSrcAttribute* attr) {
+                    auto value = BlendSrcAttribute(attr);
                     if (value != Success) {
                         return kErrored;
                     }
@@ -1666,13 +1666,25 @@
 }
 
 void Resolver::RegisterStore(const sem::ValueExpression* expr) {
-    auto& info = alias_analysis_infos_[current_function_];
     Switch(
         expr->RootIdentifier(),
         [&](const sem::GlobalVariable* global) {
-            info.module_scope_writes.insert({global, expr});
+            alias_analysis_infos_[current_function_].module_scope_writes.Add(global, expr);
         },
-        [&](const sem::Parameter* param) { info.parameter_writes.insert(param); });
+        [&](const sem::Parameter* param) {
+            alias_analysis_infos_[current_function_].parameter_writes.Add(param);
+        });
+}
+
+void Resolver::RegisterLoad(const sem::ValueExpression* expr) {
+    Switch(
+        expr->RootIdentifier(),
+        [&](const sem::GlobalVariable* global) {
+            alias_analysis_infos_[current_function_].module_scope_reads.Add(global, expr);
+        },
+        [&](const sem::Parameter* param) {
+            alias_analysis_infos_[current_function_].parameter_reads.Add(param);
+        });
 }
 
 bool Resolver::AliasAnalysis(const sem::Call* call) {
@@ -1716,8 +1728,8 @@
 
     // Track the set of root identifiers that are read and written by arguments passed in this
     // call.
-    std::unordered_map<const sem::Variable*, const sem::ValueExpression*> arg_reads;
-    std::unordered_map<const sem::Variable*, const sem::ValueExpression*> arg_writes;
+    Hashmap<const sem::Variable*, const sem::ValueExpression*, 4> arg_reads;
+    Hashmap<const sem::Variable*, const sem::ValueExpression*, 4> arg_writes;
     for (size_t i = 0; i < args.Length(); i++) {
         auto* arg = args[i];
         if (!arg->Type()->Is<core::type::Pointer>()) {
@@ -1725,60 +1737,57 @@
         }
 
         auto* root = arg->RootIdentifier();
-        if (target_info.parameter_writes.count(target->Parameters()[i])) {
+        if (target_info.parameter_writes.Contains(target->Parameters()[i])) {
             // Arguments that are written to can alias with any other argument or module-scope
             // variable access.
-            if (arg_writes.count(root)) {
-                return make_error(arg, {arg_writes.at(root), Alias::Argument, "write"});
+            if (auto write = arg_writes.Get(root)) {
+                return make_error(arg, {*write, Alias::Argument, "write"});
             }
-            if (arg_reads.count(root)) {
-                return make_error(arg, {arg_reads.at(root), Alias::Argument, "read"});
+            if (auto read = arg_reads.Get(root)) {
+                return make_error(arg, {*read, Alias::Argument, "read"});
             }
-            if (target_info.module_scope_reads.count(root)) {
-                return make_error(
-                    arg, {target_info.module_scope_reads.at(root), Alias::ModuleScope, "read"});
+            if (auto read = target_info.module_scope_reads.Get(root)) {
+                return make_error(arg, {*read, Alias::ModuleScope, "read"});
             }
-            if (target_info.module_scope_writes.count(root)) {
-                return make_error(
-                    arg, {target_info.module_scope_writes.at(root), Alias::ModuleScope, "write"});
+            if (auto write = target_info.module_scope_writes.Get(root)) {
+                return make_error(arg, {*write, Alias::ModuleScope, "write"});
             }
-            arg_writes.insert({root, arg});
+            arg_writes.Add(root, arg);
 
             // Propagate the write access to the caller.
             Switch(
                 root,
                 [&](const sem::GlobalVariable* global) {
-                    caller_info.module_scope_writes.insert({global, arg});
+                    caller_info.module_scope_writes.Add(global, arg);
                 },
-                [&](const sem::Parameter* param) { caller_info.parameter_writes.insert(param); });
-        } else if (target_info.parameter_reads.count(target->Parameters()[i])) {
+                [&](const sem::Parameter* param) { caller_info.parameter_writes.Add(param); });
+        } else if (target_info.parameter_reads.Contains(target->Parameters()[i])) {
             // Arguments that are read from can alias with arguments or module-scope variables
             // that are written to.
-            if (arg_writes.count(root)) {
-                return make_error(arg, {arg_writes.at(root), Alias::Argument, "write"});
+            if (auto write = arg_writes.Get(root)) {
+                return make_error(arg, {*write, Alias::Argument, "write"});
             }
-            if (target_info.module_scope_writes.count(root)) {
-                return make_error(
-                    arg, {target_info.module_scope_writes.at(root), Alias::ModuleScope, "write"});
+            if (auto write = target_info.module_scope_writes.Get(root)) {
+                return make_error(arg, {*write, Alias::ModuleScope, "write"});
             }
-            arg_reads.insert({root, arg});
+            arg_reads.Add(root, arg);
 
             // Propagate the read access to the caller.
             Switch(
                 root,
                 [&](const sem::GlobalVariable* global) {
-                    caller_info.module_scope_reads.insert({global, arg});
+                    caller_info.module_scope_reads.Add(global, arg);
                 },
-                [&](const sem::Parameter* param) { caller_info.parameter_reads.insert(param); });
+                [&](const sem::Parameter* param) { caller_info.parameter_reads.Add(param); });
         }
     }
 
     // Propagate module-scope variable uses to the caller.
     for (auto read : target_info.module_scope_reads) {
-        caller_info.module_scope_reads.insert({read.first, read.second});
+        caller_info.module_scope_reads.Add(read.key, read.value);
     }
     for (auto write : target_info.module_scope_writes) {
-        caller_info.module_scope_writes.insert({write.first, write.second});
+        caller_info.module_scope_writes.Add(write.key, write.value);
     }
 
     return true;
@@ -1848,14 +1857,8 @@
     load->Behaviors() = expr->Behaviors();
     b.Sem().Replace(expr->Declaration(), load);
 
-    // Track the load for the alias analysis.
-    auto& alias_info = alias_analysis_infos_[current_function_];
-    Switch(
-        expr->RootIdentifier(),
-        [&](const sem::GlobalVariable* global) {
-            alias_info.module_scope_reads.insert({global, expr});
-        },
-        [&](const sem::Parameter* param) { alias_info.parameter_reads.insert(param); });
+    // Register the load for the alias analysis.
+    RegisterLoad(expr);
 
     return load;
 }
@@ -2128,10 +2131,11 @@
         // Is this overload a constructor or conversion?
         if (match->info->flags.Contains(OverloadFlag::kIsConstructor)) {
             // Type constructor
-            auto params = Transform(match->parameters, [&](auto& p, size_t i) {
-                return b.create<sem::Parameter>(nullptr, static_cast<uint32_t>(i), p.type, p.usage);
-            });
             target_sem = constructors_.GetOrCreate(match.Get(), [&] {
+                auto params = Transform(match->parameters, [&](auto& p, size_t i) {
+                    return b.create<sem::Parameter>(nullptr, static_cast<uint32_t>(i), p.type,
+                                                    p.usage);
+                });
                 return b.create<sem::ValueConstructor>(match->return_type, std::move(params),
                                                        overload_stage);
             });
@@ -2474,16 +2478,43 @@
         CollectTextureSamplerPairs(target, call->Arguments());
     }
 
-    if (fn == wgsl::BuiltinFn::kWorkgroupUniformLoad) {
-        if (!validator_.WorkgroupUniformLoad(call)) {
-            return nullptr;
-        }
-    }
+    switch (fn) {
+        case wgsl::BuiltinFn::kWorkgroupUniformLoad:
+            if (!validator_.WorkgroupUniformLoad(call)) {
+                return nullptr;
+            }
+            RegisterLoad(args[0]);
+            break;
 
-    if (fn == wgsl::BuiltinFn::kSubgroupBroadcast) {
-        if (!validator_.SubgroupBroadcast(call)) {
-            return nullptr;
-        }
+        case wgsl::BuiltinFn::kSubgroupBroadcast:
+            if (!validator_.SubgroupBroadcast(call)) {
+                return nullptr;
+            }
+            break;
+
+        case wgsl::BuiltinFn::kAtomicLoad:
+            RegisterLoad(args[0]);
+            break;
+
+        case wgsl::BuiltinFn::kAtomicStore:
+            RegisterStore(args[0]);
+            break;
+
+        case wgsl::BuiltinFn::kAtomicAdd:
+        case wgsl::BuiltinFn::kAtomicSub:
+        case wgsl::BuiltinFn::kAtomicMax:
+        case wgsl::BuiltinFn::kAtomicMin:
+        case wgsl::BuiltinFn::kAtomicAnd:
+        case wgsl::BuiltinFn::kAtomicOr:
+        case wgsl::BuiltinFn::kAtomicXor:
+        case wgsl::BuiltinFn::kAtomicExchange:
+        case wgsl::BuiltinFn::kAtomicCompareExchangeWeak:
+            RegisterLoad(args[0]);
+            RegisterStore(args[0]);
+            break;
+
+        default:
+            break;
     }
 
     if (!validator_.BuiltinCall(call)) {
@@ -3496,8 +3527,9 @@
                         swizzle.Push(3u);
                         break;
                     default:
-                        AddError("invalid vector swizzle character",
-                                 expr->member->source.Begin() + swizzle.Length());
+                        AddError(
+                            "invalid vector swizzle character",
+                            expr->member->source.Begin() + static_cast<uint32_t>(swizzle.Length()));
                         return nullptr;
                 }
 
@@ -3790,8 +3822,8 @@
 
     return static_cast<uint32_t>(value);
 }
-tint::Result<uint32_t> Resolver::IndexAttribute(const ast::IndexAttribute* attr) {
-    ExprEvalStageConstraint constraint{core::EvaluationStage::kConstant, "@index value"};
+tint::Result<uint32_t> Resolver::BlendSrcAttribute(const ast::BlendSrcAttribute* attr) {
+    ExprEvalStageConstraint constraint{core::EvaluationStage::kConstant, "@blend_src value"};
     TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
 
     auto* materialized = Materialize(ValueExpression(attr->expr));
@@ -3807,7 +3839,7 @@
     auto const_value = materialized->ConstantValue();
     auto value = const_value->ValueAs<AInt>();
     if (value != 0 && value != 1) {
-        AddError("@index value must be zero or one", attr->source);
+        AddError("@blend_src value must be zero or one", attr->source);
         return Failure{};
     }
 
@@ -4416,12 +4448,12 @@
                     attributes.location = value.Get();
                     return true;
                 },
-                [&](const ast::IndexAttribute* attr) {
-                    auto value = IndexAttribute(attr);
+                [&](const ast::BlendSrcAttribute* attr) {
+                    auto value = BlendSrcAttribute(attr);
                     if (value != Success) {
                         return false;
                     }
-                    attributes.index = value.Get();
+                    attributes.blend_src = value.Get();
                     return true;
                 },
                 [&](const ast::ColorAttribute* attr) {
@@ -4848,7 +4880,7 @@
     ty = const_cast<core::type::Type*>(ty->UnwrapRef());
 
     if (auto* str = ty->As<sem::Struct>()) {
-        if (str->AddressSpaceUsage().count(address_space)) {
+        if (str->AddressSpaceUsage().Contains(address_space)) {
             return true;  // Already applied
         }
 
diff --git a/src/tint/lang/wgsl/resolver/resolver.h b/src/tint/lang/wgsl/resolver/resolver.h
index 344c5af..d51923c 100644
--- a/src/tint/lang/wgsl/resolver/resolver.h
+++ b/src/tint/lang/wgsl/resolver/resolver.h
@@ -314,6 +314,10 @@
     /// perform alias analysis.
     void RegisterStore(const sem::ValueExpression* expr);
 
+    /// Register a memory load of an expression, to track accesses to root identifiers in order to
+    /// perform alias analysis.
+    void RegisterLoad(const sem::ValueExpression* expr);
+
     /// Perform pointer alias analysis for `call`.
     /// @returns true is the call arguments are free from aliasing issues, false otherwise.
     bool AliasAnalysis(const sem::Call* call);
@@ -423,9 +427,9 @@
     /// @returns the color value on success.
     tint::Result<uint32_t> ColorAttribute(const ast::ColorAttribute* attr);
 
-    /// Resolves the `@index` attribute @p attr
-    /// @returns the index value on success.
-    tint::Result<uint32_t> IndexAttribute(const ast::IndexAttribute* attr);
+    /// Resolves the `@blend_src` attribute @p attr
+    /// @returns the blend_src value on success.
+    tint::Result<uint32_t> BlendSrcAttribute(const ast::BlendSrcAttribute* attr);
 
     /// Resolves the `@binding` attribute @p attr
     /// @returns the binding value on success.
@@ -679,13 +683,13 @@
     /// of determining if any two arguments alias at any callsite.
     struct AliasAnalysisInfo {
         /// The set of module-scope variables that are written to, and where that write occurs.
-        std::unordered_map<const sem::Variable*, const sem::ValueExpression*> module_scope_writes;
+        Hashmap<const sem::Variable*, const sem::ValueExpression*, 4> module_scope_writes;
         /// The set of module-scope variables that are read from, and where that read occurs.
-        std::unordered_map<const sem::Variable*, const sem::ValueExpression*> module_scope_reads;
+        Hashmap<const sem::Variable*, const sem::ValueExpression*, 4> module_scope_reads;
         /// The set of function parameters that are written to.
-        std::unordered_set<const sem::Variable*> parameter_writes;
+        Hashset<const sem::Variable*, 4> parameter_writes;
         /// The set of function parameters that are read from.
-        std::unordered_set<const sem::Variable*> parameter_reads;
+        Hashset<const sem::Variable*, 4> parameter_reads;
     };
 
     ProgramBuilder& b;
diff --git a/src/tint/lang/wgsl/resolver/resolver_test.cc b/src/tint/lang/wgsl/resolver/resolver_test.cc
index f8bcdb4..8eb0457 100644
--- a/src/tint/lang/wgsl/resolver/resolver_test.cc
+++ b/src/tint/lang/wgsl/resolver/resolver_test.cc
@@ -1069,13 +1069,13 @@
 
     auto* foo_sem = Sem().Get(foo);
     ASSERT_NE(foo_sem, nullptr);
-    ASSERT_EQ(foo_sem->CallSites().size(), 2u);
+    ASSERT_EQ(foo_sem->CallSites().Length(), 2u);
     EXPECT_EQ(foo_sem->CallSites()[0]->Declaration(), call_1);
     EXPECT_EQ(foo_sem->CallSites()[1]->Declaration(), call_2);
 
     auto* bar_sem = Sem().Get(bar);
     ASSERT_NE(bar_sem, nullptr);
-    EXPECT_EQ(bar_sem->CallSites().size(), 0u);
+    EXPECT_EQ(bar_sem->CallSites().Length(), 0u);
 }
 
 TEST_F(ResolverTest, Function_WorkgroupSize_NotSet) {
@@ -2009,21 +2009,21 @@
     EXPECT_EQ(func_c_sem->Parameters().Length(), 0u);
 
     const auto& b_eps = func_b_sem->AncestorEntryPoints();
-    ASSERT_EQ(2u, b_eps.size());
+    ASSERT_EQ(2u, b_eps.Length());
     EXPECT_EQ(Symbols().Register("ep_1"), b_eps[0]->Declaration()->name->symbol);
     EXPECT_EQ(Symbols().Register("ep_2"), b_eps[1]->Declaration()->name->symbol);
 
     const auto& a_eps = func_a_sem->AncestorEntryPoints();
-    ASSERT_EQ(1u, a_eps.size());
+    ASSERT_EQ(1u, a_eps.Length());
     EXPECT_EQ(Symbols().Register("ep_1"), a_eps[0]->Declaration()->name->symbol);
 
     const auto& c_eps = func_c_sem->AncestorEntryPoints();
-    ASSERT_EQ(2u, c_eps.size());
+    ASSERT_EQ(2u, c_eps.Length());
     EXPECT_EQ(Symbols().Register("ep_1"), c_eps[0]->Declaration()->name->symbol);
     EXPECT_EQ(Symbols().Register("ep_2"), c_eps[1]->Declaration()->name->symbol);
 
-    EXPECT_TRUE(ep_1_sem->AncestorEntryPoints().empty());
-    EXPECT_TRUE(ep_2_sem->AncestorEntryPoints().empty());
+    EXPECT_TRUE(ep_1_sem->AncestorEntryPoints().IsEmpty());
+    EXPECT_TRUE(ep_2_sem->AncestorEntryPoints().IsEmpty());
 }
 
 // Check for linear-time traversal of functions reachable from entry points.
@@ -2469,7 +2469,7 @@
 
 TEST_F(ResolverTest, ScopeDepth_NestedBlocks) {
     const ast::Statement* stmt = Return();
-    for (size_t i = 0; i < 150; i++) {
+    for (uint32_t i = 0; i < 150; i++) {
         stmt = Block(Source{{i, 1}}, stmt);
     }
     WrapInFunction(stmt);
@@ -2481,7 +2481,7 @@
 
 TEST_F(ResolverTest, ScopeDepth_NestedIf) {
     const ast::Statement* stmt = Return();
-    for (size_t i = 0; i < 150; i++) {
+    for (uint32_t i = 0; i < 150; i++) {
         stmt = If(Source{{i, 1}}, false, Block(Source{{i, 2}}, stmt));
     }
     WrapInFunction(stmt);
@@ -2493,7 +2493,7 @@
 
 TEST_F(ResolverTest, ScopeDepth_IfElseChain) {
     const ast::Statement* stmt = nullptr;
-    for (size_t i = 0; i < 150; i++) {
+    for (uint32_t i = 0; i < 150; i++) {
         stmt = If(Source{{i, 1}}, false, Block(Source{{i, 2}}), Else(stmt));
     }
     WrapInFunction(stmt);
@@ -2545,13 +2545,13 @@
     EXPECT_TRUE(r()->Resolve()) << r()->error();
 }
 
-size_t kMaxNestDepthOfCompositeType = 255;
+uint32_t kMaxNestDepthOfCompositeType = 255;
 
 TEST_F(ResolverTest, MaxNestDepthOfCompositeType_Structs_Valid) {
     auto* s = Structure("S", Vector{Member("m", ty.i32())});
-    size_t depth = 1;  // Depth of struct
-    size_t iterations = kMaxNestDepthOfCompositeType - depth;
-    for (size_t i = 0; i < iterations; ++i) {
+    uint32_t depth = 1;  // Depth of struct
+    uint32_t iterations = kMaxNestDepthOfCompositeType - depth;
+    for (uint32_t i = 0; i < iterations; ++i) {
         s = Structure("S" + std::to_string(i), Vector{Member("m", ty.Of(s))});
     }
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -2559,9 +2559,9 @@
 
 TEST_F(ResolverTest, MaxNestDepthOfCompositeType_Structs_Invalid) {
     auto* s = Structure("S", Vector{Member("m", ty.i32())});
-    size_t depth = 1;  // Depth of struct
-    size_t iterations = kMaxNestDepthOfCompositeType - depth + 1;
-    for (size_t i = 0; i < iterations; ++i) {
+    uint32_t depth = 1;  // Depth of struct
+    uint32_t iterations = kMaxNestDepthOfCompositeType - depth + 1;
+    for (uint32_t i = 0; i < iterations; ++i) {
         auto source = i == iterations - 1 ? Source{{12, 34}} : Source{{0, i}};
         s = Structure(source, "S" + std::to_string(i), Vector{Member("m", ty.Of(s))});
     }
@@ -2571,9 +2571,9 @@
 
 TEST_F(ResolverTest, MaxNestDepthOfCompositeType_StructsWithVector_Valid) {
     auto* s = Structure("S", Vector{Member("m", ty.vec3<i32>())});
-    size_t depth = 2;  // Despth of struct + vector
-    size_t iterations = kMaxNestDepthOfCompositeType - depth;
-    for (size_t i = 0; i < iterations; ++i) {
+    uint32_t depth = 2;  // Despth of struct + vector
+    uint32_t iterations = kMaxNestDepthOfCompositeType - depth;
+    for (uint32_t i = 0; i < iterations; ++i) {
         s = Structure("S" + std::to_string(i), Vector{Member("m", ty.Of(s))});
     }
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -2581,9 +2581,9 @@
 
 TEST_F(ResolverTest, MaxNestDepthOfCompositeType_StructsWithVector_Invalid) {
     auto* s = Structure("S", Vector{Member("m", ty.vec3<i32>())});
-    size_t depth = 2;  // Despth of struct + vector
-    size_t iterations = kMaxNestDepthOfCompositeType - depth + 1;
-    for (size_t i = 0; i < iterations; ++i) {
+    uint32_t depth = 2;  // Despth of struct + vector
+    uint32_t iterations = kMaxNestDepthOfCompositeType - depth + 1;
+    for (uint32_t i = 0; i < iterations; ++i) {
         auto source = i == iterations - 1 ? Source{{12, 34}} : Source{{0, i}};
         s = Structure(source, "S" + std::to_string(i), Vector{Member("m", ty.Of(s))});
     }
@@ -2593,9 +2593,9 @@
 
 TEST_F(ResolverTest, MaxNestDepthOfCompositeType_StructsWithMatrix_Valid) {
     auto* s = Structure("S", Vector{Member("m", ty.mat3x3<f32>())});
-    size_t depth = 3;  // Depth of struct + matrix
-    size_t iterations = kMaxNestDepthOfCompositeType - depth;
-    for (size_t i = 0; i < iterations; ++i) {
+    uint32_t depth = 3;  // Depth of struct + matrix
+    uint32_t iterations = kMaxNestDepthOfCompositeType - depth;
+    for (uint32_t i = 0; i < iterations; ++i) {
         s = Structure("S" + std::to_string(i), Vector{Member("m", ty.Of(s))});
     }
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -2603,9 +2603,9 @@
 
 TEST_F(ResolverTest, MaxNestDepthOfCompositeType_StructsWithMatrix_Invalid) {
     auto* s = Structure("S", Vector{Member("m", ty.mat3x3<f32>())});
-    size_t depth = 3;  // Depth of struct + matrix
-    size_t iterations = kMaxNestDepthOfCompositeType - depth + 1;
-    for (size_t i = 0; i < iterations; ++i) {
+    uint32_t depth = 3;  // Depth of struct + matrix
+    uint32_t iterations = kMaxNestDepthOfCompositeType - depth + 1;
+    for (uint32_t i = 0; i < iterations; ++i) {
         auto source = i == iterations - 1 ? Source{{12, 34}} : Source{{0, i}};
         s = Structure(source, "S" + std::to_string(i), Vector{Member("m", ty.Of(s))});
     }
@@ -2615,9 +2615,9 @@
 
 TEST_F(ResolverTest, MaxNestDepthOfCompositeType_Arrays_Valid) {
     auto a = ty.array(ty.i32(), 10_u);
-    size_t depth = 1;  // Depth of array
-    size_t iterations = kMaxNestDepthOfCompositeType - depth;
-    for (size_t i = 0; i < iterations; ++i) {
+    uint32_t depth = 1;  // Depth of array
+    uint32_t iterations = kMaxNestDepthOfCompositeType - depth;
+    for (uint32_t i = 0; i < iterations; ++i) {
         a = ty.array(a, 1_u);
     }
     Alias("a", a);
@@ -2626,9 +2626,9 @@
 
 TEST_F(ResolverTest, MaxNestDepthOfCompositeType_Arrays_Invalid) {
     auto a = ty.array(Source{{99, 88}}, ty.i32(), 10_u);
-    size_t depth = 1;  // Depth of array
-    size_t iterations = kMaxNestDepthOfCompositeType - depth + 1;
-    for (size_t i = 0; i < iterations; ++i) {
+    uint32_t depth = 1;  // Depth of array
+    uint32_t iterations = kMaxNestDepthOfCompositeType - depth + 1;
+    for (uint32_t i = 0; i < iterations; ++i) {
         auto source = (i == iterations - 1) ? Source{{12, 34}} : Source{{0, i}};
         a = ty.array(source, a, 1_u);
     }
@@ -2639,9 +2639,9 @@
 
 TEST_F(ResolverTest, MaxNestDepthOfCompositeType_ArraysOfVector_Valid) {
     auto a = ty.array<vec3<i32>, 10>();
-    size_t depth = 2;  // Depth of array + vector
-    size_t iterations = kMaxNestDepthOfCompositeType - depth;
-    for (size_t i = 0; i < iterations; ++i) {
+    uint32_t depth = 2;  // Depth of array + vector
+    uint32_t iterations = kMaxNestDepthOfCompositeType - depth;
+    for (uint32_t i = 0; i < iterations; ++i) {
         a = ty.array(a, 1_u);
     }
     Alias("a", a);
@@ -2650,9 +2650,9 @@
 
 TEST_F(ResolverTest, MaxNestDepthOfCompositeType_ArraysOfVector_Invalid) {
     auto a = ty.array(Source{{99, 88}}, ty.vec3<i32>(), 10_u);
-    size_t depth = 2;  // Depth of array + vector
-    size_t iterations = kMaxNestDepthOfCompositeType - depth + 1;
-    for (size_t i = 0; i < iterations; ++i) {
+    uint32_t depth = 2;  // Depth of array + vector
+    uint32_t iterations = kMaxNestDepthOfCompositeType - depth + 1;
+    for (uint32_t i = 0; i < iterations; ++i) {
         auto source = (i == iterations - 1) ? Source{{12, 34}} : Source{{0, i}};
         a = ty.array(source, a, 1_u);
     }
@@ -2663,9 +2663,9 @@
 
 TEST_F(ResolverTest, MaxNestDepthOfCompositeType_ArraysOfMatrix_Valid) {
     auto a = ty.array(ty.mat3x3<f32>(), 10_u);
-    size_t depth = 3;  // Depth of array + matrix
-    size_t iterations = kMaxNestDepthOfCompositeType - depth;
-    for (size_t i = 0; i < iterations; ++i) {
+    uint32_t depth = 3;  // Depth of array + matrix
+    uint32_t iterations = kMaxNestDepthOfCompositeType - depth;
+    for (uint32_t i = 0; i < iterations; ++i) {
         a = ty.array(a, 1_u);
     }
     Alias("a", a);
@@ -2674,9 +2674,9 @@
 
 TEST_F(ResolverTest, MaxNestDepthOfCompositeType_ArraysOfMatrix_Invalid) {
     auto a = ty.array(ty.mat3x3<f32>(), 10_u);
-    size_t depth = 3;  // Depth of array + matrix
-    size_t iterations = kMaxNestDepthOfCompositeType - depth + 1;
-    for (size_t i = 0; i < iterations; ++i) {
+    uint32_t depth = 3;  // Depth of array + matrix
+    uint32_t iterations = kMaxNestDepthOfCompositeType - depth + 1;
+    for (uint32_t i = 0; i < iterations; ++i) {
         auto source = (i == iterations - 1) ? Source{{12, 34}} : Source{{0, i}};
         a = ty.array(source, a, 1_u);
     }
@@ -2688,9 +2688,9 @@
 TEST_F(ResolverTest, MaxNestDepthOfCompositeType_StructsOfArray_Valid) {
     auto a = ty.array(ty.mat3x3<f32>(), 10_u);
     auto* s = Structure("S", Vector{Member("m", a)});
-    size_t depth = 4;  // Depth of struct + array + matrix
-    size_t iterations = kMaxNestDepthOfCompositeType - depth;
-    for (size_t i = 0; i < iterations; ++i) {
+    uint32_t depth = 4;  // Depth of struct + array + matrix
+    uint32_t iterations = kMaxNestDepthOfCompositeType - depth;
+    for (uint32_t i = 0; i < iterations; ++i) {
         s = Structure("S" + std::to_string(i), Vector{Member("m", ty.Of(s))});
     }
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -2699,9 +2699,9 @@
 TEST_F(ResolverTest, MaxNestDepthOfCompositeType_StructsOfArray_Invalid) {
     auto a = ty.array(ty.mat3x3<f32>(), 10_u);
     auto* s = Structure("S", Vector{Member("m", a)});
-    size_t depth = 4;  // Depth of struct + array + matrix
-    size_t iterations = kMaxNestDepthOfCompositeType - depth + 1;
-    for (size_t i = 0; i < iterations; ++i) {
+    uint32_t depth = 4;  // Depth of struct + array + matrix
+    uint32_t iterations = kMaxNestDepthOfCompositeType - depth + 1;
+    for (uint32_t i = 0; i < iterations; ++i) {
         auto source = i == iterations - 1 ? Source{{12, 34}} : Source{{0, i}};
         s = Structure(source, "S" + std::to_string(i), Vector{Member("m", ty.Of(s))});
     }
@@ -2712,9 +2712,9 @@
 TEST_F(ResolverTest, MaxNestDepthOfCompositeType_ArraysOfStruct_Valid) {
     auto* s = Structure("S", Vector{Member("m", ty.mat3x3<f32>())});
     auto a = ty.array(ty.Of(s), 10_u);
-    size_t depth = 4;  // Depth of array + struct + matrix
-    size_t iterations = kMaxNestDepthOfCompositeType - depth;
-    for (size_t i = 0; i < iterations; ++i) {
+    uint32_t depth = 4;  // Depth of array + struct + matrix
+    uint32_t iterations = kMaxNestDepthOfCompositeType - depth;
+    for (uint32_t i = 0; i < iterations; ++i) {
         a = ty.array(a, 1_u);
     }
     Alias("a", a);
@@ -2724,9 +2724,9 @@
 TEST_F(ResolverTest, MaxNestDepthOfCompositeType_ArraysOfStruct_Invalid) {
     auto* s = Structure("S", Vector{Member("m", ty.mat3x3<f32>())});
     auto a = ty.array(ty.Of(s), 10_u);
-    size_t depth = 4;  // Depth of array + struct + matrix
-    size_t iterations = kMaxNestDepthOfCompositeType - depth + 1;
-    for (size_t i = 0; i < iterations; ++i) {
+    uint32_t depth = 4;  // Depth of array + struct + matrix
+    uint32_t iterations = kMaxNestDepthOfCompositeType - depth + 1;
+    for (uint32_t i = 0; i < iterations; ++i) {
         auto source = (i == iterations - 1) ? Source{{12, 34}} : Source{{0, i}};
         a = ty.array(source, a, 1_u);
     }
diff --git a/src/tint/lang/wgsl/resolver/struct_address_space_use_test.cc b/src/tint/lang/wgsl/resolver/struct_address_space_use_test.cc
index bf5f835..5e017fc 100644
--- a/src/tint/lang/wgsl/resolver/struct_address_space_use_test.cc
+++ b/src/tint/lang/wgsl/resolver/struct_address_space_use_test.cc
@@ -47,7 +47,7 @@
 
     auto* sem = TypeOf(s)->As<core::type::Struct>();
     ASSERT_NE(sem, nullptr);
-    EXPECT_TRUE(sem->AddressSpaceUsage().empty());
+    EXPECT_TRUE(sem->AddressSpaceUsage().IsEmpty());
 }
 
 TEST_F(ResolverAddressSpaceUseTest, StructReachableFromParameter) {
diff --git a/src/tint/lang/wgsl/resolver/struct_pipeline_stage_use_test.cc b/src/tint/lang/wgsl/resolver/struct_pipeline_stage_use_test.cc
index d3537db..cb0adab 100644
--- a/src/tint/lang/wgsl/resolver/struct_pipeline_stage_use_test.cc
+++ b/src/tint/lang/wgsl/resolver/struct_pipeline_stage_use_test.cc
@@ -49,7 +49,7 @@
 
     auto* sem = TypeOf(s)->As<core::type::Struct>();
     ASSERT_NE(sem, nullptr);
-    EXPECT_TRUE(sem->PipelineStageUses().empty());
+    EXPECT_TRUE(sem->PipelineStageUses().IsEmpty());
 }
 
 TEST_F(ResolverPipelineStageUseTest, StructUsedAsNonEntryPointParam) {
@@ -61,7 +61,7 @@
 
     auto* sem = TypeOf(s)->As<core::type::Struct>();
     ASSERT_NE(sem, nullptr);
-    EXPECT_TRUE(sem->PipelineStageUses().empty());
+    EXPECT_TRUE(sem->PipelineStageUses().IsEmpty());
 }
 
 TEST_F(ResolverPipelineStageUseTest, StructUsedAsNonEntryPointReturnType) {
@@ -73,7 +73,7 @@
 
     auto* sem = TypeOf(s)->As<core::type::Struct>();
     ASSERT_NE(sem, nullptr);
-    EXPECT_TRUE(sem->PipelineStageUses().empty());
+    EXPECT_TRUE(sem->PipelineStageUses().IsEmpty());
 }
 
 TEST_F(ResolverPipelineStageUseTest, StructUsedAsVertexShaderParam) {
diff --git a/src/tint/lang/wgsl/resolver/validator.cc b/src/tint/lang/wgsl/resolver/validator.cc
index e6f7c31..907a3c4 100644
--- a/src/tint/lang/wgsl/resolver/validator.cc
+++ b/src/tint/lang/wgsl/resolver/validator.cc
@@ -47,6 +47,7 @@
 #include "src/tint/lang/wgsl/ast/alias.h"
 #include "src/tint/lang/wgsl/ast/assignment_statement.h"
 #include "src/tint/lang/wgsl/ast/bitcast_expression.h"
+#include "src/tint/lang/wgsl/ast/blend_src_attribute.h"
 #include "src/tint/lang/wgsl/ast/break_statement.h"
 #include "src/tint/lang/wgsl/ast/call_statement.h"
 #include "src/tint/lang/wgsl/ast/continue_statement.h"
@@ -55,7 +56,6 @@
 #include "src/tint/lang/wgsl/ast/for_loop_statement.h"
 #include "src/tint/lang/wgsl/ast/id_attribute.h"
 #include "src/tint/lang/wgsl/ast/if_statement.h"
-#include "src/tint/lang/wgsl/ast/index_attribute.h"
 #include "src/tint/lang/wgsl/ast/internal_attribute.h"
 #include "src/tint/lang/wgsl/ast/interpolate_attribute.h"
 #include "src/tint/lang/wgsl/ast/loop_statement.h"
@@ -1147,9 +1147,9 @@
     // TODO(jrprice): This state could be stored in sem::Function instead, and then passed to
     // sem::Function since it would be useful there too.
     Hashset<core::BuiltinValue, 4> builtins;
-    Hashset<std::pair<uint32_t, uint32_t>, 8> locations_and_indices;
+    Hashset<std::pair<uint32_t, uint32_t>, 8> locations_and_blend_srcs;
     const ast::LocationAttribute* first_nonzero_location = nullptr;
-    const ast::IndexAttribute* first_nonzero_index = nullptr;
+    const ast::BlendSrcAttribute* first_nonzero_blend_src = nullptr;
     Hashset<uint32_t, 4> colors;
     enum class ParamOrRetType {
         kParameter,
@@ -1162,14 +1162,14 @@
                                                      ParamOrRetType param_or_ret,
                                                      bool is_struct_member,
                                                      std::optional<uint32_t> location,
-                                                     std::optional<uint32_t> index,
+                                                     std::optional<uint32_t> blend_src,
                                                      std::optional<uint32_t> color) {
         // Scan attributes for pipeline IO attributes.
         // Check for overlap with attributes that have been seen previously.
         const ast::Attribute* pipeline_io_attribute = nullptr;
         const ast::LocationAttribute* location_attribute = nullptr;
         const ast::ColorAttribute* color_attribute = nullptr;
-        const ast::IndexAttribute* index_attribute = nullptr;
+        const ast::BlendSrcAttribute* blend_src_attribute = nullptr;
         const ast::InterpolateAttribute* interpolate_attribute = nullptr;
         const ast::InvariantAttribute* invariant_attribute = nullptr;
         for (auto* attr : attrs) {
@@ -1219,15 +1219,15 @@
 
                     return LocationAttribute(loc_attr, ty, stage, source);
                 },
-                [&](const ast::IndexAttribute* index_attr) {
-                    index_attribute = index_attr;
+                [&](const ast::BlendSrcAttribute* blend_src_attr) {
+                    blend_src_attribute = blend_src_attr;
 
-                    if (TINT_UNLIKELY(!index.has_value())) {
-                        TINT_ICE() << "@index has no value";
+                    if (TINT_UNLIKELY(!blend_src.has_value())) {
+                        TINT_ICE() << "@blend_src has no value";
                         return false;
                     }
 
-                    return IndexAttribute(index_attr, stage);
+                    return BlendSrcAttribute(blend_src_attr, stage);
                 },
                 [&](const ast::ColorAttribute* col_attr) {
                     color_attribute = col_attr;
@@ -1300,12 +1300,13 @@
                 }
             }
 
-            if (index_attribute) {
+            if (blend_src_attribute) {
                 // Because HLSL specifies dual source blending targets with SV_Target0 and 1, we
-                // should restrict targets with @index to location 0 for easy translation
+                // should restrict targets with @blend_src to location 0 for easy translation
                 // in the backend writers.
                 if (location.value_or(1) != 0) {
-                    AddError("@index can only be used with @location(0)", index_attribute->source);
+                    AddError("@blend_src can only be used with @location(0)",
+                             blend_src_attribute->source);
                     return false;
                 }
             }
@@ -1314,23 +1315,23 @@
                 if (!first_nonzero_location && location > 0u) {
                     first_nonzero_location = location_attribute;
                 }
-                if (!first_nonzero_index && index > 0u) {
-                    first_nonzero_index = index_attribute;
+                if (!first_nonzero_blend_src && blend_src > 0u) {
+                    first_nonzero_blend_src = blend_src_attribute;
                 }
-                if (first_nonzero_location && first_nonzero_index) {
-                    AddError("pipeline cannot use both non-zero @index and non-zero @location",
-                             first_nonzero_index->source);
+                if (first_nonzero_location && first_nonzero_blend_src) {
+                    AddError("pipeline cannot use both non-zero @blend_src and non-zero @location",
+                             first_nonzero_blend_src->source);
                     AddNote("non-zero @location declared here", first_nonzero_location->source);
                     return false;
                 }
 
-                std::pair<uint32_t, uint32_t> location_and_index(location.value(),
-                                                                 index.value_or(0));
-                if (!locations_and_indices.Add(location_and_index)) {
+                std::pair<uint32_t, uint32_t> location_and_blend_src(location.value(),
+                                                                     blend_src.value_or(0));
+                if (!locations_and_blend_srcs.Add(location_and_blend_src)) {
                     StringStream err;
                     err << "@location(" << location.value() << ") ";
-                    if (index_attribute) {
-                        err << "@index(" << index.value() << ") ";
+                    if (blend_src_attribute) {
+                        err << "@blend_src(" << blend_src.value() << ") ";
                     }
                     err << "appears multiple times";
                     AddError(err.str(), location_attribute->source);
@@ -1389,7 +1390,7 @@
                             member->Declaration()->attributes, member->Type(),
                             member->Declaration()->source, param_or_ret,
                             /*is_struct_member*/ true, member->Attributes().location,
-                            member->Attributes().index, member->Attributes().color)) {
+                            member->Attributes().blend_src, member->Attributes().color)) {
                         AddNote("while analyzing entry point '" + decl->name->symbol.Name() + "'",
                                 decl->source);
                         return false;
@@ -1413,9 +1414,9 @@
     // Clear IO sets after parameter validation. Builtin and location attributes in return types
     // should be validated independently from those used in parameters.
     builtins.Clear();
-    locations_and_indices.Clear();
+    locations_and_blend_srcs.Clear();
     first_nonzero_location = nullptr;
-    first_nonzero_index = nullptr;
+    first_nonzero_blend_src = nullptr;
 
     if (!func->ReturnType()->Is<core::type::Void>()) {
         if (!validate_entry_point_attributes(decl->return_type_attributes, func->ReturnType(),
@@ -2225,7 +2226,7 @@
         return false;
     }
 
-    Hashset<std::pair<uint32_t, uint32_t>, 8> locations_and_indices;
+    Hashset<std::pair<uint32_t, uint32_t>, 8> locations_and_blend_srcs;
     Hashset<uint32_t, 4> colors;
     for (auto* member : str->Members()) {
         if (auto* r = member->Type()->As<sem::Array>()) {
@@ -2249,7 +2250,7 @@
         }
 
         auto has_position = false;
-        const ast::IndexAttribute* index_attribute = nullptr;
+        const ast::BlendSrcAttribute* blend_src_attribute = nullptr;
         const ast::LocationAttribute* location_attribute = nullptr;
         const ast::ColorAttribute* color_attribute = nullptr;
         const ast::InvariantAttribute* invariant_attribute = nullptr;
@@ -2267,9 +2268,9 @@
                     return LocationAttribute(location, member->Type(), stage,
                                              member->Declaration()->source);
                 },
-                [&](const ast::IndexAttribute* index) {
-                    index_attribute = index;
-                    return IndexAttribute(index, stage);
+                [&](const ast::BlendSrcAttribute* blend_src) {
+                    blend_src_attribute = blend_src;
+                    return BlendSrcAttribute(blend_src, stage);
                 },
                 [&](const ast::ColorAttribute* color) {
                     color_attribute = color;
@@ -2313,12 +2314,13 @@
             return false;
         }
 
-        if (index_attribute) {
+        if (blend_src_attribute) {
             // Because HLSL specifies dual source blending targets with SV_Target0 and 1, we should
             // restrict targets with index attributes to location 0 for easy translation in the
             // backend writers.
             if (member->Attributes().location.value_or(1) != 0) {
-                AddError("@index can only be used with @location(0)", index_attribute->source);
+                AddError("@blend_src can only be used with @location(0)",
+                         blend_src_attribute->source);
                 return false;
             }
         }
@@ -2331,14 +2333,14 @@
         // Ensure all locations and index pairs are unique
         if (location_attribute) {
             uint32_t location = member->Attributes().location.value();
-            uint32_t index = member->Attributes().index.value_or(0);
+            uint32_t blend_src = member->Attributes().blend_src.value_or(0);
 
-            std::pair<uint32_t, uint32_t> location_and_index(location, index);
-            if (!locations_and_indices.Add(location_and_index)) {
+            std::pair<uint32_t, uint32_t> location_and_blend_src(location, blend_src);
+            if (!locations_and_blend_srcs.Add(location_and_blend_src)) {
                 StringStream err;
                 err << "@location(" << location << ") ";
-                if (index_attribute) {
-                    err << "@index(" << index << ") ";
+                if (blend_src_attribute) {
+                    err << "@blend_src(" << blend_src << ") ";
                 }
                 err << "appears multiple times";
                 AddError(err.str(), location_attribute->source);
@@ -2414,12 +2416,13 @@
     return true;
 }
 
-bool Validator::IndexAttribute(const ast::IndexAttribute* attr,
-                               ast::PipelineStage stage,
-                               const std::optional<bool> is_input) const {
+bool Validator::BlendSrcAttribute(const ast::BlendSrcAttribute* attr,
+                                  ast::PipelineStage stage,
+                                  const std::optional<bool> is_input) const {
     if (!enabled_extensions_.Contains(wgsl::Extension::kChromiumInternalDualSourceBlending)) {
         AddError(
-            "use of @index requires enabling extension 'chromium_internal_dual_source_blending'",
+            "use of @blend_src requires enabling extension "
+            "'chromium_internal_dual_source_blending'",
             attr->source);
         return false;
     }
diff --git a/src/tint/lang/wgsl/resolver/validator.h b/src/tint/lang/wgsl/resolver/validator.h
index 8bf5af3..4834073 100644
--- a/src/tint/lang/wgsl/resolver/validator.h
+++ b/src/tint/lang/wgsl/resolver/validator.h
@@ -371,14 +371,14 @@
                         const std::optional<bool> is_input = std::nullopt) const;
 
     /// Validates a index attribute
-    /// @param index_attr the index attribute to validate
+    /// @param blend_src_attr the blend_src attribute to validate
     /// @param stage the current pipeline stage
     /// @param is_input true if is an input variable, false if output variable, std::nullopt is
     /// unknown.
     /// @returns true on success, false otherwise.
-    bool IndexAttribute(const ast::IndexAttribute* index_attr,
-                        ast::PipelineStage stage,
-                        const std::optional<bool> is_input = std::nullopt) const;
+    bool BlendSrcAttribute(const ast::BlendSrcAttribute* blend_src_attr,
+                           ast::PipelineStage stage,
+                           const std::optional<bool> is_input = std::nullopt) const;
 
     /// Validates a loop statement
     /// @param stmt the loop statement
diff --git a/src/tint/lang/wgsl/resolver/value_constructor_validation_test.cc b/src/tint/lang/wgsl/resolver/value_constructor_validation_test.cc
index 3f0e1ca..027b471 100644
--- a/src/tint/lang/wgsl/resolver/value_constructor_validation_test.cc
+++ b/src/tint/lang/wgsl/resolver/value_constructor_validation_test.cc
@@ -2626,7 +2626,7 @@
         }
         args_tys << "vec" << param.rows << "<" + element_type_name + ">";
     }
-    const size_t kInvalidLoc = 2 * (param.columns - 1);
+    const uint32_t kInvalidLoc = 2 * (param.columns - 1);
     auto invalid_vec_type = ty.vec(param.create_element_ast_type(*this), param.rows - 1);
     args.Push(Call(Source{{12, kInvalidLoc}}, invalid_vec_type));
     args_tys << ", vec" << (param.rows - 1) << "<" + element_type_name + ">";
diff --git a/src/tint/lang/wgsl/sem/function.cc b/src/tint/lang/wgsl/sem/function.cc
index 9a56376..311c903 100644
--- a/src/tint/lang/wgsl/sem/function.cc
+++ b/src/tint/lang/wgsl/sem/function.cc
@@ -201,4 +201,8 @@
     return ret;
 }
 
+void Function::SetDiagnosticSeverity(wgsl::DiagnosticRule rule, wgsl::DiagnosticSeverity severity) {
+    diagnostic_severities_.Add(rule, severity);
+}
+
 }  // namespace tint::sem
diff --git a/src/tint/lang/wgsl/sem/function.h b/src/tint/lang/wgsl/sem/function.h
index a2a87ae..88b3c3d 100644
--- a/src/tint/lang/wgsl/sem/function.h
+++ b/src/tint/lang/wgsl/sem/function.h
@@ -155,12 +155,12 @@
 
     /// @returns the list of direct calls to functions / builtins made by this
     /// function
-    std::vector<const Call*> DirectCalls() const { return direct_calls_; }
+    const Vector<const Call*, 1>& DirectCalls() const { return direct_calls_; }
 
     /// Adds a record of the direct function / builtin calls made by this
     /// function
     /// @param call the call
-    void AddDirectCall(const Call* call) { direct_calls_.emplace_back(call); }
+    void AddDirectCall(const Call* call) { direct_calls_.Push(call); }
 
     /// @param target the target of a call
     /// @returns the Call to the given CallTarget, or nullptr the target was not
@@ -175,21 +175,19 @@
     }
 
     /// @returns the list of callsites to this function
-    std::vector<const Call*> CallSites() const { return callsites_; }
+    const Vector<const Call*, 1>& CallSites() const { return callsites_; }
 
     /// Adds a record of a callsite to this function
     /// @param call the callsite
-    void AddCallSite(const Call* call) { callsites_.emplace_back(call); }
+    void AddCallSite(const Call* call) { callsites_.Push(call); }
 
     /// @returns the ancestor entry points
-    const std::vector<const Function*>& AncestorEntryPoints() const {
-        return ancestor_entry_points_;
-    }
+    const Vector<const Function*, 1>& AncestorEntryPoints() const { return ancestor_entry_points_; }
 
     /// Adds a record that the given entry point transitively calls this function
     /// @param entry_point the entry point that transtively calls this function
     void AddAncestorEntryPoint(const sem::Function* entry_point) {
-        ancestor_entry_points_.emplace_back(entry_point);
+        ancestor_entry_points_.Push(entry_point);
     }
 
     /// Retrieves any referenced location variables
@@ -278,9 +276,7 @@
     /// Modifies the severity of a specific diagnostic rule for this function.
     /// @param rule the diagnostic rule
     /// @param severity the new diagnostic severity
-    void SetDiagnosticSeverity(wgsl::DiagnosticRule rule, wgsl::DiagnosticSeverity severity) {
-        diagnostic_severities_[rule] = severity;
-    }
+    void SetDiagnosticSeverity(wgsl::DiagnosticRule rule, wgsl::DiagnosticSeverity severity);
 
     /// @returns the diagnostic severity modifications applied to this function
     const wgsl::DiagnosticRuleSeverities& DiagnosticSeverities() const {
@@ -302,9 +298,9 @@
     UniqueVector<const Function*, 8> transitively_called_functions_;
     UniqueVector<const BuiltinFn*, 4> directly_called_builtins_;
     UniqueVector<VariablePair, 8> texture_sampler_pairs_;
-    std::vector<const Call*> direct_calls_;
-    std::vector<const Call*> callsites_;
-    std::vector<const Function*> ancestor_entry_points_;
+    Vector<const Call*, 1> direct_calls_;
+    Vector<const Call*, 1> callsites_;
+    Vector<const Function*, 1> ancestor_entry_points_;
     const Statement* discard_stmt_ = nullptr;
     sem::Behaviors behaviors_{sem::Behavior::kNext};
     wgsl::DiagnosticRuleSeverities diagnostic_severities_;
diff --git a/src/tint/lang/wgsl/sem/info.cc b/src/tint/lang/wgsl/sem/info.cc
index e9b59be..bfd6c0d 100644
--- a/src/tint/lang/wgsl/sem/info.cc
+++ b/src/tint/lang/wgsl/sem/info.cc
@@ -47,10 +47,8 @@
                                                   wgsl::DiagnosticRule rule) const {
     // Get the diagnostic severity modification for a node.
     auto check = [&](auto* node) {
-        auto& severities = node->DiagnosticSeverities();
-        auto itr = severities.find(rule);
-        if (itr != severities.end()) {
-            return itr->second;
+        if (auto severity = node->DiagnosticSeverities().Get(rule)) {
+            return *severity;
         }
         return wgsl::DiagnosticSeverity::kUndefined;
     };
diff --git a/src/tint/lang/wgsl/sem/info.h b/src/tint/lang/wgsl/sem/info.h
index f63fa1c..bd2daf4 100644
--- a/src/tint/lang/wgsl/sem/info.h
+++ b/src/tint/lang/wgsl/sem/info.h
@@ -80,7 +80,7 @@
 
     /// @param highest_node_id the last allocated (numerically highest) AST node identifier.
     void Reserve(ast::NodeID highest_node_id) {
-        nodes_.resize(std::max(highest_node_id.value + 1, nodes_.size()));
+        nodes_.Resize(std::max(highest_node_id.value + 1, static_cast<uint32_t>(nodes_.Length())));
     }
 
     /// Get looks up the semantic information for the AST node `ast_node`.
@@ -93,7 +93,7 @@
         static_assert(std::is_same_v<SEM, InferFromAST> ||
                           !tint::traits::IsTypeOrDerived<SemanticNodeTypeFor<AST>, SEM>,
                       "explicit template argument is unnecessary");
-        if (ast_node && ast_node->node_id.value < nodes_.size()) {
+        if (ast_node && ast_node->node_id.value < nodes_.Length()) {
             return As<RESULT>(nodes_[ast_node->node_id.value]);
         }
         return nullptr;
@@ -157,7 +157,7 @@
 
   private:
     // AST node index to semantic node
-    std::vector<const CastableBase*> nodes_;
+    tint::Vector<const CastableBase*, 0> nodes_;
     // The semantic module
     sem::Module* module_ = nullptr;
 };
diff --git a/src/tint/lang/wgsl/sem/module.cc b/src/tint/lang/wgsl/sem/module.cc
index 6e04901..178d662 100644
--- a/src/tint/lang/wgsl/sem/module.cc
+++ b/src/tint/lang/wgsl/sem/module.cc
@@ -39,4 +39,8 @@
 
 Module::~Module() = default;
 
+void Module::SetDiagnosticSeverity(wgsl::DiagnosticRule rule, wgsl::DiagnosticSeverity severity) {
+    diagnostic_severities_.Add(rule, severity);
+}
+
 }  // namespace tint::sem
diff --git a/src/tint/lang/wgsl/sem/module.h b/src/tint/lang/wgsl/sem/module.h
index c95798e..98c1022 100644
--- a/src/tint/lang/wgsl/sem/module.h
+++ b/src/tint/lang/wgsl/sem/module.h
@@ -61,9 +61,7 @@
     /// Modifies the severity of a specific diagnostic rule for this module.
     /// @param rule the diagnostic rule
     /// @param severity the new diagnostic severity
-    void SetDiagnosticSeverity(wgsl::DiagnosticRule rule, wgsl::DiagnosticSeverity severity) {
-        diagnostic_severities_[rule] = severity;
-    }
+    void SetDiagnosticSeverity(wgsl::DiagnosticRule rule, wgsl::DiagnosticSeverity severity);
 
     /// @returns the diagnostic severity modifications applied to this module
     const wgsl::DiagnosticRuleSeverities& DiagnosticSeverities() const {
diff --git a/src/tint/lang/wgsl/sem/statement.cc b/src/tint/lang/wgsl/sem/statement.cc
index 8497bb4..0e1a8c7 100644
--- a/src/tint/lang/wgsl/sem/statement.cc
+++ b/src/tint/lang/wgsl/sem/statement.cc
@@ -52,6 +52,11 @@
     return FindFirstParent<BlockStatement>();
 }
 
+void Statement::SetDiagnosticSeverity(wgsl::DiagnosticRule rule,
+                                      wgsl::DiagnosticSeverity severity) {
+    diagnostic_severities_.Add(rule, severity);
+}
+
 CompoundStatement::CompoundStatement(const ast::Statement* declaration,
                                      const CompoundStatement* parent,
                                      const sem::Function* function)
diff --git a/src/tint/lang/wgsl/sem/statement.h b/src/tint/lang/wgsl/sem/statement.h
index 88e1685..2e023fc 100644
--- a/src/tint/lang/wgsl/sem/statement.h
+++ b/src/tint/lang/wgsl/sem/statement.h
@@ -126,9 +126,7 @@
     /// Modifies the severity of a specific diagnostic rule for this statement.
     /// @param rule the diagnostic rule
     /// @param severity the new diagnostic severity
-    void SetDiagnosticSeverity(wgsl::DiagnosticRule rule, wgsl::DiagnosticSeverity severity) {
-        diagnostic_severities_[rule] = severity;
-    }
+    void SetDiagnosticSeverity(wgsl::DiagnosticRule rule, wgsl::DiagnosticSeverity severity);
 
     /// @returns the diagnostic severity modifications applied to this statement
     const wgsl::DiagnosticRuleSeverities& DiagnosticSeverities() const {
diff --git a/src/tint/lang/wgsl/writer/ast_printer/ast_printer.cc b/src/tint/lang/wgsl/writer/ast_printer/ast_printer.cc
index 2f51a4c..517ea6d 100644
--- a/src/tint/lang/wgsl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/wgsl/writer/ast_printer/ast_printer.cc
@@ -35,6 +35,7 @@
 #include "src/tint/lang/wgsl/ast/assignment_statement.h"
 #include "src/tint/lang/wgsl/ast/binary_expression.h"
 #include "src/tint/lang/wgsl/ast/bitcast_expression.h"
+#include "src/tint/lang/wgsl/ast/blend_src_attribute.h"
 #include "src/tint/lang/wgsl/ast/bool_literal_expression.h"
 #include "src/tint/lang/wgsl/ast/break_if_statement.h"
 #include "src/tint/lang/wgsl/ast/break_statement.h"
@@ -55,7 +56,6 @@
 #include "src/tint/lang/wgsl/ast/if_statement.h"
 #include "src/tint/lang/wgsl/ast/increment_decrement_statement.h"
 #include "src/tint/lang/wgsl/ast/index_accessor_expression.h"
-#include "src/tint/lang/wgsl/ast/index_attribute.h"
 #include "src/tint/lang/wgsl/ast/int_literal_expression.h"
 #include "src/tint/lang/wgsl/ast/internal_attribute.h"
 #include "src/tint/lang/wgsl/ast/interpolate_attribute.h"
@@ -511,8 +511,8 @@
                 EmitExpression(out, color->expr);
                 out << ")";
             },
-            [&](const ast::IndexAttribute* index) {
-                out << "index(";
+            [&](const ast::BlendSrcAttribute* index) {
+                out << "blend_src(";
                 EmitExpression(out, index->expr);
                 out << ")";
             },
diff --git a/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program.cc b/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program.cc
index 712fab3..c6498a1 100644
--- a/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program.cc
+++ b/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program.cc
@@ -1078,9 +1078,9 @@
                 if (auto location = ir_attrs.location) {
                     ast_attrs.Push(b.Location(u32(*location)));
                 }
-                if (auto index = ir_attrs.index) {
+                if (auto blend_src = ir_attrs.blend_src) {
                     Enable(wgsl::Extension::kChromiumInternalDualSourceBlending);
-                    ast_attrs.Push(b.Index(u32(*index)));
+                    ast_attrs.Push(b.BlendSrc(u32(*blend_src)));
                 }
                 if (auto builtin = ir_attrs.builtin) {
                     if (RequiresSubgroups(*builtin)) {
diff --git a/src/tint/lang/wgsl/writer/syntax_tree_printer/syntax_tree_printer.cc b/src/tint/lang/wgsl/writer/syntax_tree_printer/syntax_tree_printer.cc
index 529f9c2..6c0b0b4 100644
--- a/src/tint/lang/wgsl/writer/syntax_tree_printer/syntax_tree_printer.cc
+++ b/src/tint/lang/wgsl/writer/syntax_tree_printer/syntax_tree_printer.cc
@@ -32,6 +32,7 @@
 #include "src/tint/lang/wgsl/ast/assignment_statement.h"
 #include "src/tint/lang/wgsl/ast/binary_expression.h"
 #include "src/tint/lang/wgsl/ast/bitcast_expression.h"
+#include "src/tint/lang/wgsl/ast/blend_src_attribute.h"
 #include "src/tint/lang/wgsl/ast/bool_literal_expression.h"
 #include "src/tint/lang/wgsl/ast/break_if_statement.h"
 #include "src/tint/lang/wgsl/ast/break_statement.h"
@@ -51,7 +52,6 @@
 #include "src/tint/lang/wgsl/ast/if_statement.h"
 #include "src/tint/lang/wgsl/ast/increment_decrement_statement.h"
 #include "src/tint/lang/wgsl/ast/index_accessor_expression.h"
-#include "src/tint/lang/wgsl/ast/index_attribute.h"
 #include "src/tint/lang/wgsl/ast/int_literal_expression.h"
 #include "src/tint/lang/wgsl/ast/internal_attribute.h"
 #include "src/tint/lang/wgsl/ast/interpolate_attribute.h"
@@ -563,8 +563,8 @@
                 }
                 Line() << "]";
             },
-            [&](const ast::IndexAttribute* index) {
-                Line() << "IndexAttribute [";
+            [&](const ast::BlendSrcAttribute* index) {
+                Line() << "BlendSrcAttribute [";
                 {
                     ScopedIndent idx(this);
                     EmitExpression(index->expr);
diff --git a/src/tint/utils/containers/hashmap_base.h b/src/tint/utils/containers/hashmap_base.h
index b665891..81141ba 100644
--- a/src/tint/utils/containers/hashmap_base.h
+++ b/src/tint/utils/containers/hashmap_base.h
@@ -639,7 +639,7 @@
     size_t Wrap(size_t index) const { return index % slots_.Length(); }
 
     /// The vector of slots. The vector length is equal to its capacity.
-    Vector<Slot, kNumFixedSlots> slots_;
+    Vector<Slot, kMinSlots> slots_;
 
     /// The number of entries in the map.
     size_t count_ = 0;
diff --git a/src/tint/utils/diagnostic/source.h b/src/tint/utils/diagnostic/source.h
index 4c2fc9a..8400921 100644
--- a/src/tint/utils/diagnostic/source.h
+++ b/src/tint/utils/diagnostic/source.h
@@ -89,10 +89,10 @@
     class Location {
       public:
         /// the 1-based line number. 0 represents no line information.
-        size_t line = 0;
+        uint32_t line = 0;
         /// the 1-based column number in utf8-code units (bytes).
         /// 0 represents no column information.
-        size_t column = 0;
+        uint32_t column = 0;
 
         /// Returns true if `this` location is lexicographically less than `rhs`
         /// @param rhs location to compare against
@@ -132,7 +132,7 @@
         /// Return a column-shifted Range
         /// @param n the number of characters to shift by
         /// @returns a Range with a #begin and #end column shifted by `n`
-        inline Range operator+(size_t n) const {
+        inline Range operator+(uint32_t n) const {
             return Range{{begin.line, begin.column + n}, {end.line, end.column + n}};
         }
 
@@ -179,7 +179,7 @@
     /// Return a column-shifted Source
     /// @param n the number of characters to shift by
     /// @returns a Source with the range's columns shifted by `n`
-    inline Source operator+(size_t n) const { return Source(range + n, file); }
+    inline Source operator+(uint32_t n) const { return Source(range + n, file); }
 
     /// Returns true of `this` Source is lexicographically less than `rhs`
     /// @param rhs source to compare against