Import Tint changes from Dawn

Changes:
  - 79195ca42a7712ce671faff3bc9f96010874cbd3 tint/uniformity: implement analysis for full and partial ... by Antonio Maiorano <amaiorano@google.com>
  - 33a090f90f73277e5ea9959c21d35f7586688148 Remove unused SwitchStatement method. by dan sinclair <dsinclair@chromium.org>
  - 29fb8f8eef24ea3c80d3c2545a07e798e7f8e199 tint: optimize compile time for const_eval_*_test files by Antonio Maiorano <amaiorano@google.com>
  - 3fd42ae042e0a97de8af43061b739f3b492629a1 Convert the location attribute to expressions. by dan sinclair <dsinclair@chromium.org>
  - 155165cd52df5692d7af7f6158fb21d7490a4c88 Convert the id attribute to expressions. by dan sinclair <dsinclair@chromium.org>
  - f50ad7f63d89d8ba07076856924e2bbd7f53b1a0 tint/resolver: Make member attribute diagnostics consistent by Ben Clayton <bclayton@google.com>
  - df3a0462ad69db351d84ab24ce23279f566f6e78 tint/sem: Remove 'sem_' prefix from array / struct tests by Ben Clayton <bclayton@google.com>
  - c574151e7293f5ab2961162d7b134d70990e3bd6 tint: Remove junk from copyright header by Ben Clayton <bclayton@google.com>
  - cd4b6c147912a1f0d3a198667a31e32e96950e51 tint/sem: Add missing 'const' to static const char* by Ben Clayton <bclayton@google.com>
  - 4e0689b6653c276b626c958081786b36aee0e8ce tint/sem: Move variable decls to CompoundStatement by Ben Clayton <bclayton@google.com>
GitOrigin-RevId: 79195ca42a7712ce671faff3bc9f96010874cbd3
Change-Id: I4f27d61d0452e06b2be66ee8c03605f7b7e59074
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/106240
Reviewed-by: Ben Clayton <bclayton@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
Commit-Queue: Copybara Prod <copybara-worker-blackhole@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
diff --git a/src/tint/BUILD.gn b/src/tint/BUILD.gn
index e87e7cd..5ef8252 100644
--- a/src/tint/BUILD.gn
+++ b/src/tint/BUILD.gn
@@ -1163,6 +1163,7 @@
 
   tint_unittests_source_set("tint_unittests_sem_src") {
     sources = [
+      "sem/array_test.cc",
       "sem/atomic_test.cc",
       "sem/bool_test.cc",
       "sem/builtin_test.cc",
@@ -1179,9 +1180,8 @@
       "sem/reference_test.cc",
       "sem/sampled_texture_test.cc",
       "sem/sampler_test.cc",
-      "sem/sem_array_test.cc",
-      "sem/sem_struct_test.cc",
       "sem/storage_texture_test.cc",
+      "sem/struct_test.cc",
       "sem/texture_test.cc",
       "sem/type_manager_test.cc",
       "sem/type_test.cc",
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index b3dd6ce..0811598 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -838,6 +838,7 @@
     resolver/variable_test.cc
     resolver/variable_validation_test.cc
     scope_stack_test.cc
+    sem/array_test.cc
     sem/atomic.cc
     sem/bool_test.cc
     sem/builtin_test.cc
@@ -854,9 +855,8 @@
     sem/reference_test.cc
     sem/sampled_texture_test.cc
     sem/sampler_test.cc
-    sem/sem_array_test.cc
-    sem/sem_struct_test.cc
     sem/storage_texture_test.cc
+    sem/struct_test.cc
     sem/texture_test.cc
     sem/type_manager_test.cc
     sem/type_test.cc
diff --git a/src/tint/ast/storage_texture_test.cc b/src/tint/ast/storage_texture_test.cc
index 8f1b221..41f3d65 100644
--- a/src/tint/ast/storage_texture_test.cc
+++ b/src/tint/ast/storage_texture_test.cc
@@ -1,4 +1,4 @@
-// Copyright 2020 The Tint Authors->
+// Copyright 2020 The Tint Authors
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
diff --git a/src/tint/ast/switch_statement.h b/src/tint/ast/switch_statement.h
index 82a9aa4..eb083f0 100644
--- a/src/tint/ast/switch_statement.h
+++ b/src/tint/ast/switch_statement.h
@@ -38,9 +38,6 @@
     SwitchStatement(SwitchStatement&&);
     ~SwitchStatement() override;
 
-    /// @returns true if this is a default statement
-    bool IsDefault() const { return condition == nullptr; }
-
     /// Clones this node and all transitive child nodes using the `CloneContext`
     /// `ctx`.
     /// @param ctx the clone context
diff --git a/src/tint/reader/wgsl/parser_impl.cc b/src/tint/reader/wgsl/parser_impl.cc
index 7c06020..8d6040d 100644
--- a/src/tint/reader/wgsl/parser_impl.cc
+++ b/src/tint/reader/wgsl/parser_impl.cc
@@ -3495,15 +3495,16 @@
     if (t == "id") {
         const char* use = "id attribute";
         return expect_paren_block(use, [&]() -> Result {
-            auto val = expect_positive_sint(use);
-            if (val.errored) {
+            auto expr = expression();
+            if (expr.errored) {
                 return Failure::kErrored;
             }
+            if (!expr.matched) {
+                return add_error(peek(), "expected id expression");
+            }
             match(Token::Type::kComma);
 
-            return create<ast::IdAttribute>(
-                t.source(), create<ast::IntLiteralExpression>(
-                                val.value, ast::IntLiteralExpression::Suffix::kNone));
+            return create<ast::IdAttribute>(t.source(), expr.value);
         });
     }
 
@@ -3538,15 +3539,16 @@
     if (t == "location") {
         const char* use = "location attribute";
         return expect_paren_block(use, [&]() -> Result {
-            auto val = expect_positive_sint(use);
-            if (val.errored) {
+            auto expr = expression();
+            if (expr.errored) {
                 return Failure::kErrored;
             }
+            if (!expr.matched) {
+                return add_error(peek(), "expected location expression");
+            }
             match(Token::Type::kComma);
 
-            return builder_.Location(t.source(),
-                                     create<ast::IntLiteralExpression>(
-                                         val.value, ast::IntLiteralExpression::Suffix::kNone));
+            return builder_.Location(t.source(), expr.value);
         });
     }
 
diff --git a/src/tint/reader/wgsl/parser_impl_error_msg_test.cc b/src/tint/reader/wgsl/parser_impl_error_msg_test.cc
index 0fc27dd..9416a4d 100644
--- a/src/tint/reader/wgsl/parser_impl_error_msg_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_error_msg_test.cc
@@ -930,7 +930,7 @@
 )");
 }
 
-TEST_F(ParserImplErrorTest, GlobalDeclVarAttrListMissingComma) {
+TEST_F(ParserImplErrorTest, GlobalDeclVarAttrListMissingAt) {
     EXPECT("@location(1) group(2) var i : i32;",
            R"(test.wgsl:1:14 error: expected declaration after attributes
 @location(1) group(2) var i : i32;
@@ -959,10 +959,34 @@
 }
 
 TEST_F(ParserImplErrorTest, GlobalDeclVarAttrLocationInvalidValue) {
-    EXPECT("@location(x) var i : i32;",
-           R"(test.wgsl:1:11 error: expected signed integer literal for location attribute
-@location(x) var i : i32;
-          ^
+    EXPECT("@location(if) var i : i32;",
+           R"(test.wgsl:1:11 error: expected location expression
+@location(if) var i : i32;
+          ^^
+)");
+}
+
+TEST_F(ParserImplErrorTest, GlobalDeclVarAttrIdMissingLParen) {
+    EXPECT("@id 1) var i : i32;",
+           R"(test.wgsl:1:5 error: expected '(' for id attribute
+@id 1) var i : i32;
+    ^
+)");
+}
+
+TEST_F(ParserImplErrorTest, GlobalDeclVarAttrIdMissingRParen) {
+    EXPECT("@id (1 var i : i32;",
+           R"(test.wgsl:1:8 error: expected ')' for id attribute
+@id (1 var i : i32;
+       ^^^
+)");
+}
+
+TEST_F(ParserImplErrorTest, GlobalDeclVarAttrIdInvalidValue) {
+    EXPECT("@id(if) var i : i32;",
+           R"(test.wgsl:1:5 error: expected id expression
+@id(if) var i : i32;
+    ^^
 )");
 }
 
diff --git a/src/tint/reader/wgsl/parser_impl_global_constant_decl_test.cc b/src/tint/reader/wgsl/parser_impl_global_constant_decl_test.cc
index 01ed4d9..0531066 100644
--- a/src/tint/reader/wgsl/parser_impl_global_constant_decl_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_global_constant_decl_test.cc
@@ -263,37 +263,5 @@
     ASSERT_EQ(id_attr, nullptr);
 }
 
-TEST_F(ParserImplTest, GlobalOverrideDecl_MissingId) {
-    auto p = parser("@id() override a : f32 = 1.");
-    auto attrs = p->attribute_list();
-    EXPECT_TRUE(attrs.errored);
-    EXPECT_FALSE(attrs.matched);
-
-    auto e = p->global_constant_decl(attrs.value);
-    EXPECT_TRUE(e.matched);
-    EXPECT_FALSE(e.errored);
-    auto* override = e.value->As<ast::Override>();
-    ASSERT_NE(override, nullptr);
-
-    EXPECT_TRUE(p->has_error());
-    EXPECT_EQ(p->error(), "1:5: expected signed integer literal for id attribute");
-}
-
-TEST_F(ParserImplTest, GlobalOverrideDecl_InvalidId) {
-    auto p = parser("@id(-7) override a : f32 = 1.");
-    auto attrs = p->attribute_list();
-    EXPECT_TRUE(attrs.errored);
-    EXPECT_FALSE(attrs.matched);
-
-    auto e = p->global_constant_decl(attrs.value);
-    EXPECT_TRUE(e.matched);
-    EXPECT_FALSE(e.errored);
-    auto* override = e.value->As<ast::Override>();
-    ASSERT_NE(override, nullptr);
-
-    EXPECT_TRUE(p->has_error());
-    EXPECT_EQ(p->error(), "1:5: id attribute must be positive");
-}
-
 }  // namespace
 }  // namespace tint::reader::wgsl
diff --git a/src/tint/reader/wgsl/parser_impl_struct_attribute_decl_test.cc b/src/tint/reader/wgsl/parser_impl_struct_attribute_decl_test.cc
index e015033..051104b 100644
--- a/src/tint/reader/wgsl/parser_impl_struct_attribute_decl_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_struct_attribute_decl_test.cc
@@ -46,7 +46,7 @@
     EXPECT_TRUE(attrs.errored);
     EXPECT_FALSE(attrs.matched);
     EXPECT_TRUE(attrs.value.IsEmpty());
-    EXPECT_EQ(p->error(), "1:11: expected signed integer literal for location attribute");
+    EXPECT_EQ(p->error(), "1:11: expected location expression");
 }
 
 TEST_F(ParserImplTest, AttributeDecl_MissingParenRight) {
diff --git a/src/tint/reader/wgsl/parser_impl_variable_attribute_test.cc b/src/tint/reader/wgsl/parser_impl_variable_attribute_test.cc
index 86e5487..1b538a2 100644
--- a/src/tint/reader/wgsl/parser_impl_variable_attribute_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_variable_attribute_test.cc
@@ -17,6 +17,105 @@
 namespace tint::reader::wgsl {
 namespace {
 
+TEST_F(ParserImplTest, Attribute_Id) {
+    auto p = parser("id(4)");
+    auto attr = p->attribute();
+    EXPECT_TRUE(attr.matched);
+    EXPECT_FALSE(attr.errored);
+    ASSERT_NE(attr.value, nullptr);
+    auto* var_attr = attr.value->As<ast::Attribute>();
+    ASSERT_NE(var_attr, nullptr);
+    ASSERT_FALSE(p->has_error());
+    ASSERT_TRUE(var_attr->Is<ast::IdAttribute>());
+
+    auto* loc = var_attr->As<ast::IdAttribute>();
+    ASSERT_TRUE(loc->expr->Is<ast::IntLiteralExpression>());
+    auto* exp = loc->expr->As<ast::IntLiteralExpression>();
+    EXPECT_EQ(exp->value, 4u);
+}
+
+TEST_F(ParserImplTest, Attribute_Id_Expression) {
+    auto p = parser("id(4 + 5)");
+    auto attr = p->attribute();
+    EXPECT_TRUE(attr.matched);
+    EXPECT_FALSE(attr.errored);
+    ASSERT_NE(attr.value, nullptr);
+    auto* var_attr = attr.value->As<ast::Attribute>();
+    ASSERT_NE(var_attr, nullptr);
+    ASSERT_FALSE(p->has_error());
+    ASSERT_TRUE(var_attr->Is<ast::IdAttribute>());
+
+    auto* loc = var_attr->As<ast::IdAttribute>();
+    ASSERT_TRUE(loc->expr->Is<ast::BinaryExpression>());
+    auto* expr = loc->expr->As<ast::BinaryExpression>();
+
+    EXPECT_EQ(ast::BinaryOp::kAdd, expr->op);
+    auto* v = expr->lhs->As<ast::IntLiteralExpression>();
+    ASSERT_NE(nullptr, v);
+    EXPECT_EQ(v->value, 4u);
+
+    v = expr->rhs->As<ast::IntLiteralExpression>();
+    ASSERT_NE(nullptr, v);
+    EXPECT_EQ(v->value, 5u);
+}
+
+TEST_F(ParserImplTest, Attribute_Id_TrailingComma) {
+    auto p = parser("id(4,)");
+    auto attr = p->attribute();
+    EXPECT_TRUE(attr.matched);
+    EXPECT_FALSE(attr.errored);
+    ASSERT_NE(attr.value, nullptr);
+    auto* var_attr = attr.value->As<ast::Attribute>();
+    ASSERT_NE(var_attr, nullptr);
+    ASSERT_FALSE(p->has_error());
+    ASSERT_TRUE(var_attr->Is<ast::IdAttribute>());
+
+    auto* loc = var_attr->As<ast::IdAttribute>();
+    ASSERT_TRUE(loc->expr->Is<ast::IntLiteralExpression>());
+    auto* exp = loc->expr->As<ast::IntLiteralExpression>();
+    EXPECT_EQ(exp->value, 4u);
+}
+
+TEST_F(ParserImplTest, Attribute_Id_MissingLeftParen) {
+    auto p = parser("id 4)");
+    auto attr = p->attribute();
+    EXPECT_FALSE(attr.matched);
+    EXPECT_TRUE(attr.errored);
+    EXPECT_EQ(attr.value, nullptr);
+    EXPECT_TRUE(p->has_error());
+    EXPECT_EQ(p->error(), "1:4: expected '(' for id attribute");
+}
+
+TEST_F(ParserImplTest, Attribute_Id_MissingRightParen) {
+    auto p = parser("id(4");
+    auto attr = p->attribute();
+    EXPECT_FALSE(attr.matched);
+    EXPECT_TRUE(attr.errored);
+    EXPECT_EQ(attr.value, nullptr);
+    EXPECT_TRUE(p->has_error());
+    EXPECT_EQ(p->error(), "1:5: expected ')' for id attribute");
+}
+
+TEST_F(ParserImplTest, Attribute_Id_MissingValue) {
+    auto p = parser("id()");
+    auto attr = p->attribute();
+    EXPECT_FALSE(attr.matched);
+    EXPECT_TRUE(attr.errored);
+    EXPECT_EQ(attr.value, nullptr);
+    EXPECT_TRUE(p->has_error());
+    EXPECT_EQ(p->error(), "1:4: expected id expression");
+}
+
+TEST_F(ParserImplTest, Attribute_Id_MissingInvalid) {
+    auto p = parser("id(if)");
+    auto attr = p->attribute();
+    EXPECT_FALSE(attr.matched);
+    EXPECT_TRUE(attr.errored);
+    EXPECT_EQ(attr.value, nullptr);
+    EXPECT_TRUE(p->has_error());
+    EXPECT_EQ(p->error(), "1:4: expected id expression");
+}
+
 TEST_F(ParserImplTest, Attribute_Location) {
     auto p = parser("location(4)");
     auto attr = p->attribute();
@@ -34,6 +133,31 @@
     EXPECT_EQ(exp->value, 4u);
 }
 
+TEST_F(ParserImplTest, Attribute_Location_Expression) {
+    auto p = parser("location(4 + 5)");
+    auto attr = p->attribute();
+    EXPECT_TRUE(attr.matched);
+    EXPECT_FALSE(attr.errored);
+    ASSERT_NE(attr.value, nullptr);
+    auto* var_attr = attr.value->As<ast::Attribute>();
+    ASSERT_NE(var_attr, nullptr);
+    ASSERT_FALSE(p->has_error());
+    ASSERT_TRUE(var_attr->Is<ast::LocationAttribute>());
+
+    auto* loc = var_attr->As<ast::LocationAttribute>();
+    ASSERT_TRUE(loc->expr->Is<ast::BinaryExpression>());
+    auto* expr = loc->expr->As<ast::BinaryExpression>();
+
+    EXPECT_EQ(ast::BinaryOp::kAdd, expr->op);
+    auto* v = expr->lhs->As<ast::IntLiteralExpression>();
+    ASSERT_NE(nullptr, v);
+    EXPECT_EQ(v->value, 4u);
+
+    v = expr->rhs->As<ast::IntLiteralExpression>();
+    ASSERT_NE(nullptr, v);
+    EXPECT_EQ(v->value, 5u);
+}
+
 TEST_F(ParserImplTest, Attribute_Location_TrailingComma) {
     auto p = parser("location(4,)");
     auto attr = p->attribute();
@@ -78,17 +202,17 @@
     EXPECT_TRUE(attr.errored);
     EXPECT_EQ(attr.value, nullptr);
     EXPECT_TRUE(p->has_error());
-    EXPECT_EQ(p->error(), "1:10: expected signed integer literal for location attribute");
+    EXPECT_EQ(p->error(), "1:10: expected location expression");
 }
 
 TEST_F(ParserImplTest, Attribute_Location_MissingInvalid) {
-    auto p = parser("location(nan)");
+    auto p = parser("location(if)");
     auto attr = p->attribute();
     EXPECT_FALSE(attr.matched);
     EXPECT_TRUE(attr.errored);
     EXPECT_EQ(attr.value, nullptr);
     EXPECT_TRUE(p->has_error());
-    EXPECT_EQ(p->error(), "1:10: expected signed integer literal for location attribute");
+    EXPECT_EQ(p->error(), "1:10: expected location expression");
 }
 
 struct BuiltinData {
diff --git a/src/tint/resolver/attribute_validation_test.cc b/src/tint/resolver/attribute_validation_test.cc
index 552b470..212f594 100644
--- a/src/tint/resolver/attribute_validation_test.cc
+++ b/src/tint/resolver/attribute_validation_test.cc
@@ -673,7 +673,7 @@
                               "a", ty.f32(), utils::Vector{MemberAlign(Source{{12, 34}}, "val")})});
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(12:34 error: 'align' value must be a positive, power-of-two integer)");
+              R"(12:34 error: @align value must be a positive, power-of-two integer)");
 }
 
 TEST_F(StructMemberAttributeTest, Align_Attribute_ConstPowerOfTwo) {
@@ -683,7 +683,7 @@
                               "a", ty.f32(), utils::Vector{MemberAlign(Source{{12, 34}}, "val")})});
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(12:34 error: 'align' value must be a positive, power-of-two integer)");
+              R"(12:34 error: @align value must be a positive, power-of-two integer)");
 }
 
 TEST_F(StructMemberAttributeTest, Align_Attribute_ConstF32) {
@@ -692,7 +692,7 @@
     Structure("mystruct", utils::Vector{Member(
                               "a", ty.f32(), utils::Vector{MemberAlign(Source{{12, 34}}, "val")})});
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: 'align' must be an i32 or u32 value)");
+    EXPECT_EQ(r()->error(), R"(12:34 error: @align must be an i32 or u32 value)");
 }
 
 TEST_F(StructMemberAttributeTest, Align_Attribute_ConstU32) {
@@ -717,7 +717,7 @@
     Structure("mystruct", utils::Vector{Member(
                               "a", ty.f32(), utils::Vector{MemberAlign(Source{{12, 34}}, "val")})});
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: 'align' must be an i32 or u32 value)");
+    EXPECT_EQ(r()->error(), R"(12:34 error: @align must be an i32 or u32 value)");
 }
 
 TEST_F(StructMemberAttributeTest, Align_Attribute_Var) {
@@ -757,7 +757,7 @@
     Structure("mystruct", utils::Vector{Member(
                               "a", ty.f32(), utils::Vector{MemberSize(Source{{12, 34}}, "val")})});
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: 'size' attribute must be positive)");
+    EXPECT_EQ(r()->error(), R"(12:34 error: @size must be a positive integer)");
 }
 
 TEST_F(StructMemberAttributeTest, Size_Attribute_ConstF32) {
@@ -766,7 +766,7 @@
     Structure("mystruct", utils::Vector{Member(
                               "a", ty.f32(), utils::Vector{MemberSize(Source{{12, 34}}, "val")})});
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: 'size' must be an i32 or u32 value)");
+    EXPECT_EQ(r()->error(), R"(12:34 error: @size must be an i32 or u32 value)");
 }
 
 TEST_F(StructMemberAttributeTest, Size_Attribute_ConstU32) {
@@ -791,7 +791,7 @@
     Structure("mystruct", utils::Vector{Member(
                               "a", ty.f32(), utils::Vector{MemberSize(Source{{12, 34}}, "val")})});
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: 'size' must be an i32 or u32 value)");
+    EXPECT_EQ(r()->error(), R"(12:34 error: @size must be an i32 or u32 value)");
 }
 
 TEST_F(StructMemberAttributeTest, Size_Attribute_Var) {
@@ -1640,6 +1640,41 @@
     EXPECT_EQ(r()->error(), R"(12:34 error: 'group' must be an i32 or u32 value)");
 }
 
+using IdTest = ResolverTest;
+
+TEST_F(IdTest, Const_I32) {
+    Override("val", ty.f32(), utils::Vector{Id(1_i)});
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+}
+
+TEST_F(IdTest, Const_U32) {
+    Override("val", ty.f32(), utils::Vector{Id(1_u)});
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+}
+
+TEST_F(IdTest, Const_AInt) {
+    Override("val", ty.f32(), utils::Vector{Id(1_a)});
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+}
+
+TEST_F(IdTest, Negative) {
+    Override("val", ty.f32(), utils::Vector{Id(Source{{12, 34}}, -1_i)});
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), R"(12:34 error: 'id' value must be non-negative)");
+}
+
+TEST_F(IdTest, F32) {
+    Override("val", ty.f32(), utils::Vector{Id(Source{{12, 34}}, 1_f)});
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), R"(12:34 error: 'id' must be an i32 or u32 value)");
+}
+
+TEST_F(IdTest, AFloat) {
+    Override("val", ty.f32(), utils::Vector{Id(Source{{12, 34}}, 1.0_a)});
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), R"(12:34 error: 'id' must be an i32 or u32 value)");
+}
+
 }  // namespace
 }  // namespace InterpolateTests
 
diff --git a/src/tint/resolver/const_eval_binary_op_test.cc b/src/tint/resolver/const_eval_binary_op_test.cc
index 18b28da..5f43499 100644
--- a/src/tint/resolver/const_eval_binary_op_test.cc
+++ b/src/tint/resolver/const_eval_binary_op_test.cc
@@ -54,47 +54,39 @@
     auto op = std::get<0>(GetParam());
     auto& c = std::get<1>(GetParam());
 
-    std::visit(
-        [&](auto&& expected) {
-            using T = typename std::decay_t<decltype(expected)>::ElementType;
-            if constexpr (std::is_same_v<T, AInt> || std::is_same_v<T, AFloat>) {
-                if (c.overflow) {
-                    // Overflow is not allowed for abstract types. This is tested separately.
-                    return;
-                }
-            }
+    auto* expected = ToValueBase(c.expected);
+    if (expected->IsAbstract() && c.overflow) {
+        // Overflow is not allowed for abstract types. This is tested separately.
+        return;
+    }
 
-            auto* lhs_expr = std::visit([&](auto&& value) { return value.Expr(*this); }, c.lhs);
-            auto* rhs_expr = std::visit([&](auto&& value) { return value.Expr(*this); }, c.rhs);
-            auto* expr = create<ast::BinaryExpression>(op, lhs_expr, rhs_expr);
+    auto* lhs = ToValueBase(c.lhs);
+    auto* rhs = ToValueBase(c.rhs);
 
-            GlobalConst("C", expr);
-            auto* expected_expr = expected.Expr(*this);
-            GlobalConst("E", expected_expr);
-            ASSERT_TRUE(r()->Resolve()) << r()->error();
+    auto* lhs_expr = lhs->Expr(*this);
+    auto* rhs_expr = rhs->Expr(*this);
+    auto* expr = create<ast::BinaryExpression>(op, lhs_expr, rhs_expr);
+    GlobalConst("C", expr);
+    ASSERT_TRUE(r()->Resolve()) << r()->error();
 
-            auto* sem = Sem().Get(expr);
-            const sem::Constant* value = sem->ConstantValue();
-            ASSERT_NE(value, nullptr);
-            EXPECT_TYPE(value->Type(), sem->Type());
+    auto* sem = Sem().Get(expr);
+    const sem::Constant* value = sem->ConstantValue();
+    ASSERT_NE(value, nullptr);
+    EXPECT_TYPE(value->Type(), sem->Type());
 
-            auto* expected_sem = Sem().Get(expected_expr);
-            const sem::Constant* expected_value = expected_sem->ConstantValue();
-            ASSERT_NE(expected_value, nullptr);
-            EXPECT_TYPE(expected_value->Type(), expected_sem->Type());
-
-            ForEachElemPair(value, expected_value,
-                            [&](const sem::Constant* a, const sem::Constant* b) {
-                                EXPECT_EQ(a->As<T>(), b->As<T>());
-                                if constexpr (IsIntegral<T>) {
-                                    // Check that the constant's integer doesn't contain unexpected
-                                    // data in the MSBs that are outside of the bit-width of T.
-                                    EXPECT_EQ(a->As<AInt>(), b->As<AInt>());
-                                }
-                                return HasFailure() ? Action::kStop : Action::kContinue;
-                            });
-        },
-        c.expected);
+    auto values_flat = ScalarArgsFrom(value);
+    auto expected_values_flat = expected->Args();
+    ASSERT_EQ(values_flat.values.Length(), expected_values_flat.values.Length());
+    for (size_t i = 0; i < values_flat.values.Length(); ++i) {
+        auto& a = values_flat.values[i];
+        auto& b = expected_values_flat.values[i];
+        EXPECT_EQ(a, b);
+        if (expected->IsIntegral()) {
+            // Check that the constant's integer doesn't contain unexpected
+            // data in the MSBs that are outside of the bit-width of T.
+            EXPECT_EQ(builder::As<AInt>(a), builder::As<AInt>(b));
+        }
+    }
 }
 
 INSTANTIATE_TEST_SUITE_P(MixedAbstractArgs,
@@ -658,21 +650,15 @@
 TEST_P(ResolverConstEvalBinaryOpTest_Overflow, Test) {
     Enable(ast::Extension::kF16);
     auto& c = GetParam();
-    auto* lhs_expr = std::visit([&](auto&& value) { return value.Expr(*this); }, c.lhs);
-    auto* rhs_expr = std::visit([&](auto&& value) { return value.Expr(*this); }, c.rhs);
+    auto* lhs = ToValueBase(c.lhs);
+    auto* rhs = ToValueBase(c.rhs);
+    auto* lhs_expr = lhs->Expr(*this);
+    auto* rhs_expr = rhs->Expr(*this);
     auto* expr = create<ast::BinaryExpression>(Source{{1, 1}}, c.op, lhs_expr, rhs_expr);
     GlobalConst("C", expr);
     ASSERT_FALSE(r()->Resolve());
-
-    std::string type_name = std::visit(
-        [&](auto&& value) {
-            using ValueType = std::decay_t<decltype(value)>;
-            return builder::FriendlyName<ValueType>();
-        },
-        c.lhs);
-
     EXPECT_THAT(r()->error(), HasSubstr("1:1 error: '"));
-    EXPECT_THAT(r()->error(), HasSubstr("' cannot be represented as '" + type_name + "'"));
+    EXPECT_THAT(r()->error(), HasSubstr("' cannot be represented as '" + lhs->TypeName() + "'"));
 }
 INSTANTIATE_TEST_SUITE_P(
     Test,
@@ -854,10 +840,8 @@
 using ResolverConstEvalShiftLeftConcreteGeqBitWidthError =
     ResolverTestWithParam<std::tuple<Types, Types>>;
 TEST_P(ResolverConstEvalShiftLeftConcreteGeqBitWidthError, Test) {
-    auto* lhs_expr =
-        std::visit([&](auto&& value) { return value.Expr(*this); }, std::get<0>(GetParam()));
-    auto* rhs_expr =
-        std::visit([&](auto&& value) { return value.Expr(*this); }, std::get<1>(GetParam()));
+    auto* lhs_expr = ToValueBase(std::get<0>(GetParam()))->Expr(*this);
+    auto* rhs_expr = ToValueBase(std::get<1>(GetParam()))->Expr(*this);
     GlobalConst("c", Shl(Source{{1, 1}}, lhs_expr, rhs_expr));
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(
@@ -880,10 +864,8 @@
 // AInt left shift results in sign change error
 using ResolverConstEvalShiftLeftSignChangeError = ResolverTestWithParam<std::tuple<Types, Types>>;
 TEST_P(ResolverConstEvalShiftLeftSignChangeError, Test) {
-    auto* lhs_expr =
-        std::visit([&](auto&& value) { return value.Expr(*this); }, std::get<0>(GetParam()));
-    auto* rhs_expr =
-        std::visit([&](auto&& value) { return value.Expr(*this); }, std::get<1>(GetParam()));
+    auto* lhs_expr = ToValueBase(std::get<0>(GetParam()))->Expr(*this);
+    auto* rhs_expr = ToValueBase(std::get<1>(GetParam()))->Expr(*this);
     GlobalConst("c", Shl(Source{{1, 1}}, lhs_expr, rhs_expr));
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(), "1:1 error: shift left operation results in sign change");
diff --git a/src/tint/resolver/const_eval_builtin_test.cc b/src/tint/resolver/const_eval_builtin_test.cc
index 3936fba..86223ba 100644
--- a/src/tint/resolver/const_eval_builtin_test.cc
+++ b/src/tint/resolver/const_eval_builtin_test.cc
@@ -83,54 +83,57 @@
         std::visit([&](auto&& v) { args.Push(v.Expr(*this)); }, a);
     }
 
-    std::visit(
-        [&](auto&& expected) {
-            using T = typename std::decay_t<decltype(expected)>::ElementType;
-            auto* expr = Call(sem::str(builtin), std::move(args));
+    auto* expected = ToValueBase(c.expected);
+    auto* expr = Call(sem::str(builtin), std::move(args));
 
-            GlobalConst("C", expr);
-            auto* expected_expr = expected.Expr(*this);
-            GlobalConst("E", expected_expr);
+    GlobalConst("C", expr);
+    auto* expected_expr = expected->Expr(*this);
+    GlobalConst("E", expected_expr);
 
-            EXPECT_TRUE(r()->Resolve()) << r()->error();
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
 
-            auto* sem = Sem().Get(expr);
-            const sem::Constant* value = sem->ConstantValue();
-            ASSERT_NE(value, nullptr);
-            EXPECT_TYPE(value->Type(), sem->Type());
+    auto* sem = Sem().Get(expr);
+    const sem::Constant* value = sem->ConstantValue();
+    ASSERT_NE(value, nullptr);
+    EXPECT_TYPE(value->Type(), sem->Type());
 
-            auto* expected_sem = Sem().Get(expected_expr);
-            const sem::Constant* expected_value = expected_sem->ConstantValue();
-            ASSERT_NE(expected_value, nullptr);
-            EXPECT_TYPE(expected_value->Type(), expected_sem->Type());
+    auto* expected_sem = Sem().Get(expected_expr);
+    const sem::Constant* expected_value = expected_sem->ConstantValue();
+    ASSERT_NE(expected_value, nullptr);
+    EXPECT_TYPE(expected_value->Type(), expected_sem->Type());
 
-            ForEachElemPair(value, expected_value,
-                            [&](const sem::Constant* a, const sem::Constant* b) {
-                                auto v = a->As<T>();
-                                auto e = b->As<T>();
-                                if constexpr (std::is_same_v<bool, T>) {
-                                    EXPECT_EQ(v, e);
-                                } else if constexpr (IsFloatingPoint<T>) {
-                                    if (std::isnan(e)) {
-                                        EXPECT_TRUE(std::isnan(v));
-                                    } else {
-                                        auto vf = (c.expected_pos_or_neg ? Abs(v) : v);
-                                        if (c.float_compare) {
-                                            EXPECT_FLOAT_EQ(vf, e);
-                                        } else {
-                                            EXPECT_EQ(vf, e);
-                                        }
-                                    }
-                                } else {
-                                    EXPECT_EQ((c.expected_pos_or_neg ? Abs(v) : v), e);
-                                    // Check that the constant's integer doesn't contain unexpected
-                                    // data in the MSBs that are outside of the bit-width of T.
-                                    EXPECT_EQ(a->As<AInt>(), b->As<AInt>());
-                                }
-                                return HasFailure() ? Action::kStop : Action::kContinue;
-                            });
-        },
-        c.expected);
+    // @TODO(amaiorano): Rewrite using ScalarArgsFrom()
+    ForEachElemPair(value, expected_value, [&](const sem::Constant* a, const sem::Constant* b) {
+        std::visit(
+            [&](auto&& ct_expected) {
+                using T = typename std::decay_t<decltype(ct_expected)>::ElementType;
+
+                auto v = a->As<T>();
+                auto e = b->As<T>();
+                if constexpr (std::is_same_v<bool, T>) {
+                    EXPECT_EQ(v, e);
+                } else if constexpr (IsFloatingPoint<T>) {
+                    if (std::isnan(e)) {
+                        EXPECT_TRUE(std::isnan(v));
+                    } else {
+                        auto vf = (c.expected_pos_or_neg ? Abs(v) : v);
+                        if (c.float_compare) {
+                            EXPECT_FLOAT_EQ(vf, e);
+                        } else {
+                            EXPECT_EQ(vf, e);
+                        }
+                    }
+                } else {
+                    EXPECT_EQ((c.expected_pos_or_neg ? Abs(v) : v), e);
+                    // Check that the constant's integer doesn't contain unexpected
+                    // data in the MSBs that are outside of the bit-width of T.
+                    EXPECT_EQ(a->As<AInt>(), b->As<AInt>());
+                }
+            },
+            c.expected);
+
+        return HasFailure() ? Action::kStop : Action::kContinue;
+    });
 }
 
 INSTANTIATE_TEST_SUITE_P(  //
diff --git a/src/tint/resolver/const_eval_conversion_test.cc b/src/tint/resolver/const_eval_conversion_test.cc
index 35657ee..7e6a6fc 100644
--- a/src/tint/resolver/const_eval_conversion_test.cc
+++ b/src/tint/resolver/const_eval_conversion_test.cc
@@ -29,20 +29,7 @@
     builder::Value<bool>>;
 
 static std::ostream& operator<<(std::ostream& o, const Scalar& scalar) {
-    std::visit(
-        [&](auto&& v) {
-            using ValueType = std::decay_t<decltype(v)>;
-            o << ValueType::DataType::Name() << "(";
-            for (auto& a : v.args.values) {
-                o << std::get<typename ValueType::ElementType>(a);
-                if (&a != &v.args.values.Back()) {
-                    o << ", ";
-                }
-            }
-            o << ")";
-        },
-        scalar);
-    return o;
+    return ToValueBase(scalar)->Print(o);
 }
 
 enum class Kind {
@@ -96,7 +83,7 @@
     const auto& type = std::get<1>(GetParam()).type;
     const auto unrepresentable = std::get<1>(GetParam()).unrepresentable;
 
-    auto* input_val = std::visit([&](auto val) { return val.Expr(*this); }, input);
+    auto* input_val = ToValueBase(input)->Expr(*this);
     auto* expr = Construct(type.ast(*this), input_val);
     if (kind == Kind::kVector) {
         expr = Construct(ty.vec(nullptr, 3), expr);
@@ -120,7 +107,7 @@
         ASSERT_NE(sem->ConstantValue(), nullptr);
         EXPECT_TYPE(sem->ConstantValue()->Type(), target_sem_ty);
 
-        auto expected_values = std::visit([&](auto&& val) { return val.args; }, expected);
+        auto expected_values = ToValueBase(expected)->Args();
         if (kind == Kind::kVector) {
             expected_values.values.Push(expected_values.values[0]);
             expected_values.values.Push(expected_values.values[0]);
diff --git a/src/tint/resolver/const_eval_test.h b/src/tint/resolver/const_eval_test.h
index 3840540..2761daf 100644
--- a/src/tint/resolver/const_eval_test.h
+++ b/src/tint/resolver/const_eval_test.h
@@ -41,6 +41,8 @@
 inline void CollectScalarArgs(const sem::Constant* c, builder::ScalarArgs& args) {
     Switch(
         c->Type(),  //
+        [&](const sem::AbstractInt*) { args.values.Push(c->As<AInt>()); },
+        [&](const sem::AbstractFloat*) { args.values.Push(c->As<AFloat>()); },
         [&](const sem::Bool*) { args.values.Push(c->As<bool>()); },
         [&](const sem::I32*) { args.values.Push(c->As<i32>()); },
         [&](const sem::U32*) { args.values.Push(c->As<u32>()); },
@@ -136,6 +138,7 @@
 using builder::Mat;
 using builder::Val;
 using builder::Value;
+using builder::ValueBase;
 using builder::Vec;
 
 using Types = std::variant<  //
@@ -188,21 +191,18 @@
     //
     >;
 
+/// Returns the current Value<T> in the `types` variant as a `ValueBase` pointer to use the
+/// polymorphic API. This trades longer compile times using std::variant for longer runtime via
+/// virtual function calls.
+template <typename ValueVariant>
+inline const ValueBase* ToValueBase(const ValueVariant& types) {
+    return std::visit(
+        [](auto&& t) -> const ValueBase* { return static_cast<const ValueBase*>(&t); }, types);
+}
+
+/// Prints Types to ostream
 inline std::ostream& operator<<(std::ostream& o, const Types& types) {
-    std::visit(
-        [&](auto&& v) {
-            using ValueType = std::decay_t<decltype(v)>;
-            o << ValueType::DataType::Name() << "(";
-            for (auto& a : v.args.values) {
-                o << std::get<typename ValueType::ElementType>(a);
-                if (&a != &v.args.values.Back()) {
-                    o << ", ";
-                }
-            }
-            o << ")";
-        },
-        types);
-    return o;
+    return ToValueBase(types)->Print(o);
 }
 
 // Calls `f` on deepest elements of both `a` and `b`. If function returns Action::kStop, it stops
diff --git a/src/tint/resolver/const_eval_unary_op_test.cc b/src/tint/resolver/const_eval_unary_op_test.cc
index 80b8caa..fced490 100644
--- a/src/tint/resolver/const_eval_unary_op_test.cc
+++ b/src/tint/resolver/const_eval_unary_op_test.cc
@@ -51,40 +51,34 @@
 
     auto op = std::get<0>(GetParam());
     auto& c = std::get<1>(GetParam());
-    std::visit(
-        [&](auto&& expected) {
-            using T = typename std::decay_t<decltype(expected)>::ElementType;
 
-            auto* input_expr = std::visit([&](auto&& value) { return value.Expr(*this); }, c.input);
-            auto* expr = create<ast::UnaryOpExpression>(op, input_expr);
+    auto* expected = ToValueBase(c.expected);
+    auto* input = ToValueBase(c.input);
 
-            GlobalConst("C", expr);
-            auto* expected_expr = expected.Expr(*this);
-            GlobalConst("E", expected_expr);
-            ASSERT_TRUE(r()->Resolve()) << r()->error();
+    auto* input_expr = input->Expr(*this);
+    auto* expr = create<ast::UnaryOpExpression>(op, input_expr);
 
-            auto* sem = Sem().Get(expr);
-            const sem::Constant* value = sem->ConstantValue();
-            ASSERT_NE(value, nullptr);
-            EXPECT_TYPE(value->Type(), sem->Type());
+    GlobalConst("C", expr);
+    ASSERT_TRUE(r()->Resolve()) << r()->error();
 
-            auto* expected_sem = Sem().Get(expected_expr);
-            const sem::Constant* expected_value = expected_sem->ConstantValue();
-            ASSERT_NE(expected_value, nullptr);
-            EXPECT_TYPE(expected_value->Type(), expected_sem->Type());
+    auto* sem = Sem().Get(expr);
+    const sem::Constant* value = sem->ConstantValue();
+    ASSERT_NE(value, nullptr);
+    EXPECT_TYPE(value->Type(), sem->Type());
 
-            ForEachElemPair(value, expected_value,
-                            [&](const sem::Constant* a, const sem::Constant* b) {
-                                EXPECT_EQ(a->As<T>(), b->As<T>());
-                                if constexpr (IsIntegral<T>) {
-                                    // Check that the constant's integer doesn't contain unexpected
-                                    // data in the MSBs that are outside of the bit-width of T.
-                                    EXPECT_EQ(a->As<AInt>(), b->As<AInt>());
-                                }
-                                return HasFailure() ? Action::kStop : Action::kContinue;
-                            });
-        },
-        c.expected);
+    auto values_flat = ScalarArgsFrom(value);
+    auto expected_values_flat = expected->Args();
+    ASSERT_EQ(values_flat.values.Length(), expected_values_flat.values.Length());
+    for (size_t i = 0; i < values_flat.values.Length(); ++i) {
+        auto& a = values_flat.values[i];
+        auto& b = expected_values_flat.values[i];
+        EXPECT_EQ(a, b);
+        if (expected->IsIntegral()) {
+            // Check that the constant's integer doesn't contain unexpected
+            // data in the MSBs that are outside of the bit-width of T.
+            EXPECT_EQ(builder::As<AInt>(a), builder::As<AInt>(b));
+        }
+    }
 }
 INSTANTIATE_TEST_SUITE_P(Complement,
                          ResolverConstEvalUnaryOpTest,
diff --git a/src/tint/resolver/dependency_graph.cc b/src/tint/resolver/dependency_graph.cc
index 514e0cb..e84eec5 100644
--- a/src/tint/resolver/dependency_graph.cc
+++ b/src/tint/resolver/dependency_graph.cc
@@ -427,6 +427,14 @@
                 TraverseExpression(group->expr);
                 return true;
             },
+            [&](const ast::IdAttribute* id) {
+                TraverseExpression(id->expr);
+                return true;
+            },
+            [&](const ast::LocationAttribute* loc) {
+                TraverseExpression(loc->expr);
+                return true;
+            },
             [&](const ast::StructMemberAlignAttribute* align) {
                 TraverseExpression(align->expr);
                 return true;
@@ -445,9 +453,8 @@
             return;
         }
 
-        if (attr->IsAnyOf<ast::BuiltinAttribute, ast::IdAttribute, ast::InternalAttribute,
-                          ast::InterpolateAttribute, ast::InvariantAttribute,
-                          ast::LocationAttribute, ast::StageAttribute, ast::StrideAttribute,
+        if (attr->IsAnyOf<ast::BuiltinAttribute, ast::InternalAttribute, ast::InterpolateAttribute,
+                          ast::InvariantAttribute, ast::StageAttribute, ast::StrideAttribute,
                           ast::StructMemberOffsetAttribute>()) {
             return;
         }
diff --git a/src/tint/resolver/dependency_graph_test.cc b/src/tint/resolver/dependency_graph_test.cc
index 150d65a..724a527 100644
--- a/src/tint/resolver/dependency_graph_test.cc
+++ b/src/tint/resolver/dependency_graph_test.cc
@@ -1291,6 +1291,8 @@
     GlobalVar(Sym(), ty.sampler(ast::SamplerKind::kSampler));
 
     GlobalVar(Sym(), ty.i32(), utils::Vector{Binding(V), Group(V)});
+    GlobalVar(Sym(), ty.i32(), utils::Vector{Location(V)});
+    Override(Sym(), ty.i32(), utils::Vector{Id(V)});
 
     Func(Sym(), utils::Empty, ty.void_(), utils::Empty);
 #undef V
diff --git a/src/tint/resolver/resolver.cc b/src/tint/resolver/resolver.cc
index eebd5ea..c252473 100644
--- a/src/tint/resolver/resolver.cc
+++ b/src/tint/resolver/resolver.cc
@@ -449,18 +449,24 @@
     sem->SetConstructor(rhs);
 
     if (auto* id_attr = ast::GetAttribute<ast::IdAttribute>(v->attributes)) {
-        auto* materialize = Materialize(Expression(id_attr->expr));
-        if (!materialize) {
+        ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant, "@id"};
+        TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
+
+        auto* materialized = Materialize(Expression(id_attr->expr));
+        if (!materialized) {
             return nullptr;
         }
-        auto* c = materialize->ConstantValue();
-        if (!c) {
-            // TODO(crbug.com/tint/1633): Handle invalid materialization when expressions are
-            // supported.
+        if (!materialized->Type()->IsAnyOf<sem::I32, sem::U32>()) {
+            AddError("'id' must be an i32 or u32 value", id_attr->source);
             return nullptr;
         }
 
-        auto value = c->As<uint32_t>();
+        auto const_value = materialized->ConstantValue();
+        auto value = const_value->As<AInt>();
+        if (value < 0) {
+            AddError("'id' value must be non-negative", id_attr->source);
+            return nullptr;
+        }
         if (value > std::numeric_limits<decltype(OverrideId::value)>::max()) {
             AddError("override IDs must be between 0 and " +
                          std::to_string(std::numeric_limits<decltype(OverrideId::value)>::max()),
@@ -669,17 +675,22 @@
 
         std::optional<uint32_t> location;
         if (auto* attr = ast::GetAttribute<ast::LocationAttribute>(var->attributes)) {
-            auto* materialize = Materialize(Expression(attr->expr));
-            if (!materialize) {
+            auto* materialized = Materialize(Expression(attr->expr));
+            if (!materialized) {
                 return nullptr;
             }
-            auto* c = materialize->ConstantValue();
-            if (!c) {
-                // TODO(crbug.com/tint/1633): Add error message about invalid materialization
-                // when location can be an expression.
+            if (!materialized->Type()->IsAnyOf<sem::I32, sem::U32>()) {
+                AddError("'location' must be an i32 or u32 value", attr->source);
                 return nullptr;
             }
-            location = c->As<uint32_t>();
+
+            auto const_value = materialized->ConstantValue();
+            auto value = const_value->As<AInt>();
+            if (value < 0) {
+                AddError("'location' value must be non-negative", attr->source);
+                return nullptr;
+            }
+            location = u32(value);
         }
 
         sem = builder_->create<sem::GlobalVariable>(
@@ -2268,21 +2279,16 @@
                     current_statement_->FindFirstParent<sem::LoopContinuingBlockStatement>()) {
                 auto* loop_block = continuing_block->FindFirstParent<sem::LoopBlockStatement>();
                 if (loop_block->FirstContinue()) {
-                    auto& decls = loop_block->Decls();
                     // If our identifier is in loop_block->decls, make sure its index is
                     // less than first_continue
-                    auto iter = std::find_if(decls.begin(), decls.end(),
-                                             [&symbol](auto* v) { return v->symbol == symbol; });
-                    if (iter != decls.end()) {
-                        auto var_decl_index =
-                            static_cast<size_t>(std::distance(decls.begin(), iter));
-                        if (var_decl_index >= loop_block->NumDeclsAtFirstContinue()) {
+                    if (auto* decl = loop_block->Decls().Find(symbol)) {
+                        if (decl->order >= loop_block->NumDeclsAtFirstContinue()) {
                             AddError("continue statement bypasses declaration of '" +
                                          builder_->Symbols().NameFor(symbol) + "'",
                                      loop_block->FirstContinue()->source);
                             AddNote("identifier '" + builder_->Symbols().NameFor(symbol) +
                                         "' declared here",
-                                    (*iter)->source);
+                                    decl->variable->Declaration()->source);
                             AddNote("identifier '" + builder_->Symbols().NameFor(symbol) +
                                         "' referenced in continuing block here",
                                     expr->source);
@@ -2842,112 +2848,128 @@
         std::optional<uint32_t> location;
         for (auto* attr : member->attributes) {
             Mark(attr);
-            if (auto* o = attr->As<ast::StructMemberOffsetAttribute>()) {
-                // Offset attributes are not part of the WGSL spec, but are emitted
-                // by the SPIR-V reader.
+            bool ok = Switch(
+                attr,  //
+                [&](const ast::StructMemberOffsetAttribute* o) {
+                    // Offset attributes are not part of the WGSL spec, but are emitted
+                    // by the SPIR-V reader.
+                    ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant,
+                                                       "@offset value"};
+                    TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
 
-                ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant,
-                                                   "@offset value"};
-                TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
-
-                auto* materialized = Materialize(Expression(o->expr));
-                if (!materialized) {
-                    return nullptr;
-                }
-                auto const_value = materialized->ConstantValue();
-                if (!const_value) {
-                    AddError("'offset' must be constant expression", o->expr->source);
-                    return nullptr;
-                }
-                offset = const_value->As<uint64_t>();
-
-                if (offset < struct_size) {
-                    AddError("offsets must be in ascending order", o->source);
-                    return nullptr;
-                }
-                align = 1;
-                has_offset_attr = true;
-            } else if (auto* a = attr->As<ast::StructMemberAlignAttribute>()) {
-                ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant, "@align"};
-                TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
-
-                auto* materialized = Materialize(Expression(a->expr));
-                if (!materialized) {
-                    return nullptr;
-                }
-                if (!materialized->Type()->IsAnyOf<sem::I32, sem::U32>()) {
-                    AddError("'align' must be an i32 or u32 value", a->source);
-                    return nullptr;
-                }
-
-                auto const_value = materialized->ConstantValue();
-                if (!const_value) {
-                    AddError("'align' must be constant expression", a->source);
-                    return nullptr;
-                }
-                auto value = const_value->As<AInt>();
-
-                if (value <= 0 || !utils::IsPowerOfTwo(value)) {
-                    AddError("'align' value must be a positive, power-of-two integer", a->source);
-                    return nullptr;
-                }
-                align = u32(value);
-                has_align_attr = true;
-            } else if (auto* s = attr->As<ast::StructMemberSizeAttribute>()) {
-                ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant, "@size"};
-                TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
-
-                auto* materialized = Materialize(Expression(s->expr));
-                if (!materialized) {
-                    return nullptr;
-                }
-                if (!materialized->Type()->IsAnyOf<sem::U32, sem::I32>()) {
-                    AddError("'size' must be an i32 or u32 value", s->source);
-                    return nullptr;
-                }
-
-                auto const_value = materialized->ConstantValue();
-                if (!const_value) {
-                    AddError("'size' must be constant expression", s->expr->source);
-                    return nullptr;
-                }
-                {
-                    auto value = const_value->As<AInt>();
-                    if (value <= 0) {
-                        AddError("'size' attribute must be positive", s->source);
-                        return nullptr;
+                    auto* materialized = Materialize(Expression(o->expr));
+                    if (!materialized) {
+                        return false;
                     }
-                }
-                auto value = const_value->As<uint64_t>();
-                if (value < size) {
-                    AddError("'size' must be at least as big as the type's size (" +
-                                 std::to_string(size) + ")",
-                             s->source);
-                    return nullptr;
-                }
-                size = u32(value);
-                has_size_attr = true;
-            } else if (auto* l = attr->As<ast::LocationAttribute>()) {
-                ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant, "@location"};
-                TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
+                    auto const_value = materialized->ConstantValue();
+                    if (!const_value) {
+                        AddError("@offset must be constant expression", o->expr->source);
+                        return false;
+                    }
+                    offset = const_value->As<uint64_t>();
 
-                auto* materialize = Materialize(Expression(l->expr));
-                if (!materialize) {
-                    return nullptr;
-                }
-                auto* c = materialize->ConstantValue();
-                if (!c) {
-                    // TODO(crbug.com/tint/1633): Add error message about invalid materialization
-                    // when location can be an expression.
-                    return nullptr;
-                }
-                location = c->As<uint32_t>();
+                    if (offset < struct_size) {
+                        AddError("offsets must be in ascending order", o->source);
+                        return false;
+                    }
+                    align = 1;
+                    has_offset_attr = true;
+                    return true;
+                },
+                [&](const ast::StructMemberAlignAttribute* a) {
+                    ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant, "@align"};
+                    TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
+
+                    auto* materialized = Materialize(Expression(a->expr));
+                    if (!materialized) {
+                        return false;
+                    }
+                    if (!materialized->Type()->IsAnyOf<sem::I32, sem::U32>()) {
+                        AddError("@align must be an i32 or u32 value", a->source);
+                        return false;
+                    }
+
+                    auto const_value = materialized->ConstantValue();
+                    if (!const_value) {
+                        AddError("@align must be constant expression", a->source);
+                        return false;
+                    }
+                    auto value = const_value->As<AInt>();
+
+                    if (value <= 0 || !utils::IsPowerOfTwo(value)) {
+                        AddError("@align value must be a positive, power-of-two integer",
+                                 a->source);
+                        return false;
+                    }
+                    align = u32(value);
+                    has_align_attr = true;
+                    return true;
+                },
+                [&](const ast::StructMemberSizeAttribute* s) {
+                    ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant, "@size"};
+                    TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
+
+                    auto* materialized = Materialize(Expression(s->expr));
+                    if (!materialized) {
+                        return false;
+                    }
+                    if (!materialized->Type()->IsAnyOf<sem::U32, sem::I32>()) {
+                        AddError("@size must be an i32 or u32 value", s->source);
+                        return false;
+                    }
+
+                    auto const_value = materialized->ConstantValue();
+                    if (!const_value) {
+                        AddError("@size must be constant expression", s->expr->source);
+                        return false;
+                    }
+                    {
+                        auto value = const_value->As<AInt>();
+                        if (value <= 0) {
+                            AddError("@size must be a positive integer", s->source);
+                            return false;
+                        }
+                    }
+                    auto value = const_value->As<uint64_t>();
+                    if (value < size) {
+                        AddError("@size must be at least as big as the type's size (" +
+                                     std::to_string(size) + ")",
+                                 s->source);
+                        return false;
+                    }
+                    size = u32(value);
+                    has_size_attr = true;
+                    return true;
+                },
+                [&](const ast::LocationAttribute* l) {
+                    ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant,
+                                                       "@location"};
+                    TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
+
+                    auto* materialize = Materialize(Expression(l->expr));
+                    if (!materialize) {
+                        return false;
+                    }
+                    auto* c = materialize->ConstantValue();
+                    if (!c) {
+                        // TODO(crbug.com/tint/1633): Add error message about invalid
+                        // materialization when location can be an expression.
+                        return false;
+                    }
+                    location = c->As<uint32_t>();
+                    return true;
+                },
+                [&](Default) {
+                    // The validator will check attributes can be applied to the struct member.
+                    return true;
+                });
+            if (!ok) {
+                return nullptr;
             }
         }
 
         if (has_offset_attr && (has_align_attr || has_size_attr)) {
-            AddError("offset attributes cannot be used with align or size attributes",
-                     member->source);
+            AddError("@offset cannot be used with @align or @size", member->source);
             return nullptr;
         }
 
@@ -3128,9 +3150,7 @@
             }
         }
 
-        if (current_block_) {  // Not all statements are inside a block
-            current_block_->AddDecl(stmt->variable);
-        }
+        current_compound_statement_->AddDecl(variable->As<sem::LocalVariable>());
 
         if (auto* ctor = variable->Constructor()) {
             sem->Behaviors() = ctor->Behaviors();
@@ -3232,7 +3252,7 @@
         if (auto* block = sem->FindFirstParent<sem::LoopBlockStatement>()) {
             if (!block->FirstContinue()) {
                 const_cast<sem::LoopBlockStatement*>(block)->SetFirstContinue(
-                    stmt, block->Decls().size());
+                    stmt, block->Decls().Count());
             }
         }
 
@@ -3337,12 +3357,10 @@
     builder_->Sem().Add(ast, sem);
 
     auto* as_compound = As<sem::CompoundStatement, CastFlags::kDontErrorOnImpossibleCast>(sem);
-    auto* as_block = As<sem::BlockStatement, CastFlags::kDontErrorOnImpossibleCast>(sem);
 
     TINT_SCOPED_ASSIGNMENT(current_statement_, sem);
     TINT_SCOPED_ASSIGNMENT(current_compound_statement_,
                            as_compound ? as_compound : current_compound_statement_);
-    TINT_SCOPED_ASSIGNMENT(current_block_, as_block ? as_block : current_block_);
 
     if (!callback()) {
         return nullptr;
diff --git a/src/tint/resolver/resolver.h b/src/tint/resolver/resolver.h
index f699996..c25b48e 100644
--- a/src/tint/resolver/resolver.h
+++ b/src/tint/resolver/resolver.h
@@ -375,11 +375,8 @@
     /// * Assigns `sem` to #current_statement_
     /// * Assigns `sem` to #current_compound_statement_ if `sem` derives from
     ///   sem::CompoundStatement.
-    /// * Assigns `sem` to #current_block_ if `sem` derives from
-    ///   sem::BlockStatement.
     /// * Then calls `callback`.
-    /// * Before returning #current_statement_, #current_compound_statement_, and
-    ///   #current_block_ are restored to their original values.
+    /// * Before returning #current_statement_ and #current_compound_statement_ are restored to their original values.
     /// @returns `sem` if `callback` returns true, otherwise `nullptr`.
     template <typename SEM, typename F>
     SEM* StatementScope(const ast::Statement* ast, SEM* sem, F&& callback);
@@ -441,7 +438,6 @@
     sem::Function* current_function_ = nullptr;
     sem::Statement* current_statement_ = nullptr;
     sem::CompoundStatement* current_compound_statement_ = nullptr;
-    sem::BlockStatement* current_block_ = nullptr;
 };
 
 }  // namespace tint::resolver
diff --git a/src/tint/resolver/resolver_test_helper.h b/src/tint/resolver/resolver_test_helper.h
index 923f3ff..57fe14a 100644
--- a/src/tint/resolver/resolver_test_helper.h
+++ b/src/tint/resolver/resolver_test_helper.h
@@ -206,6 +206,12 @@
     utils::Vector<Storage, 16> values;
 };
 
+/// Returns current variant value in `s` cast to type `T`
+template <typename T>
+T As(ScalarArgs::Storage& s) {
+    return std::visit([](auto&& v) { return static_cast<T>(v); }, s);
+}
+
 /// @param o the std::ostream to write to
 /// @param args the ScalarArgs
 /// @return the std::ostream so calls can be chained
@@ -750,10 +756,45 @@
             DataType<T>::Name};
 }
 
+/// Base class for Value<T>
+struct ValueBase {
+    /// Constructor
+    ValueBase() = default;
+    /// Destructor
+    virtual ~ValueBase() = default;
+    /// Move constructor
+    ValueBase(ValueBase&&) = default;
+    /// Copy constructor
+    ValueBase(const ValueBase&) = default;
+    /// Copy assignment operator
+    /// @returns this instance
+    ValueBase& operator=(const ValueBase&) = default;
+    /// Creates an `ast::Expression` for the type T passing in previously stored args
+    /// @param b the ProgramBuilder
+    /// @returns an expression node
+    virtual const ast::Expression* Expr(ProgramBuilder& b) const = 0;
+    /// @returns args used to create expression via `Expr`
+    virtual const ScalarArgs& Args() const = 0;
+    /// @returns true if element type is abstract
+    virtual bool IsAbstract() const = 0;
+    /// @returns true if element type is an integral
+    virtual bool IsIntegral() const = 0;
+    /// @returns element type name
+    virtual std::string TypeName() const = 0;
+    /// Prints this value to the output stream
+    /// @param o the output stream
+    /// @returns input argument `o`
+    virtual std::ostream& Print(std::ostream& o) const = 0;
+};
+
 /// Value<T> is an instance of a value of type DataType<T>. Useful for storing values to create
 /// expressions with.
 template <typename T>
-struct Value {
+struct Value : ValueBase {
+    /// Constructor
+    /// @param a the scalar args
+    explicit Value(ScalarArgs a) : args(std::move(a)) {}
+
     /// Alias to T
     using Type = T;
     /// Alias to DataType<T>
@@ -764,15 +805,43 @@
     /// Creates a Value<T> with `args`
     /// @param args the args that will be passed to the expression
     /// @returns a Value<T>
-    static Value Create(ScalarArgs args) { return Value{CreatePtrsFor<T>(), std::move(args)}; }
+    static Value Create(ScalarArgs args) { return Value{std::move(args)}; }
 
     /// Creates an `ast::Expression` for the type T passing in previously stored args
     /// @param b the ProgramBuilder
     /// @returns an expression node
-    const ast::Expression* Expr(ProgramBuilder& b) const { return (*create.expr)(b, args); }
+    const ast::Expression* Expr(ProgramBuilder& b) const override {
+        auto create = CreatePtrsFor<T>();
+        return (*create.expr)(b, args);
+    }
 
-    /// functions to create values / types of the value
-    CreatePtrs create;
+    /// @returns args used to create expression via `Expr`
+    const ScalarArgs& Args() const override { return args; }
+
+    /// @returns true if element type is abstract
+    bool IsAbstract() const override { return tint::IsAbstract<ElementType>; }
+
+    /// @returns true if element type is an integral
+    bool IsIntegral() const override { return tint::IsIntegral<ElementType>; }
+
+    /// @returns element type name
+    std::string TypeName() const override { return tint::FriendlyName<ElementType>(); }
+
+    /// Prints this value to the output stream
+    /// @param o the output stream
+    /// @returns input argument `o`
+    std::ostream& Print(std::ostream& o) const override {
+        o << TypeName() << "(";
+        for (auto& a : args.values) {
+            o << std::get<ElementType>(a);
+            if (&a != &args.values.Back()) {
+                o << ", ";
+            }
+        }
+        o << ")";
+        return o;
+    }
+
     /// args to create expression with
     ScalarArgs args;
 };
diff --git a/src/tint/resolver/uniformity.cc b/src/tint/resolver/uniformity.cc
index f75536b..84c42da 100644
--- a/src/tint/resolver/uniformity.cc
+++ b/src/tint/resolver/uniformity.cc
@@ -47,6 +47,23 @@
 
 namespace {
 
+/// Unwraps `u->expr`'s chain of indirect (*) and address-of (&) expressions, returning the first
+/// expression that is neither of these.
+/// E.g. If `u` is `*(&(*(&p)))`, returns `p`.
+const ast::Expression* UnwrapIndirectAndAddressOfChain(const ast::UnaryOpExpression* u) {
+    auto* e = u->expr;
+    while (true) {
+        auto* unary = e->As<ast::UnaryOpExpression>();
+        if (unary &&
+            (unary->op == ast::UnaryOp::kIndirection || unary->op == ast::UnaryOp::kAddressOf)) {
+            e = unary->expr;
+        } else {
+            break;
+        }
+    }
+    return e;
+}
+
 /// CallSiteTag describes the uniformity requirements on the call sites of a function.
 enum CallSiteTag {
     CallSiteRequiredToBeUniform,
@@ -203,6 +220,10 @@
     /// Includes pointer parameters.
     std::unordered_set<const sem::Variable*> local_var_decls;
 
+    /// The set of partial pointer variables - pointers that point to a subobject (into an array or
+    /// struct).
+    std::unordered_set<const sem::Variable*> partial_ptrs;
+
     /// LoopSwitchInfo tracks information about the value of variables for a control flow construct.
     struct LoopSwitchInfo {
         /// The type of this control flow construct.
@@ -481,8 +502,8 @@
                 }
 
                 // Remove any variables declared in this scope from the set of in-scope variables.
-                for (auto* d : sem_.Get<sem::BlockStatement>(b)->Decls()) {
-                    current_function_->local_var_decls.erase(sem_.Get<sem::LocalVariable>(d));
+                for (auto decl : sem_.Get<sem::BlockStatement>(b)->Decls()) {
+                    current_function_->local_var_decls.erase(decl.value.variable);
                 }
 
                 return cf;
@@ -943,14 +964,27 @@
 
             [&](const ast::VariableDeclStatement* decl) {
                 Node* node;
+                auto* sem_var = sem_.Get(decl->variable);
                 if (decl->variable->constructor) {
                     auto [cf1, v] = ProcessExpression(cf, decl->variable->constructor);
                     cf = cf1;
                     node = v;
+
+                    // Store if lhs is a partial pointer
+                    if (sem_var->Type()->Is<sem::Pointer>()) {
+                        auto* init = sem_.Get(decl->variable->constructor);
+                        if (auto* unary_init = init->Declaration()->As<ast::UnaryOpExpression>()) {
+                            auto* e = UnwrapIndirectAndAddressOfChain(unary_init);
+                            if (e->IsAnyOf<ast::IndexAccessorExpression,
+                                           ast::MemberAccessorExpression>()) {
+                                current_function_->partial_ptrs.insert(sem_var);
+                            }
+                        }
+                    }
                 } else {
                     node = cf;
                 }
-                current_function_->variables.Set(sem_.Get(decl->variable), node);
+                current_function_->variables.Set(sem_var, node);
 
                 if (decl->variable->Is<ast::Var>()) {
                     current_function_->local_var_decls.insert(
@@ -1126,11 +1160,37 @@
             });
     }
 
+    /// @param u unary expression with op == kIndirection
+    /// @returns true if `u` is an indirection unary expression that ultimately dereferences a
+    /// partial pointer, false otherwise.
+    bool IsDerefOfPartialPointer(const ast::UnaryOpExpression* u) {
+        TINT_ASSERT(Resolver, u->op == ast::UnaryOp::kIndirection);
+
+        // To determine if we're dereferencing a partial pointer, unwrap *&
+        // chains; if the final expression is an identifier, see if it's a
+        // partial pointer. If it's not an identifier, then it must be an
+        // index/acessor expression, and thus a partial pointer.
+        auto* e = UnwrapIndirectAndAddressOfChain(u);
+        if (auto* var_user = sem_.Get<sem::VariableUser>(e)) {
+            if (current_function_->partial_ptrs.count(var_user->Variable())) {
+                return true;
+            }
+        } else {
+            TINT_ASSERT(
+                Resolver,
+                (e->IsAnyOf<ast::IndexAccessorExpression, ast::MemberAccessorExpression>()));
+            return true;
+        }
+        return false;
+    }
+
     /// Process an LValue expression.
     /// @param cf the input control flow node
     /// @param expr the expression to process
     /// @returns a pair of (control flow node, variable node)
-    std::pair<Node*, Node*> ProcessLValueExpression(Node* cf, const ast::Expression* expr) {
+    std::pair<Node*, Node*> ProcessLValueExpression(Node* cf,
+                                                    const ast::Expression* expr,
+                                                    bool is_partial_reference = false) {
         return Switch(
             expr,
 
@@ -1144,9 +1204,11 @@
                     auto* value = CreateNode(name + "_lvalue");
                     auto* old_value = current_function_->variables.Set(local, value);
 
-                    // Aggregate values link back to their previous value, as they can never become
-                    // uniform again.
-                    if (!local->Type()->UnwrapRef()->is_scalar() && old_value) {
+                    // If i is part of an expression that is a partial reference to a variable (e.g.
+                    // index or member access), we link back to the variable's previous value. If
+                    // the previous value was non-uniform, a partial assignment will not make it
+                    // uniform.
+                    if (is_partial_reference && old_value) {
                         value->AddEdge(old_value);
                     }
 
@@ -1160,14 +1222,15 @@
             },
 
             [&](const ast::IndexAccessorExpression* i) {
-                auto [cf1, l1] = ProcessLValueExpression(cf, i->object);
+                auto [cf1, l1] =
+                    ProcessLValueExpression(cf, i->object, /*is_partial_reference*/ true);
                 auto [cf2, v2] = ProcessExpression(cf1, i->index);
                 l1->AddEdge(v2);
                 return std::pair<Node*, Node*>(cf2, l1);
             },
 
             [&](const ast::MemberAccessorExpression* m) {
-                return ProcessLValueExpression(cf, m->structure);
+                return ProcessLValueExpression(cf, m->structure, /*is_partial_reference*/ true);
             },
 
             [&](const ast::UnaryOpExpression* u) {
@@ -1179,15 +1242,17 @@
                     auto* deref = CreateNode(name + "_deref");
                     auto* old_value = current_function_->variables.Set(source_var, deref);
 
-                    // Aggregate values link back to their previous value, as they can never become
-                    // uniform again.
-                    if (!source_var->Type()->UnwrapRef()->UnwrapPtr()->is_scalar() && old_value) {
-                        deref->AddEdge(old_value);
+                    if (old_value) {
+                        // If derefercing a partial reference or partial pointer, we link back to
+                        // the variable's previous value. If the previous value was non-uniform, a
+                        // partial assignment will not make it uniform.
+                        if (is_partial_reference || IsDerefOfPartialPointer(u)) {
+                            deref->AddEdge(old_value);
+                        }
                     }
-
                     return std::pair<Node*, Node*>(cf, deref);
                 }
-                return ProcessLValueExpression(cf, u->expr);
+                return ProcessLValueExpression(cf, u->expr, is_partial_reference);
             },
 
             [&](Default) {
diff --git a/src/tint/resolver/uniformity_test.cc b/src/tint/resolver/uniformity_test.cc
index 52cccbe..efff223 100644
--- a/src/tint/resolver/uniformity_test.cc
+++ b/src/tint/resolver/uniformity_test.cc
@@ -5450,7 +5450,7 @@
 }
 
 TEST_F(UniformityAnalysisTest, VectorElement_ElementBecomesUniform) {
-    // For aggregate types, we conservatively consider them to be forever non-uniform once they
+    // For aggregate types, we conservatively consider them to be non-uniform once they
     // become non-uniform. Test that after assigning a uniform value to an element, that element is
     // still considered to be non-uniform.
     std::string src = R"(
@@ -5482,8 +5482,156 @@
 )");
 }
 
+TEST_F(UniformityAnalysisTest, VectorElement_VectorBecomesUniform_FullAssignment) {
+    std::string src = R"(
+@group(0) @binding(0) var<storage, read_write> rw : i32;
+
+fn foo() {
+  var v : vec4<i32>;
+  v[1] = rw;
+  v = vec4(1, 2, 3, 4);
+  if (v[1] == 0) {
+    workgroupBarrier();
+  }
+}
+)";
+
+    RunTest(src, true);
+}
+
+TEST_F(UniformityAnalysisTest, VectorElementViaMember_VectorBecomesUniform_FullAssignment) {
+    std::string src = R"(
+@group(0) @binding(0) var<storage, read_write> rw : i32;
+
+fn foo() {
+  var v : vec4<i32>;
+  v.y = rw;
+  v = vec4(1, 2, 3, 4);
+  if (v.y == 0) {
+    workgroupBarrier();
+  }
+}
+)";
+
+    RunTest(src, true);
+}
+
+TEST_F(UniformityAnalysisTest, VectorElement_VectorBecomesUniform_ThroughPointer_FullAssignment) {
+    std::string src = R"(
+@group(0) @binding(0) var<storage, read_write> rw : i32;
+
+fn foo() {
+  var v : vec4<i32>;
+  v[1] = rw;
+  *(&v) = vec4(1, 2, 3, 4);
+  if (v[1] == 0) {
+    workgroupBarrier();
+  }
+}
+)";
+
+    RunTest(src, true);
+}
+
+TEST_F(UniformityAnalysisTest,
+       VectorElement_VectorBecomesUniform_ThroughPointerChain_FullAssignment) {
+    std::string src = R"(
+@group(0) @binding(0) var<storage, read_write> rw : i32;
+
+fn foo() {
+  var v : vec4<i32>;
+  v[1] = rw;
+  *(&(*(&(*(&v))))) = vec4(1, 2, 3, 4);
+  if (v[1] == 0) {
+    workgroupBarrier();
+  }
+}
+)";
+
+    RunTest(src, true);
+}
+
+TEST_F(UniformityAnalysisTest,
+       VectorElement_VectorBecomesUniform_ThroughCapturedPointer_FullAssignment) {
+    std::string src = R"(
+@group(0) @binding(0) var<storage, read_write> rw : i32;
+
+fn foo() {
+  var v : vec4<i32>;
+  v[1] = rw;
+  let p = &v;
+  *p = vec4(1, 2, 3, 4);
+  if (v[1] == 0) {
+    workgroupBarrier();
+  }
+}
+)";
+
+    RunTest(src, true);
+}
+
+TEST_F(UniformityAnalysisTest, VectorElement_VectorBecomesUniform_PartialAssignment) {
+    std::string src = R"(
+@group(0) @binding(0) var<storage, read_write> rw : i32;
+
+fn foo() {
+  var v : vec4<i32>;
+  v[1] = rw;
+  v = vec4(1, 2, 3, v[3]);
+  if (v[1] == 0) {
+    workgroupBarrier();
+  }
+}
+)";
+
+    RunTest(src, false);
+    EXPECT_EQ(error_,
+              R"(test:9:5 error: 'workgroupBarrier' must only be called from uniform control flow
+    workgroupBarrier();
+    ^^^^^^^^^^^^^^^^
+
+test:8:3 note: control flow depends on non-uniform value
+  if (v[1] == 0) {
+  ^^
+
+test:6:10 note: reading from read_write storage buffer 'rw' may result in a non-uniform value
+  v[1] = rw;
+         ^^
+)");
+}
+
+TEST_F(UniformityAnalysisTest, VectorElementViaMember_VectorBecomesUniform_PartialAssignment) {
+    std::string src = R"(
+@group(0) @binding(0) var<storage, read_write> rw : i32;
+
+fn foo() {
+  var v : vec4<i32>;
+  v.y = rw;
+  v = vec4(1, 2, 3, v.w);
+  if (v.y == 0) {
+    workgroupBarrier();
+  }
+}
+)";
+
+    RunTest(src, false);
+    EXPECT_EQ(error_,
+              R"(test:9:5 error: 'workgroupBarrier' must only be called from uniform control flow
+    workgroupBarrier();
+    ^^^^^^^^^^^^^^^^
+
+test:8:3 note: control flow depends on non-uniform value
+  if (v.y == 0) {
+  ^^
+
+test:6:9 note: reading from read_write storage buffer 'rw' may result in a non-uniform value
+  v.y = rw;
+        ^^
+)");
+}
+
 TEST_F(UniformityAnalysisTest, VectorElement_DifferentElementBecomesUniform) {
-    // For aggregate types, we conservatively consider them to be forever non-uniform once they
+    // For aggregate types, we conservatively consider them to be non-uniform once they
     // become non-uniform. Test that after assigning a uniform value to an element, the whole vector
     // is still considered to be non-uniform.
     std::string src = R"(
@@ -5544,6 +5692,467 @@
 )");
 }
 
+TEST_F(UniformityAnalysisTest, MatrixElement_ElementBecomesUniform) {
+    // For aggregate types, we conservatively consider them to be non-uniform once they
+    // become non-uniform. Test that after assigning a uniform value to an element, that element is
+    // still considered to be non-uniform.
+    std::string src = R"(
+@group(0) @binding(0) var<storage, read_write> rw : f32;
+
+fn foo() {
+  var m : mat3x3<f32>;
+  m[1][1] = rw;
+  m[1][1] = 42.0;
+  if (m[1][1] == 0.0) {
+    workgroupBarrier();
+  }
+}
+)";
+
+    RunTest(src, false);
+    EXPECT_EQ(error_,
+              R"(test:9:5 error: 'workgroupBarrier' must only be called from uniform control flow
+    workgroupBarrier();
+    ^^^^^^^^^^^^^^^^
+
+test:8:3 note: control flow depends on non-uniform value
+  if (m[1][1] == 0.0) {
+  ^^
+
+test:6:13 note: reading from read_write storage buffer 'rw' may result in a non-uniform value
+  m[1][1] = rw;
+            ^^
+)");
+}
+
+TEST_F(UniformityAnalysisTest, MatrixElement_ElementBecomesUniform_FullAssignment) {
+    std::string src = R"(
+@group(0) @binding(0) var<storage, read_write> rw : f32;
+
+fn foo() {
+  var m : mat3x3<f32>;
+  m[1][1] = rw;
+  m = mat3x3<f32>(vec3(1.0, 2.0, 3.0), vec3(4.0, 5.0, 6.0), vec3(7.0, 8.0, 9.0));
+  if (m[1][1] == 0.0) {
+    workgroupBarrier();
+  }
+}
+)";
+
+    RunTest(src, true);
+}
+
+TEST_F(UniformityAnalysisTest, MatrixElement_ElementBecomesUniform_ThroughPointer_FullAssignment) {
+    std::string src = R"(
+@group(0) @binding(0) var<storage, read_write> rw : f32;
+
+fn foo() {
+  var m : mat3x3<f32>;
+  m[1][1] = rw;
+  *(&m) = mat3x3<f32>(vec3(1.0, 2.0, 3.0), vec3(4.0, 5.0, 6.0), vec3(7.0, 8.0, 9.0));
+  if (m[1][1] == 0.0) {
+    workgroupBarrier();
+  }
+}
+)";
+
+    RunTest(src, true);
+}
+
+TEST_F(UniformityAnalysisTest,
+       MatrixElement_ElementBecomesUniform_ThroughPointerChain_FullAssignment) {
+    std::string src = R"(
+@group(0) @binding(0) var<storage, read_write> rw : f32;
+
+fn foo() {
+  var m : mat3x3<f32>;
+  m[1][1] = rw;
+  *(&(*(&(*(&m))))) = mat3x3<f32>(vec3(1.0, 2.0, 3.0), vec3(4.0, 5.0, 6.0), vec3(7.0, 8.0, 9.0));
+  if (m[1][1] == 0.0) {
+    workgroupBarrier();
+  }
+}
+)";
+
+    RunTest(src, true);
+}
+
+TEST_F(UniformityAnalysisTest,
+       MatrixElement_ElementBecomesUniform_ThroughCapturedPointer_FullAssignment) {
+    std::string src = R"(
+@group(0) @binding(0) var<storage, read_write> rw : f32;
+
+fn foo() {
+  var m : mat3x3<f32>;
+  m[1][1] = rw;
+  let p = &m;
+  *p = mat3x3<f32>(vec3(1.0, 2.0, 3.0), vec3(4.0, 5.0, 6.0), vec3(7.0, 8.0, 9.0));
+  if (m[1][1] == 0.0) {
+    workgroupBarrier();
+  }
+}
+)";
+
+    RunTest(src, true);
+}
+
+TEST_F(UniformityAnalysisTest, MatrixElement_ColumnBecomesUniform) {
+    // For aggregate types, we conservatively consider them to be non-uniform once they
+    // become non-uniform. Test that after assigning a uniform value to an element, that element is
+    // still considered to be non-uniform.
+    std::string src = R"(
+@group(0) @binding(0) var<storage, read_write> rw : f32;
+
+fn foo() {
+  var m : mat3x3<f32>;
+  m[1][1] = rw;
+  m[1] = vec3(0.0, 42.0, 0.0);
+  if (m[1][1] == 0.0) {
+    workgroupBarrier();
+  }
+}
+)";
+
+    RunTest(src, false);
+    EXPECT_EQ(error_,
+              R"(test:9:5 error: 'workgroupBarrier' must only be called from uniform control flow
+    workgroupBarrier();
+    ^^^^^^^^^^^^^^^^
+
+test:8:3 note: control flow depends on non-uniform value
+  if (m[1][1] == 0.0) {
+  ^^
+
+test:6:13 note: reading from read_write storage buffer 'rw' may result in a non-uniform value
+  m[1][1] = rw;
+            ^^
+)");
+}
+
+TEST_F(UniformityAnalysisTest, MatrixElement_ColumnBecomesUniform_ThroughPartialPointer) {
+    // For aggregate types, we conservatively consider them to be non-uniform once they
+    // become non-uniform. Test that after assigning a uniform value to an element, that element is
+    // still considered to be non-uniform.
+    std::string src = R"(
+@group(0) @binding(0) var<storage, read_write> rw : f32;
+
+fn foo() {
+  var m : mat3x3<f32>;
+  m[1][1] = rw;
+  *(&(m[1])) = vec3(0.0, 42.0, 0.0);
+  if (m[1][1] == 0.0) {
+    workgroupBarrier();
+  }
+}
+)";
+
+    RunTest(src, false);
+    EXPECT_EQ(error_,
+              R"(test:9:5 error: 'workgroupBarrier' must only be called from uniform control flow
+    workgroupBarrier();
+    ^^^^^^^^^^^^^^^^
+
+test:8:3 note: control flow depends on non-uniform value
+  if (m[1][1] == 0.0) {
+  ^^
+
+test:6:13 note: reading from read_write storage buffer 'rw' may result in a non-uniform value
+  m[1][1] = rw;
+            ^^
+)");
+}
+
+TEST_F(UniformityAnalysisTest, MatrixElement_ColumnBecomesUniform_ThroughPartialPointerChain) {
+    // For aggregate types, we conservatively consider them to be non-uniform once they
+    // become non-uniform. Test that after assigning a uniform value to an element, that element is
+    // still considered to be non-uniform.
+    std::string src = R"(
+@group(0) @binding(0) var<storage, read_write> rw : f32;
+
+fn foo() {
+  var m : mat3x3<f32>;
+  m[1][1] = rw;
+  *(&(*(&(m[1])))) = vec3(0.0, 42.0, 0.0);
+  if (m[1][1] == 0.0) {
+    workgroupBarrier();
+  }
+}
+)";
+
+    RunTest(src, false);
+    EXPECT_EQ(error_,
+              R"(test:9:5 error: 'workgroupBarrier' must only be called from uniform control flow
+    workgroupBarrier();
+    ^^^^^^^^^^^^^^^^
+
+test:8:3 note: control flow depends on non-uniform value
+  if (m[1][1] == 0.0) {
+  ^^
+
+test:6:13 note: reading from read_write storage buffer 'rw' may result in a non-uniform value
+  m[1][1] = rw;
+            ^^
+)");
+}
+
+TEST_F(UniformityAnalysisTest, MatrixElement_ColumnBecomesUniform_ThroughCapturedPartialPointer) {
+    // For aggregate types, we conservatively consider them to be non-uniform once they
+    // become non-uniform. Test that after assigning a uniform value to an element, that element is
+    // still considered to be non-uniform.
+    std::string src = R"(
+@group(0) @binding(0) var<storage, read_write> rw : f32;
+
+fn foo() {
+  var m : mat3x3<f32>;
+  let p = &m[1];
+  m[1][1] = rw;
+  *p = vec3(0.0, 42.0, 0.0);
+  if (m[1][1] == 0.0) {
+    workgroupBarrier();
+  }
+}
+)";
+
+    RunTest(src, false);
+    EXPECT_EQ(error_,
+              R"(test:10:5 error: 'workgroupBarrier' must only be called from uniform control flow
+    workgroupBarrier();
+    ^^^^^^^^^^^^^^^^
+
+test:9:3 note: control flow depends on non-uniform value
+  if (m[1][1] == 0.0) {
+  ^^
+
+test:7:13 note: reading from read_write storage buffer 'rw' may result in a non-uniform value
+  m[1][1] = rw;
+            ^^
+)");
+}
+
+TEST_F(UniformityAnalysisTest,
+       MatrixElement_ColumnBecomesUniform_ThroughCapturedPartialPointerChain) {
+    // For aggregate types, we conservatively consider them to be non-uniform once they
+    // become non-uniform. Test that after assigning a uniform value to an element, that element is
+    // still considered to be non-uniform.
+    std::string src = R"(
+@group(0) @binding(0) var<storage, read_write> rw : f32;
+
+fn foo() {
+  var m : mat3x3<f32>;
+  let p = &m[1];
+  m[1][1] = rw;
+  *(&(*p)) = vec3(0.0, 42.0, 0.0);
+  if (m[1][1] == 0.0) {
+    workgroupBarrier();
+  }
+}
+)";
+
+    RunTest(src, false);
+    EXPECT_EQ(error_,
+              R"(test:10:5 error: 'workgroupBarrier' must only be called from uniform control flow
+    workgroupBarrier();
+    ^^^^^^^^^^^^^^^^
+
+test:9:3 note: control flow depends on non-uniform value
+  if (m[1][1] == 0.0) {
+  ^^
+
+test:7:13 note: reading from read_write storage buffer 'rw' may result in a non-uniform value
+  m[1][1] = rw;
+            ^^
+)");
+}
+
+TEST_F(UniformityAnalysisTest, MatrixElement_ColumnBecomesUniform_ThroughCapturedPointer) {
+    // For aggregate types, we conservatively consider them to be non-uniform once they
+    // become non-uniform. Test that after assigning a uniform value to an element, that element is
+    // still considered to be non-uniform.
+    std::string src = R"(
+@group(0) @binding(0) var<storage, read_write> rw : f32;
+
+fn foo() {
+  var m : mat3x3<f32>;
+  let p = &m;
+  m[1][1] = rw;
+  (*p)[1] = vec3(0.0, 42.0, 0.0);
+  if (m[1][1] == 0.0) {
+    workgroupBarrier();
+  }
+}
+)";
+
+    RunTest(src, false);
+    EXPECT_EQ(error_,
+              R"(test:10:5 error: 'workgroupBarrier' must only be called from uniform control flow
+    workgroupBarrier();
+    ^^^^^^^^^^^^^^^^
+
+test:9:3 note: control flow depends on non-uniform value
+  if (m[1][1] == 0.0) {
+  ^^
+
+test:7:13 note: reading from read_write storage buffer 'rw' may result in a non-uniform value
+  m[1][1] = rw;
+            ^^
+)");
+}
+
+TEST_F(UniformityAnalysisTest, MatrixElement_MatrixBecomesUniform_PartialAssignment) {
+    std::string src = R"(
+@group(0) @binding(0) var<storage, read_write> rw : f32;
+
+fn foo() {
+  var m : mat3x3<f32>;
+  m[1][1] = rw;
+  m = mat3x3<f32>(vec3(1.0, 2.0, 3.0), vec3(4.0, 5.0, 6.0), m[2]);
+  if (m[1][1] == 0.0) {
+    workgroupBarrier();
+  }
+}
+)";
+
+    RunTest(src, false);
+    EXPECT_EQ(error_,
+              R"(test:9:5 error: 'workgroupBarrier' must only be called from uniform control flow
+    workgroupBarrier();
+    ^^^^^^^^^^^^^^^^
+
+test:8:3 note: control flow depends on non-uniform value
+  if (m[1][1] == 0.0) {
+  ^^
+
+test:6:13 note: reading from read_write storage buffer 'rw' may result in a non-uniform value
+  m[1][1] = rw;
+            ^^
+)");
+}
+
+TEST_F(UniformityAnalysisTest,
+       MatrixElement_MatrixBecomesUniform_PartialAssignment_ThroughPointer) {
+    std::string src = R"(
+@group(0) @binding(0) var<storage, read_write> rw : f32;
+
+fn foo() {
+  var m : mat3x3<f32>;
+  m[1][1] = rw;
+  *(&m) = mat3x3<f32>(vec3(1.0, 2.0, 3.0), vec3(4.0, 5.0, 6.0), m[2]);
+  if (m[1][1] == 0.0) {
+    workgroupBarrier();
+  }
+}
+)";
+
+    RunTest(src, false);
+    EXPECT_EQ(error_,
+              R"(test:9:5 error: 'workgroupBarrier' must only be called from uniform control flow
+    workgroupBarrier();
+    ^^^^^^^^^^^^^^^^
+
+test:8:3 note: control flow depends on non-uniform value
+  if (m[1][1] == 0.0) {
+  ^^
+
+test:6:13 note: reading from read_write storage buffer 'rw' may result in a non-uniform value
+  m[1][1] = rw;
+            ^^
+)");
+}
+
+TEST_F(UniformityAnalysisTest,
+       MatrixElement_MatrixBecomesUniform_PartialAssignment_ThroughCapturedPointer) {
+    std::string src = R"(
+@group(0) @binding(0) var<storage, read_write> rw : f32;
+
+fn foo() {
+  var m : mat3x3<f32>;
+  let p = &m;
+  m[1][1] = rw;
+  *p = mat3x3<f32>(vec3(1.0, 2.0, 3.0), vec3(4.0, 5.0, 6.0), (*p)[2]);
+  if (m[1][1] == 0.0) {
+    workgroupBarrier();
+  }
+}
+)";
+
+    RunTest(src, false);
+    EXPECT_EQ(error_,
+              R"(test:10:5 error: 'workgroupBarrier' must only be called from uniform control flow
+    workgroupBarrier();
+    ^^^^^^^^^^^^^^^^
+
+test:9:3 note: control flow depends on non-uniform value
+  if (m[1][1] == 0.0) {
+  ^^
+
+test:7:13 note: reading from read_write storage buffer 'rw' may result in a non-uniform value
+  m[1][1] = rw;
+            ^^
+)");
+}
+
+TEST_F(UniformityAnalysisTest,
+       MatrixElement_MatrixBecomesUniform_PartialAssignment_ThroughCapturedPointerChain) {
+    std::string src = R"(
+@group(0) @binding(0) var<storage, read_write> rw : f32;
+
+fn foo() {
+  var m : mat3x3<f32>;
+  let p = &(*(&m));
+  m[1][1] = rw;
+  *p = mat3x3<f32>(vec3(1.0, 2.0, 3.0), vec3(4.0, 5.0, 6.0), (*p)[2]);
+  if (m[1][1] == 0.0) {
+    workgroupBarrier();
+  }
+}
+)";
+
+    RunTest(src, false);
+    EXPECT_EQ(error_,
+              R"(test:10:5 error: 'workgroupBarrier' must only be called from uniform control flow
+    workgroupBarrier();
+    ^^^^^^^^^^^^^^^^
+
+test:9:3 note: control flow depends on non-uniform value
+  if (m[1][1] == 0.0) {
+  ^^
+
+test:7:13 note: reading from read_write storage buffer 'rw' may result in a non-uniform value
+  m[1][1] = rw;
+            ^^
+)");
+}
+
+TEST_F(UniformityAnalysisTest, MatrixElement_DifferentElementBecomesUniform) {
+    std::string src = R"(
+@group(0) @binding(0) var<storage, read_write> rw : f32;
+
+fn foo() {
+  var m : mat3x3<f32>;
+  m[1][1] = rw;
+  m[2][2] = 42.0;
+  if (m[1][1] == 0.0) {
+    workgroupBarrier();
+  }
+}
+)";
+
+    RunTest(src, false);
+    EXPECT_EQ(error_,
+              R"(test:9:5 error: 'workgroupBarrier' must only be called from uniform control flow
+    workgroupBarrier();
+    ^^^^^^^^^^^^^^^^
+
+test:8:3 note: control flow depends on non-uniform value
+  if (m[1][1] == 0.0) {
+  ^^
+
+test:6:13 note: reading from read_write storage buffer 'rw' may result in a non-uniform value
+  m[1][1] = rw;
+            ^^
+)");
+}
+
 TEST_F(UniformityAnalysisTest, StructMember_Uniform) {
     std::string src = R"(
 struct S {
@@ -5680,7 +6289,7 @@
 }
 
 TEST_F(UniformityAnalysisTest, StructMember_MemberBecomesUniform) {
-    // For aggregate types, we conservatively consider them to be forever non-uniform once they
+    // For aggregate types, we conservatively consider them to be non-uniform once they
     // become non-uniform. Test that after assigning a uniform value to a member, that member is
     // still considered to be non-uniform.
     std::string src = R"(
@@ -5716,8 +6325,312 @@
 )");
 }
 
+TEST_F(UniformityAnalysisTest, StructMember_MemberBecomesUniformThroughCapturedPointer) {
+    // For aggregate types, we conservatively consider them to be non-uniform once they
+    // become non-uniform. Test that after assigning a uniform value to a member, that member is
+    // still considered to be non-uniform.
+    std::string src = R"(
+struct S {
+  a : i32,
+  b : i32,
+}
+@group(0) @binding(0) var<storage, read_write> rw : i32;
+
+fn foo() {
+  var s : S;
+  let p = &s;
+  s.a = rw;
+  (*p).a = 0;
+  if (s.a == 0) {
+    workgroupBarrier();
+  }
+}
+)";
+
+    RunTest(src, false);
+    EXPECT_EQ(error_,
+              R"(test:14:5 error: 'workgroupBarrier' must only be called from uniform control flow
+    workgroupBarrier();
+    ^^^^^^^^^^^^^^^^
+
+test:13:3 note: control flow depends on non-uniform value
+  if (s.a == 0) {
+  ^^
+
+test:11:9 note: reading from read_write storage buffer 'rw' may result in a non-uniform value
+  s.a = rw;
+        ^^
+)");
+}
+
+TEST_F(UniformityAnalysisTest, StructMember_MemberBecomesUniformThroughCapturedPartialPointer) {
+    // For aggregate types, we conservatively consider them to be non-uniform once they
+    // become non-uniform. Test that after assigning a uniform value to a member, that member is
+    // still considered to be non-uniform.
+    std::string src = R"(
+struct S {
+  a : i32,
+  b : i32,
+}
+@group(0) @binding(0) var<storage, read_write> rw : i32;
+
+fn foo() {
+  var s : S;
+  let p = &s.a;
+  s.a = rw;
+  (*p) = 0;
+  if (s.a == 0) {
+    workgroupBarrier();
+  }
+}
+)";
+
+    RunTest(src, false);
+    EXPECT_EQ(error_,
+              R"(test:14:5 error: 'workgroupBarrier' must only be called from uniform control flow
+    workgroupBarrier();
+    ^^^^^^^^^^^^^^^^
+
+test:13:3 note: control flow depends on non-uniform value
+  if (s.a == 0) {
+  ^^
+
+test:11:9 note: reading from read_write storage buffer 'rw' may result in a non-uniform value
+  s.a = rw;
+        ^^
+)");
+}
+
+TEST_F(UniformityAnalysisTest, StructMember_StructBecomesUniform_FullAssignment) {
+    std::string src = R"(
+struct S {
+  a : i32,
+  b : i32,
+}
+@group(0) @binding(0) var<storage, read_write> rw : i32;
+
+fn foo() {
+  var s : S;
+  s.a = rw;
+  s = S(1, 2);
+  if (s.a == 0) {
+    workgroupBarrier();
+  }
+}
+)";
+
+    RunTest(src, true);
+}
+
+TEST_F(UniformityAnalysisTest, StructMember_StructBecomesUniform_PartialAssignment) {
+    std::string src = R"(
+struct S {
+  a : i32,
+  b : i32,
+}
+@group(0) @binding(0) var<storage, read_write> rw : i32;
+
+fn foo() {
+  var s : S;
+  s.a = rw;
+  s = S(1, s.b);
+  if (s.a == 0) {
+    workgroupBarrier();
+  }
+}
+)";
+
+    RunTest(src, false);
+    EXPECT_EQ(error_,
+              R"(test:13:5 error: 'workgroupBarrier' must only be called from uniform control flow
+    workgroupBarrier();
+    ^^^^^^^^^^^^^^^^
+
+test:12:3 note: control flow depends on non-uniform value
+  if (s.a == 0) {
+  ^^
+
+test:10:9 note: reading from read_write storage buffer 'rw' may result in a non-uniform value
+  s.a = rw;
+        ^^
+)");
+}
+
+TEST_F(UniformityAnalysisTest, StructMember_StructBecomesUniform_FullAssignment_ThroughPointer) {
+    std::string src = R"(
+struct S {
+  a : i32,
+  b : i32,
+}
+@group(0) @binding(0) var<storage, read_write> rw : i32;
+
+fn foo() {
+  var s : S;
+  s.a = rw;
+  *(&s) = S(1, 2);
+  if (s.a == 0) {
+    workgroupBarrier();
+  }
+}
+)";
+
+    RunTest(src, true);
+}
+
+TEST_F(UniformityAnalysisTest,
+       StructMember_StructBecomesUniform_FullAssignment_ThroughCapturedPointer) {
+    std::string src = R"(
+struct S {
+  a : i32,
+  b : i32,
+}
+@group(0) @binding(0) var<storage, read_write> rw : i32;
+
+fn foo() {
+  var s : S;
+  let p = &s;
+  s.a = rw;
+  *p = S(1, 2);
+  if (s.a == 0) {
+    workgroupBarrier();
+  }
+}
+)";
+
+    RunTest(src, true);
+}
+
+TEST_F(UniformityAnalysisTest,
+       StructMember_StructBecomesUniform_FullAssignment_ThroughCapturedPointerChain) {
+    std::string src = R"(
+struct S {
+  a : i32,
+  b : i32,
+}
+@group(0) @binding(0) var<storage, read_write> rw : i32;
+
+fn foo() {
+  var s : S;
+  let p = &(*(&s));
+  s.a = rw;
+  *p = S(1, 2);
+  if (s.a == 0) {
+    workgroupBarrier();
+  }
+}
+)";
+
+    RunTest(src, true);
+}
+
+TEST_F(UniformityAnalysisTest, StructMember_StructBecomesUniform_PartialAssignment_ThroughPointer) {
+    std::string src = R"(
+struct S {
+  a : i32,
+  b : i32,
+}
+@group(0) @binding(0) var<storage, read_write> rw : i32;
+
+fn foo() {
+  var s : S;
+  s.a = rw;
+  *(&s) = S(1, (*(&s)).b);
+  if (s.a == 0) {
+    workgroupBarrier();
+  }
+}
+)";
+
+    RunTest(src, false);
+    EXPECT_EQ(error_,
+              R"(test:13:5 error: 'workgroupBarrier' must only be called from uniform control flow
+    workgroupBarrier();
+    ^^^^^^^^^^^^^^^^
+
+test:12:3 note: control flow depends on non-uniform value
+  if (s.a == 0) {
+  ^^
+
+test:10:9 note: reading from read_write storage buffer 'rw' may result in a non-uniform value
+  s.a = rw;
+        ^^
+)");
+}
+
+TEST_F(UniformityAnalysisTest,
+       StructMember_StructBecomesUniform_PartialAssignment_ThroughCapturedPointer) {
+    std::string src = R"(
+struct S {
+  a : i32,
+  b : i32,
+}
+@group(0) @binding(0) var<storage, read_write> rw : i32;
+
+fn foo() {
+  var s : S;
+  let p = &s;
+  s.a = rw;
+  *p = S(1, (*p).b);
+  if (s.a == 0) {
+    workgroupBarrier();
+  }
+}
+)";
+
+    RunTest(src, false);
+    EXPECT_EQ(error_,
+              R"(test:14:5 error: 'workgroupBarrier' must only be called from uniform control flow
+    workgroupBarrier();
+    ^^^^^^^^^^^^^^^^
+
+test:13:3 note: control flow depends on non-uniform value
+  if (s.a == 0) {
+  ^^
+
+test:11:9 note: reading from read_write storage buffer 'rw' may result in a non-uniform value
+  s.a = rw;
+        ^^
+)");
+}
+
+TEST_F(UniformityAnalysisTest,
+       StructMember_StructBecomesUniform_PartialAssignment_ThroughCapturedPointerChain) {
+    std::string src = R"(
+struct S {
+  a : i32,
+  b : i32,
+}
+@group(0) @binding(0) var<storage, read_write> rw : i32;
+
+fn foo() {
+  var s : S;
+  let p = &(*(&s));
+  s.a = rw;
+  *p = S(1, (*p).b);
+  if (s.a == 0) {
+    workgroupBarrier();
+  }
+}
+)";
+
+    RunTest(src, false);
+    EXPECT_EQ(error_,
+              R"(test:14:5 error: 'workgroupBarrier' must only be called from uniform control flow
+    workgroupBarrier();
+    ^^^^^^^^^^^^^^^^
+
+test:13:3 note: control flow depends on non-uniform value
+  if (s.a == 0) {
+  ^^
+
+test:11:9 note: reading from read_write storage buffer 'rw' may result in a non-uniform value
+  s.a = rw;
+        ^^
+)");
+}
+
 TEST_F(UniformityAnalysisTest, StructMember_DifferentMemberBecomesUniform) {
-    // For aggregate types, we conservatively consider them to be forever non-uniform once they
+    // For aggregate types, we conservatively consider them to be non-uniform once they
     // become non-uniform. Test that after assigning a uniform value to a member, the whole struct
     // is still considered to be non-uniform.
     std::string src = R"(
@@ -5868,7 +6781,8 @@
 )");
 }
 
-TEST_F(UniformityAnalysisTest, ArrayElement_DifferentElementBecomesNonUniformThroughPointer) {
+TEST_F(UniformityAnalysisTest,
+       ArrayElement_DifferentElementBecomesNonUniformThroughPartialPointer) {
     std::string src = R"(
 @group(0) @binding(0) var<storage, read_write> rw : i32;
 
@@ -5931,8 +6845,55 @@
 )");
 }
 
+TEST_F(UniformityAnalysisTest, ArrayElement_ElementBecomesUniform_FullAssignment) {
+    std::string src = R"(
+@group(0) @binding(0) var<storage, read_write> rw : i32;
+
+fn foo() {
+  var arr : array<i32, 4>;
+  arr[1] = rw;
+  arr = array<i32, 4>(1, 2, 3, 4);
+  if (arr[1] == 0) {
+    workgroupBarrier();
+  }
+}
+)";
+
+    RunTest(src, true);
+}
+
+TEST_F(UniformityAnalysisTest, ArrayElement_ElementBecomesUniform_PartialAssignment) {
+    std::string src = R"(
+@group(0) @binding(0) var<storage, read_write> rw : i32;
+
+fn foo() {
+  var arr : array<i32, 4>;
+  arr[1] = rw;
+  arr = array<i32, 4>(1, 2, 3, arr[3]);
+  if (arr[1] == 0) {
+    workgroupBarrier();
+  }
+}
+)";
+
+    RunTest(src, false);
+    EXPECT_EQ(error_,
+              R"(test:9:5 error: 'workgroupBarrier' must only be called from uniform control flow
+    workgroupBarrier();
+    ^^^^^^^^^^^^^^^^
+
+test:8:3 note: control flow depends on non-uniform value
+  if (arr[1] == 0) {
+  ^^
+
+test:6:12 note: reading from read_write storage buffer 'rw' may result in a non-uniform value
+  arr[1] = rw;
+           ^^
+)");
+}
+
 TEST_F(UniformityAnalysisTest, ArrayElement_DifferentElementBecomesUniform) {
-    // For aggregate types, we conservatively consider them to be forever non-uniform once they
+    // For aggregate types, we conservatively consider them to be non-uniform once they
     // become non-uniform. Test that after assigning a uniform value to an element, the whole array
     // is still considered to be non-uniform.
     std::string src = R"(
@@ -5964,8 +6925,74 @@
 )");
 }
 
-TEST_F(UniformityAnalysisTest, ArrayElement_ElementBecomesUniformThroughPointer) {
-    // For aggregate types, we conservatively consider them to be forever non-uniform once they
+TEST_F(UniformityAnalysisTest, ArrayElement_ElementBecomesUniform_ThroughPartialPointer) {
+    // For aggregate types, we conservatively consider them to be non-uniform once they
+    // become non-uniform. Test that after assigning a uniform value to an element through a
+    // pointer, the whole array is still considered to be non-uniform.
+    std::string src = R"(
+@group(0) @binding(0) var<storage, read_write> rw : i32;
+
+fn foo() {
+  var arr : array<i32, 4>;
+  arr[1] = rw;
+  *(&(arr[2])) = 42;
+  if (arr[1] == 0) {
+    workgroupBarrier();
+  }
+}
+)";
+
+    RunTest(src, false);
+    EXPECT_EQ(error_,
+              R"(test:9:5 error: 'workgroupBarrier' must only be called from uniform control flow
+    workgroupBarrier();
+    ^^^^^^^^^^^^^^^^
+
+test:8:3 note: control flow depends on non-uniform value
+  if (arr[1] == 0) {
+  ^^
+
+test:6:12 note: reading from read_write storage buffer 'rw' may result in a non-uniform value
+  arr[1] = rw;
+           ^^
+)");
+}
+
+TEST_F(UniformityAnalysisTest, ArrayElement_ElementBecomesUniform_ThroughPartialPointerChain) {
+    // For aggregate types, we conservatively consider them to be non-uniform once they
+    // become non-uniform. Test that after assigning a uniform value to an element through a
+    // pointer, the whole array is still considered to be non-uniform.
+    std::string src = R"(
+@group(0) @binding(0) var<storage, read_write> rw : i32;
+
+fn foo() {
+  var arr : array<i32, 4>;
+  arr[1] = rw;
+  *(&(*(&(*(&(arr[2])))))) = 42;
+  if (arr[1] == 0) {
+    workgroupBarrier();
+  }
+}
+)";
+
+    RunTest(src, false);
+    EXPECT_EQ(error_,
+              R"(test:9:5 error: 'workgroupBarrier' must only be called from uniform control flow
+    workgroupBarrier();
+    ^^^^^^^^^^^^^^^^
+
+test:8:3 note: control flow depends on non-uniform value
+  if (arr[1] == 0) {
+  ^^
+
+test:6:12 note: reading from read_write storage buffer 'rw' may result in a non-uniform value
+  arr[1] = rw;
+           ^^
+)");
+}
+
+TEST_F(UniformityAnalysisTest, ArrayElement_ElementBecomesUniform_ThroughCapturedPartialPointer) {
+    // For aggregate types, we conservatively consider them to be non-uniform once they
     // become non-uniform. Test that after assigning a uniform value to an element through a
     // pointer, the whole array is still considered to be non-uniform.
     std::string src = R"(
@@ -5998,6 +7025,148 @@
 )");
 }
 
+TEST_F(UniformityAnalysisTest,
+       ArrayElement_ElementBecomesUniform_ThroughCapturedPartialPointerChain) {
+    // For aggregate types, we conservatively consider them to be non-uniform once they
+    // become non-uniform. Test that after assigning a uniform value to an element through a
+    // pointer, the whole array is still considered to be non-uniform.
+    std::string src = R"(
+@group(0) @binding(0) var<storage, read_write> rw : i32;
+
+fn foo() {
+  var arr : array<i32, 4>;
+  let pa = &(*(&arr[2]));
+  arr[1] = rw;
+  *pa = 42;
+  if (arr[1] == 0) {
+    workgroupBarrier();
+  }
+}
+)";
+
+    RunTest(src, false);
+    EXPECT_EQ(error_,
+              R"(test:10:5 error: 'workgroupBarrier' must only be called from uniform control flow
+    workgroupBarrier();
+    ^^^^^^^^^^^^^^^^
+
+test:9:3 note: control flow depends on non-uniform value
+  if (arr[1] == 0) {
+  ^^
+
+test:7:12 note: reading from read_write storage buffer 'rw' may result in a non-uniform value
+  arr[1] = rw;
+           ^^
+)");
+}
+
+TEST_F(UniformityAnalysisTest, ArrayElement_ElementBecomesUniform_ThroughCapturedPointer) {
+    // For aggregate types, we conservatively consider them to be non-uniform once they
+    // become non-uniform. Test that after assigning a uniform value to an element through a
+    // pointer, the whole array is still considered to be non-uniform.
+    std::string src = R"(
+@group(0) @binding(0) var<storage, read_write> rw : i32;
+
+fn foo() {
+  var arr : array<i32, 4>;
+  let pa = &arr;
+  arr[1] = rw;
+  (*pa)[2] = 42;
+  if (arr[1] == 0) {
+    workgroupBarrier();
+  }
+}
+)";
+
+    RunTest(src, false);
+    EXPECT_EQ(error_,
+              R"(test:10:5 error: 'workgroupBarrier' must only be called from uniform control flow
+    workgroupBarrier();
+    ^^^^^^^^^^^^^^^^
+
+test:9:3 note: control flow depends on non-uniform value
+  if (arr[1] == 0) {
+  ^^
+
+test:7:12 note: reading from read_write storage buffer 'rw' may result in a non-uniform value
+  arr[1] = rw;
+           ^^
+)");
+}
+
+TEST_F(UniformityAnalysisTest, ArrayElement_ArrayBecomesUniform_ThroughPointer_FullAssignment) {
+    std::string src = R"(
+@group(0) @binding(0) var<storage, read_write> rw : i32;
+
+fn foo() {
+  var arr : array<i32, 4>;
+  arr[1] = rw;
+  *(&arr) = array<i32, 4>();
+  if (arr[1] == 0) {
+    workgroupBarrier();
+  }
+}
+)";
+
+    RunTest(src, true);
+}
+
+TEST_F(UniformityAnalysisTest,
+       ArrayElement_ArrayBecomesUniform_ThroughPointerChain_FullAssignment) {
+    std::string src = R"(
+@group(0) @binding(0) var<storage, read_write> rw : i32;
+
+fn foo() {
+  var arr : array<i32, 4>;
+  arr[1] = rw;
+  *(&(*(&(*(&arr))))) = array<i32, 4>();
+  if (arr[1] == 0) {
+    workgroupBarrier();
+  }
+}
+)";
+
+    RunTest(src, true);
+}
+
+TEST_F(UniformityAnalysisTest,
+       ArrayElement_ArrayBecomesUniform_ThroughCapturedPointer_FullAssignment) {
+    std::string src = R"(
+@group(0) @binding(0) var<storage, read_write> rw : i32;
+
+fn foo() {
+  var arr : array<i32, 4>;
+  let pa = &arr;
+  arr[1] = rw;
+  *pa = array<i32, 4>();
+  if (arr[1] == 0) {
+    workgroupBarrier();
+  }
+}
+)";
+
+    RunTest(src, true);
+}
+
+TEST_F(UniformityAnalysisTest,
+       ArrayElement_ArrayBecomesUniform_ThroughCapturedPointerChain_FullAssignment) {
+    std::string src = R"(
+@group(0) @binding(0) var<storage, read_write> rw : i32;
+
+fn foo() {
+  var arr : array<i32, 4>;
+  let pa = &(*(&arr));
+  arr[1] = rw;
+  *pa = array<i32, 4>();
+  if (arr[1] == 0) {
+    workgroupBarrier();
+  }
+}
+)";
+
+    RunTest(src, true);
+}
+
 ////////////////////////////////////////////////////////////////////////////////
 /// Miscellaneous statement and expression tests.
 ////////////////////////////////////////////////////////////////////////////////
diff --git a/src/tint/resolver/validation_test.cc b/src/tint/resolver/validation_test.cc
index edce7eb..fb210c0 100644
--- a/src/tint/resolver/validation_test.cc
+++ b/src/tint/resolver/validation_test.cc
@@ -1243,7 +1243,7 @@
                    });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: 'align' value must be a positive, power-of-two integer");
+    EXPECT_EQ(r()->error(), "12:34 error: @align value must be a positive, power-of-two integer");
 }
 
 TEST_F(ResolverValidationTest, NonPOTStructMemberAlignAttribute) {
@@ -1252,7 +1252,7 @@
                    });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: 'align' value must be a positive, power-of-two integer");
+    EXPECT_EQ(r()->error(), "12:34 error: @align value must be a positive, power-of-two integer");
 }
 
 TEST_F(ResolverValidationTest, ZeroStructMemberAlignAttribute) {
@@ -1261,7 +1261,7 @@
                    });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: 'align' value must be a positive, power-of-two integer");
+    EXPECT_EQ(r()->error(), "12:34 error: @align value must be a positive, power-of-two integer");
 }
 
 TEST_F(ResolverValidationTest, ZeroStructMemberSizeAttribute) {
@@ -1270,7 +1270,7 @@
                    });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: 'size' must be at least as big as the type's size (4)");
+    EXPECT_EQ(r()->error(), "12:34 error: @size must be at least as big as the type's size (4)");
 }
 
 TEST_F(ResolverValidationTest, OffsetAndSizeAttribute) {
@@ -1280,9 +1280,7 @@
                    });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              "12:34 error: offset attributes cannot be used with align or size "
-              "attributes");
+    EXPECT_EQ(r()->error(), "12:34 error: @offset cannot be used with @align or @size");
 }
 
 TEST_F(ResolverValidationTest, OffsetAndAlignAttribute) {
@@ -1292,9 +1290,7 @@
                    });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              "12:34 error: offset attributes cannot be used with align or size "
-              "attributes");
+    EXPECT_EQ(r()->error(), "12:34 error: @offset cannot be used with @align or @size");
 }
 
 TEST_F(ResolverValidationTest, OffsetAndAlignAndSizeAttribute) {
@@ -1304,9 +1300,7 @@
                    });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              "12:34 error: offset attributes cannot be used with align or size "
-              "attributes");
+    EXPECT_EQ(r()->error(), "12:34 error: @offset cannot be used with @align or @size");
 }
 
 TEST_F(ResolverTest, Expr_Constructor_Cast_Pointer) {
diff --git a/src/tint/sem/array.cc b/src/tint/sem/array.cc
index 31827cb..74ef382 100644
--- a/src/tint/sem/array.cc
+++ b/src/tint/sem/array.cc
@@ -26,7 +26,7 @@
 
 namespace tint::sem {
 
-const char* Array::kErrExpectedConstantCount =
+const char* const Array::kErrExpectedConstantCount =
     "array size is an override-expression, when expected a constant-expression.\n"
     "Was the SubstituteOverride transform run?";
 
diff --git a/src/tint/sem/array.h b/src/tint/sem/array.h
index 693549d..ca66a16 100644
--- a/src/tint/sem/array.h
+++ b/src/tint/sem/array.h
@@ -111,7 +111,7 @@
   public:
     /// An error message string stating that the array count was expected to be a constant
     /// expression. Used by multiple writers and transforms.
-    static const char* kErrExpectedConstantCount;
+    static const char* const kErrExpectedConstantCount;
 
     /// Constructor
     /// @param element the array element type
diff --git a/src/tint/sem/sem_array_test.cc b/src/tint/sem/array_test.cc
similarity index 100%
rename from src/tint/sem/sem_array_test.cc
rename to src/tint/sem/array_test.cc
diff --git a/src/tint/sem/block_statement.cc b/src/tint/sem/block_statement.cc
index 51bad0f..12b8318 100644
--- a/src/tint/sem/block_statement.cc
+++ b/src/tint/sem/block_statement.cc
@@ -35,10 +35,6 @@
     return Base::Declaration()->As<ast::BlockStatement>();
 }
 
-void BlockStatement::AddDecl(const ast::Variable* var) {
-    decls_.push_back(var);
-}
-
 FunctionBlockStatement::FunctionBlockStatement(const sem::Function* function)
     : Base(function->Declaration()->body, nullptr, function) {
     TINT_ASSERT(Semantic, function);
diff --git a/src/tint/sem/block_statement.h b/src/tint/sem/block_statement.h
index 4f12122..e5d4969 100644
--- a/src/tint/sem/block_statement.h
+++ b/src/tint/sem/block_statement.h
@@ -47,16 +47,6 @@
     /// @returns the AST block statement associated with this semantic block
     /// statement
     const ast::BlockStatement* Declaration() const;
-
-    /// @returns the declarations associated with this block
-    const std::vector<const ast::Variable*>& Decls() const { return decls_; }
-
-    /// Associates a declaration with this block.
-    /// @param var a variable declaration to be added to the block
-    void AddDecl(const ast::Variable* var);
-
-  private:
-    std::vector<const ast::Variable*> decls_;
 };
 
 /// The root block statement for a function
diff --git a/src/tint/sem/statement.cc b/src/tint/sem/statement.cc
index fb8e557..685f0d6 100644
--- a/src/tint/sem/statement.cc
+++ b/src/tint/sem/statement.cc
@@ -17,8 +17,10 @@
 #include "src/tint/ast/block_statement.h"
 #include "src/tint/ast/loop_statement.h"
 #include "src/tint/ast/statement.h"
+#include "src/tint/ast/variable.h"
 #include "src/tint/sem/block_statement.h"
 #include "src/tint/sem/statement.h"
+#include "src/tint/sem/variable.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::sem::Statement);
 TINT_INSTANTIATE_TYPEINFO(tint::sem::CompoundStatement);
@@ -43,4 +45,8 @@
 
 CompoundStatement::~CompoundStatement() = default;
 
+void CompoundStatement::AddDecl(const sem::LocalVariable* var) {
+    decls_.Add(var->Declaration()->symbol, OrderedLocalVariable{decls_.Count(), var});
+}
+
 }  // namespace tint::sem
diff --git a/src/tint/sem/statement.h b/src/tint/sem/statement.h
index bdcb55c..09112d5 100644
--- a/src/tint/sem/statement.h
+++ b/src/tint/sem/statement.h
@@ -17,6 +17,8 @@
 
 #include "src/tint/sem/behavior.h"
 #include "src/tint/sem/node.h"
+#include "src/tint/symbol.h"
+#include "src/tint/utils/hashmap.h"
 
 // Forward declarations
 namespace tint::ast {
@@ -26,6 +28,7 @@
 class BlockStatement;
 class CompoundStatement;
 class Function;
+class LocalVariable;
 }  // namespace tint::sem
 
 namespace tint::sem {
@@ -128,6 +131,25 @@
 
     /// Destructor
     ~CompoundStatement() override;
+
+    /// OrderedLocalVariable describes a local variable declaration, and order of declaration.
+    struct OrderedLocalVariable {
+        /// The 0-based declaration order index of the variable
+        size_t order;
+        /// The variable
+        const LocalVariable* variable;
+    };
+
+    /// @returns a map of variable name to variable declarations associated with this block
+    const utils::Hashmap<Symbol, OrderedLocalVariable, 4>& Decls() const { return decls_; }
+
+    /// Associates a declaration with this block.
+    /// @note this method must be called in variable declaration order
+    /// @param var a variable declaration to be added to the block
+    void AddDecl(const LocalVariable* var);
+
+  private:
+    utils::Hashmap<Symbol, OrderedLocalVariable, 4> decls_;
 };
 
 template <typename Pred>
diff --git a/src/tint/sem/storage_texture_test.cc b/src/tint/sem/storage_texture_test.cc
index 8a9351e..23d6964 100644
--- a/src/tint/sem/storage_texture_test.cc
+++ b/src/tint/sem/storage_texture_test.cc
@@ -1,4 +1,4 @@
-// Copyright 2020 The Tint Authors->
+// Copyright 2020 The Tint Authors
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
diff --git a/src/tint/sem/sem_struct_test.cc b/src/tint/sem/struct_test.cc
similarity index 100%
rename from src/tint/sem/sem_struct_test.cc
rename to src/tint/sem/struct_test.cc