Import Tint changes from Dawn

Changes:
  - fbabce4ec77f087877e12e5ee9b3e0dea80ec08d [tint][ir] Validate no calls to entry points by James Price <jrprice@google.com>
  - ef011009ed1479b5dc554f26a1e7da31cd957f79 [tint][wgsl][reader] Expand expression Source spans by Ben Clayton <bclayton@google.com>
  - 842ab015bacf7d32fa978db60b0ff9c4075d79fe [tint][uniformity] Fix mutable pointer parameters by James Price <jrprice@google.com>
  - af25bec4a0ec2804567e18fe1feaf817f18c38f4 [tint][ast] Fix dynamic indices in DirectVariableAccess by Ben Clayton <bclayton@google.com>
GitOrigin-RevId: fbabce4ec77f087877e12e5ee9b3e0dea80ec08d
Change-Id: If0f9d64c7f1f3abbf37d82182caf3414609af33c
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/169800
Kokoro: Kokoro <noreply+kokoro@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
diff --git a/src/tint/lang/core/ir/transform/direct_variable_access.cc b/src/tint/lang/core/ir/transform/direct_variable_access.cc
index 12b078b..e62d52b 100644
--- a/src/tint/lang/core/ir/transform/direct_variable_access.cc
+++ b/src/tint/lang/core/ir/transform/direct_variable_access.cc
@@ -380,7 +380,7 @@
                 return variant_fn;
             });
 
-            // Repoint the target of the call to the variant.
+            // Re-point the target of the call to the variant.
             call->SetTarget(new_target);
         });
     }
diff --git a/src/tint/lang/core/ir/transform/direct_variable_access_test.cc b/src/tint/lang/core/ir/transform/direct_variable_access_test.cc
index ed37b51..e6244da 100644
--- a/src/tint/lang/core/ir/transform/direct_variable_access_test.cc
+++ b/src/tint/lang/core/ir/transform/direct_variable_access_test.cc
@@ -1158,6 +1158,128 @@
     EXPECT_EQ(expect, str());
 }
 
+TEST_F(IR_DirectVariableAccessTest_UniformAS, CallChaining2) {
+    auto* T3 = ty.vec4<i32>();
+    auto* T2 = ty.array(T3, 5);
+    auto* T1 = ty.array(T2, 5);
+    auto* T = ty.array(T1, 5);
+
+    Var* input = nullptr;
+    b.Append(b.ir.root_block,
+             [&] {  //
+                 input = b.Var("U", ty.ptr<uniform>(T));
+                 input->SetBindingPoint(0, 0);
+             });
+
+    auto* f2 = b.Function("f2", T3);
+    {
+        auto* p = b.FunctionParam("p", ty.ptr<uniform>(T2));
+        f2->SetParams({p});
+        b.Append(f2->Block(),
+                 [&] { b.Return(f2, b.Load(b.Access<ptr<uniform, vec4<i32>>>(p, 3_u))); });
+    }
+
+    auto* f1 = b.Function("f1", T3);
+    {
+        auto* p = b.FunctionParam("p", ty.ptr<uniform>(T1));
+        f1->SetParams({p});
+        b.Append(f1->Block(),
+                 [&] { b.Return(f1, b.Call(f2, b.Access(ty.ptr<uniform>(T2), p, 2_u))); });
+    }
+
+    auto* f0 = b.Function("f0", T3);
+    {
+        auto* p = b.FunctionParam("p", ty.ptr<uniform>(T));
+        f0->SetParams({p});
+        b.Append(f0->Block(),
+                 [&] { b.Return(f0, b.Call(f1, b.Access(ty.ptr<uniform>(T1), p, 1_u))); });
+    }
+
+    auto* main = b.Function("main", ty.void_());
+    b.Append(main->Block(), [&] {
+        b.Call(f0, input);
+        b.Return(main);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %U:ptr<uniform, array<array<array<vec4<i32>, 5>, 5>, 5>, read> = var @binding_point(0, 0)
+}
+
+%f2 = func(%p:ptr<uniform, array<vec4<i32>, 5>, read>):vec4<i32> -> %b2 {
+  %b2 = block {
+    %4:ptr<uniform, vec4<i32>, read> = access %p, 3u
+    %5:vec4<i32> = load %4
+    ret %5
+  }
+}
+%f1 = func(%p_1:ptr<uniform, array<array<vec4<i32>, 5>, 5>, read>):vec4<i32> -> %b3 {  # %p_1: 'p'
+  %b3 = block {
+    %8:ptr<uniform, array<vec4<i32>, 5>, read> = access %p_1, 2u
+    %9:vec4<i32> = call %f2, %8
+    ret %9
+  }
+}
+%f0 = func(%p_2:ptr<uniform, array<array<array<vec4<i32>, 5>, 5>, 5>, read>):vec4<i32> -> %b4 {  # %p_2: 'p'
+  %b4 = block {
+    %12:ptr<uniform, array<array<vec4<i32>, 5>, 5>, read> = access %p_2, 1u
+    %13:vec4<i32> = call %f1, %12
+    ret %13
+  }
+}
+%main = func():void -> %b5 {
+  %b5 = block {
+    %15:vec4<i32> = call %f0, %U
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %U:ptr<uniform, array<array<array<vec4<i32>, 5>, 5>, 5>, read> = var @binding_point(0, 0)
+}
+
+%f2_U_X_X = func(%p_indices:array<u32, 2>):vec4<i32> -> %b2 {
+  %b2 = block {
+    %4:u32 = access %p_indices, 0u
+    %5:u32 = access %p_indices, 1u
+    %6:ptr<uniform, array<vec4<i32>, 5>, read> = access %U, %4, %5
+    %7:ptr<uniform, vec4<i32>, read> = access %6, 3u
+    %8:vec4<i32> = load %7
+    ret %8
+  }
+}
+%f1_U_X = func(%p_indices_1:array<u32, 1>):vec4<i32> -> %b3 {  # %p_indices_1: 'p_indices'
+  %b3 = block {
+    %11:u32 = access %p_indices_1, 0u
+    %12:array<u32, 2> = construct %11, 2u
+    %13:vec4<i32> = call %f2_U_X_X, %12
+    ret %13
+  }
+}
+%f0_U = func():vec4<i32> -> %b4 {
+  %b4 = block {
+    %15:array<u32, 1> = construct 1u
+    %16:vec4<i32> = call %f1_U_X, %15
+    ret %16
+  }
+}
+%main = func():void -> %b5 {
+  %b5 = block {
+    %18:vec4<i32> = call %f0_U
+    ret
+  }
+}
+)";
+
+    Run(DirectVariableAccess, DirectVariableAccessOptions{});
+
+    EXPECT_EQ(expect, str());
+}
+
 }  // namespace uniform_as_tests
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -1727,6 +1849,128 @@
     EXPECT_EQ(expect, str());
 }
 
+TEST_F(IR_DirectVariableAccessTest_StorageAS, CallChaining2) {
+    auto* T3 = ty.vec4<i32>();
+    auto* T2 = ty.array(T3, 5);
+    auto* T1 = ty.array(T2, 5);
+    auto* T = ty.array(T1, 5);
+
+    Var* input = nullptr;
+    b.Append(b.ir.root_block,
+             [&] {  //
+                 input = b.Var("U", ty.ptr<storage>(T));
+                 input->SetBindingPoint(0, 0);
+             });
+
+    auto* f2 = b.Function("f2", T3);
+    {
+        auto* p = b.FunctionParam("p", ty.ptr<storage>(T2));
+        f2->SetParams({p});
+        b.Append(f2->Block(),
+                 [&] { b.Return(f2, b.Load(b.Access<ptr<storage, vec4<i32>>>(p, 3_u))); });
+    }
+
+    auto* f1 = b.Function("f1", T3);
+    {
+        auto* p = b.FunctionParam("p", ty.ptr<storage>(T1));
+        f1->SetParams({p});
+        b.Append(f1->Block(),
+                 [&] { b.Return(f1, b.Call(f2, b.Access(ty.ptr<storage>(T2), p, 2_u))); });
+    }
+
+    auto* f0 = b.Function("f0", T3);
+    {
+        auto* p = b.FunctionParam("p", ty.ptr<storage>(T));
+        f0->SetParams({p});
+        b.Append(f0->Block(),
+                 [&] { b.Return(f0, b.Call(f1, b.Access(ty.ptr<storage>(T1), p, 1_u))); });
+    }
+
+    auto* main = b.Function("main", ty.void_());
+    b.Append(main->Block(), [&] {
+        b.Call(f0, input);
+        b.Return(main);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %U:ptr<storage, array<array<array<vec4<i32>, 5>, 5>, 5>, read_write> = var @binding_point(0, 0)
+}
+
+%f2 = func(%p:ptr<storage, array<vec4<i32>, 5>, read_write>):vec4<i32> -> %b2 {
+  %b2 = block {
+    %4:ptr<storage, vec4<i32>, read_write> = access %p, 3u
+    %5:vec4<i32> = load %4
+    ret %5
+  }
+}
+%f1 = func(%p_1:ptr<storage, array<array<vec4<i32>, 5>, 5>, read_write>):vec4<i32> -> %b3 {  # %p_1: 'p'
+  %b3 = block {
+    %8:ptr<storage, array<vec4<i32>, 5>, read_write> = access %p_1, 2u
+    %9:vec4<i32> = call %f2, %8
+    ret %9
+  }
+}
+%f0 = func(%p_2:ptr<storage, array<array<array<vec4<i32>, 5>, 5>, 5>, read_write>):vec4<i32> -> %b4 {  # %p_2: 'p'
+  %b4 = block {
+    %12:ptr<storage, array<array<vec4<i32>, 5>, 5>, read_write> = access %p_2, 1u
+    %13:vec4<i32> = call %f1, %12
+    ret %13
+  }
+}
+%main = func():void -> %b5 {
+  %b5 = block {
+    %15:vec4<i32> = call %f0, %U
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %U:ptr<storage, array<array<array<vec4<i32>, 5>, 5>, 5>, read_write> = var @binding_point(0, 0)
+}
+
+%f2_U_X_X = func(%p_indices:array<u32, 2>):vec4<i32> -> %b2 {
+  %b2 = block {
+    %4:u32 = access %p_indices, 0u
+    %5:u32 = access %p_indices, 1u
+    %6:ptr<storage, array<vec4<i32>, 5>, read_write> = access %U, %4, %5
+    %7:ptr<storage, vec4<i32>, read_write> = access %6, 3u
+    %8:vec4<i32> = load %7
+    ret %8
+  }
+}
+%f1_U_X = func(%p_indices_1:array<u32, 1>):vec4<i32> -> %b3 {  # %p_indices_1: 'p_indices'
+  %b3 = block {
+    %11:u32 = access %p_indices_1, 0u
+    %12:array<u32, 2> = construct %11, 2u
+    %13:vec4<i32> = call %f2_U_X_X, %12
+    ret %13
+  }
+}
+%f0_U = func():vec4<i32> -> %b4 {
+  %b4 = block {
+    %15:array<u32, 1> = construct 1u
+    %16:vec4<i32> = call %f1_U_X, %15
+    ret %16
+  }
+}
+%main = func():void -> %b5 {
+  %b5 = block {
+    %18:vec4<i32> = call %f0_U
+    ret
+  }
+}
+)";
+
+    Run(DirectVariableAccess, DirectVariableAccessOptions{});
+
+    EXPECT_EQ(expect, str());
+}
+
 }  // namespace storage_as_tests
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -2194,6 +2438,128 @@
     EXPECT_EQ(expect, str());
 }
 
+TEST_F(IR_DirectVariableAccessTest_WorkgroupAS, CallChaining2) {
+    auto* T3 = ty.vec4<i32>();
+    auto* T2 = ty.array(T3, 5);
+    auto* T1 = ty.array(T2, 5);
+    auto* T = ty.array(T1, 5);
+
+    Var* input = nullptr;
+    b.Append(b.ir.root_block,
+             [&] {  //
+                 input = b.Var("U", ty.ptr<workgroup>(T));
+                 input->SetBindingPoint(0, 0);
+             });
+
+    auto* f2 = b.Function("f2", T3);
+    {
+        auto* p = b.FunctionParam("p", ty.ptr<workgroup>(T2));
+        f2->SetParams({p});
+        b.Append(f2->Block(),
+                 [&] { b.Return(f2, b.Load(b.Access<ptr<workgroup, vec4<i32>>>(p, 3_u))); });
+    }
+
+    auto* f1 = b.Function("f1", T3);
+    {
+        auto* p = b.FunctionParam("p", ty.ptr<workgroup>(T1));
+        f1->SetParams({p});
+        b.Append(f1->Block(),
+                 [&] { b.Return(f1, b.Call(f2, b.Access(ty.ptr<workgroup>(T2), p, 2_u))); });
+    }
+
+    auto* f0 = b.Function("f0", T3);
+    {
+        auto* p = b.FunctionParam("p", ty.ptr<workgroup>(T));
+        f0->SetParams({p});
+        b.Append(f0->Block(),
+                 [&] { b.Return(f0, b.Call(f1, b.Access(ty.ptr<workgroup>(T1), p, 1_u))); });
+    }
+
+    auto* main = b.Function("main", ty.void_());
+    b.Append(main->Block(), [&] {
+        b.Call(f0, input);
+        b.Return(main);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %U:ptr<workgroup, array<array<array<vec4<i32>, 5>, 5>, 5>, read_write> = var @binding_point(0, 0)
+}
+
+%f2 = func(%p:ptr<workgroup, array<vec4<i32>, 5>, read_write>):vec4<i32> -> %b2 {
+  %b2 = block {
+    %4:ptr<workgroup, vec4<i32>, read_write> = access %p, 3u
+    %5:vec4<i32> = load %4
+    ret %5
+  }
+}
+%f1 = func(%p_1:ptr<workgroup, array<array<vec4<i32>, 5>, 5>, read_write>):vec4<i32> -> %b3 {  # %p_1: 'p'
+  %b3 = block {
+    %8:ptr<workgroup, array<vec4<i32>, 5>, read_write> = access %p_1, 2u
+    %9:vec4<i32> = call %f2, %8
+    ret %9
+  }
+}
+%f0 = func(%p_2:ptr<workgroup, array<array<array<vec4<i32>, 5>, 5>, 5>, read_write>):vec4<i32> -> %b4 {  # %p_2: 'p'
+  %b4 = block {
+    %12:ptr<workgroup, array<array<vec4<i32>, 5>, 5>, read_write> = access %p_2, 1u
+    %13:vec4<i32> = call %f1, %12
+    ret %13
+  }
+}
+%main = func():void -> %b5 {
+  %b5 = block {
+    %15:vec4<i32> = call %f0, %U
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %U:ptr<workgroup, array<array<array<vec4<i32>, 5>, 5>, 5>, read_write> = var @binding_point(0, 0)
+}
+
+%f2_U_X_X = func(%p_indices:array<u32, 2>):vec4<i32> -> %b2 {
+  %b2 = block {
+    %4:u32 = access %p_indices, 0u
+    %5:u32 = access %p_indices, 1u
+    %6:ptr<workgroup, array<vec4<i32>, 5>, read_write> = access %U, %4, %5
+    %7:ptr<workgroup, vec4<i32>, read_write> = access %6, 3u
+    %8:vec4<i32> = load %7
+    ret %8
+  }
+}
+%f1_U_X = func(%p_indices_1:array<u32, 1>):vec4<i32> -> %b3 {  # %p_indices_1: 'p_indices'
+  %b3 = block {
+    %11:u32 = access %p_indices_1, 0u
+    %12:array<u32, 2> = construct %11, 2u
+    %13:vec4<i32> = call %f2_U_X_X, %12
+    ret %13
+  }
+}
+%f0_U = func():vec4<i32> -> %b4 {
+  %b4 = block {
+    %15:array<u32, 1> = construct 1u
+    %16:vec4<i32> = call %f1_U_X, %15
+    ret %16
+  }
+}
+%main = func():void -> %b5 {
+  %b5 = block {
+    %18:vec4<i32> = call %f0_U
+    ret
+  }
+}
+)";
+
+    Run(DirectVariableAccess, DirectVariableAccessOptions{});
+
+    EXPECT_EQ(expect, str());
+}
+
 }  // namespace workgroup_as_tests
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -3323,6 +3689,214 @@
     EXPECT_EQ(expect, str());
 }
 
+TEST_F(IR_DirectVariableAccessTest_PrivateAS, Enabled_CallChaining2) {
+    auto* T3 = ty.vec4<i32>();
+    auto* T2 = ty.array(T3, 5);
+    auto* T1 = ty.array(T2, 5);
+    auto* T = ty.array(T1, 5);
+
+    Var* P = nullptr;
+    b.Append(b.ir.root_block,
+             [&] {  //
+                 P = b.Var("P", ty.ptr<private_>(T));
+             });
+
+    auto* f2 = b.Function("f2", T3);
+    {
+        auto* p = b.FunctionParam("p", ty.ptr<private_>(T2));
+        f2->SetParams({p});
+        b.Append(f2->Block(), [&] {
+            b.Return(f2, b.Load(b.Access<ptr<private_, vec4<i32>, read_write>>(p, 3_u)));
+        });
+    }
+
+    auto* f1 = b.Function("f1", T3);
+    {
+        auto* p = b.FunctionParam("p", ty.ptr<private_>(T1));
+        f1->SetParams({p});
+        b.Append(f1->Block(),
+                 [&] { b.Return(f1, b.Call(f2, b.Access(ty.ptr<private_>(T2), p, 2_u))); });
+    }
+
+    auto* f0 = b.Function("f0", T3);
+    {
+        auto* p = b.FunctionParam("p", ty.ptr<private_>(T));
+        f0->SetParams({p});
+        b.Append(f0->Block(),
+                 [&] { b.Return(f0, b.Call(f1, b.Access(ty.ptr<private_>(T1), p, 1_u))); });
+    }
+
+    auto* main = b.Function("main", ty.void_());
+    b.Append(main->Block(), [&] {
+        b.Call(f0, P);
+        b.Return(main);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %P:ptr<private, array<array<array<vec4<i32>, 5>, 5>, 5>, read_write> = var
+}
+
+%f2 = func(%p:ptr<private, array<vec4<i32>, 5>, read_write>):vec4<i32> -> %b2 {
+  %b2 = block {
+    %4:ptr<private, vec4<i32>, read_write> = access %p, 3u
+    %5:vec4<i32> = load %4
+    ret %5
+  }
+}
+%f1 = func(%p_1:ptr<private, array<array<vec4<i32>, 5>, 5>, read_write>):vec4<i32> -> %b3 {  # %p_1: 'p'
+  %b3 = block {
+    %8:ptr<private, array<vec4<i32>, 5>, read_write> = access %p_1, 2u
+    %9:vec4<i32> = call %f2, %8
+    ret %9
+  }
+}
+%f0 = func(%p_2:ptr<private, array<array<array<vec4<i32>, 5>, 5>, 5>, read_write>):vec4<i32> -> %b4 {  # %p_2: 'p'
+  %b4 = block {
+    %12:ptr<private, array<array<vec4<i32>, 5>, 5>, read_write> = access %p_2, 1u
+    %13:vec4<i32> = call %f1, %12
+    ret %13
+  }
+}
+%main = func():void -> %b5 {
+  %b5 = block {
+    %15:vec4<i32> = call %f0, %P
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %P:ptr<private, array<array<array<vec4<i32>, 5>, 5>, 5>, read_write> = var
+}
+
+%f2_P_X_X = func(%p_indices:array<u32, 2>):vec4<i32> -> %b2 {
+  %b2 = block {
+    %4:u32 = access %p_indices, 0u
+    %5:u32 = access %p_indices, 1u
+    %6:ptr<private, array<vec4<i32>, 5>, read_write> = access %P, %4, %5
+    %7:ptr<private, vec4<i32>, read_write> = access %6, 3u
+    %8:vec4<i32> = load %7
+    ret %8
+  }
+}
+%f1_P_X = func(%p_indices_1:array<u32, 1>):vec4<i32> -> %b3 {  # %p_indices_1: 'p_indices'
+  %b3 = block {
+    %11:u32 = access %p_indices_1, 0u
+    %12:array<u32, 2> = construct %11, 2u
+    %13:vec4<i32> = call %f2_P_X_X, %12
+    ret %13
+  }
+}
+%f0_P = func():vec4<i32> -> %b4 {
+  %b4 = block {
+    %15:array<u32, 1> = construct 1u
+    %16:vec4<i32> = call %f1_P_X, %15
+    ret %16
+  }
+}
+%main = func():void -> %b5 {
+  %b5 = block {
+    %18:vec4<i32> = call %f0_P
+    ret
+  }
+}
+)";
+
+    Run(DirectVariableAccess, kTransformPrivate);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_PrivateAS, Disabled_CallChaining2) {
+    auto* T3 = ty.vec4<i32>();
+    auto* T2 = ty.array(T3, 5);
+    auto* T1 = ty.array(T2, 5);
+    auto* T = ty.array(T1, 5);
+
+    Var* P = nullptr;
+    b.Append(b.ir.root_block,
+             [&] {  //
+                 P = b.Var("P", ty.ptr<private_>(T));
+             });
+
+    auto* f2 = b.Function("f2", T3);
+    {
+        auto* p = b.FunctionParam("p", ty.ptr<private_>(T2));
+        f2->SetParams({p});
+        b.Append(f2->Block(), [&] {
+            b.Return(f2, b.Load(b.Access<ptr<private_, vec4<i32>, read_write>>(p, 3_u)));
+        });
+    }
+
+    auto* f1 = b.Function("f1", T3);
+    {
+        auto* p = b.FunctionParam("p", ty.ptr<private_>(T1));
+        f1->SetParams({p});
+        b.Append(f1->Block(),
+                 [&] { b.Return(f1, b.Call(f2, b.Access(ty.ptr<private_>(T2), p, 2_u))); });
+    }
+
+    auto* f0 = b.Function("f0", T3);
+    {
+        auto* p = b.FunctionParam("p", ty.ptr<private_>(T));
+        f0->SetParams({p});
+        b.Append(f0->Block(),
+                 [&] { b.Return(f0, b.Call(f1, b.Access(ty.ptr<private_>(T1), p, 1_u))); });
+    }
+
+    auto* main = b.Function("main", ty.void_());
+    b.Append(main->Block(), [&] {
+        b.Call(f0, P);
+        b.Return(main);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %P:ptr<private, array<array<array<vec4<i32>, 5>, 5>, 5>, read_write> = var
+}
+
+%f2 = func(%p:ptr<private, array<vec4<i32>, 5>, read_write>):vec4<i32> -> %b2 {
+  %b2 = block {
+    %4:ptr<private, vec4<i32>, read_write> = access %p, 3u
+    %5:vec4<i32> = load %4
+    ret %5
+  }
+}
+%f1 = func(%p_1:ptr<private, array<array<vec4<i32>, 5>, 5>, read_write>):vec4<i32> -> %b3 {  # %p_1: 'p'
+  %b3 = block {
+    %8:ptr<private, array<vec4<i32>, 5>, read_write> = access %p_1, 2u
+    %9:vec4<i32> = call %f2, %8
+    ret %9
+  }
+}
+%f0 = func(%p_2:ptr<private, array<array<array<vec4<i32>, 5>, 5>, 5>, read_write>):vec4<i32> -> %b4 {  # %p_2: 'p'
+  %b4 = block {
+    %12:ptr<private, array<array<vec4<i32>, 5>, 5>, read_write> = access %p_2, 1u
+    %13:vec4<i32> = call %f1, %12
+    ret %13
+  }
+}
+%main = func():void -> %b5 {
+  %b5 = block {
+    %15:vec4<i32> = call %f0, %P
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(DirectVariableAccess, DirectVariableAccessOptions{});
+
+    EXPECT_EQ(expect, str());
+}
+
 }  // namespace private_as_tests
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -3841,6 +4415,567 @@
     EXPECT_EQ(expect, str());
 }
 
+TEST_F(IR_DirectVariableAccessTest_FunctionAS, Enabled_CallChaining) {
+    auto* Inner =
+        ty.Struct(mod.symbols.New("Inner"), {
+                                                {mod.symbols.Register("mat"), ty.mat3x4<f32>()},
+                                            });
+    auto* Outer =
+        ty.Struct(mod.symbols.New("Outer"), {
+                                                {mod.symbols.Register("arr"), ty.array(Inner, 4)},
+                                                {mod.symbols.Register("mat"), ty.mat3x4<f32>()},
+                                            });
+
+    auto* f0 = b.Function("f0", ty.f32());
+    {
+        auto* p = b.FunctionParam("p", ty.ptr<function, vec4<f32>>());
+        f0->SetParams({p});
+        b.Append(f0->Block(), [&] { b.Return(f0, b.LoadVectorElement(p, 0_u)); });
+    }
+
+    auto* f1 = b.Function("f1", ty.f32());
+    {
+        auto* p = b.FunctionParam("p", ty.ptr<function, mat3x4<f32>>());
+        f1->SetParams({p});
+        b.Append(f1->Block(), [&] {
+            auto* res = b.Var<function, f32>("res");
+            {
+                // res += f0(&(*p)[1]);
+                auto* call_0 = b.Call(f0, b.Access(ty.ptr<function, vec4<f32>>(), p, 1_i));
+                b.Store(res, b.Add(ty.f32(), b.Load(res), call_0));
+            }
+            {
+                // let p_vec = &(*p)[1];
+                // res += f0(p_vec);
+                auto* p_vec = b.Access(ty.ptr<function, vec4<f32>>(), p, 1_i);
+                b.ir.SetName(p_vec, "p_vec");
+                auto* call_0 = b.Call(f0, p_vec);
+                b.Store(res, b.Add(ty.f32(), b.Load(res), call_0));
+            }
+            b.Return(f1, b.Load(res));
+        });
+    }
+
+    auto* f2 = b.Function("f2", ty.f32());
+    {
+        auto* p = b.FunctionParam("p", ty.ptr<function>(Inner));
+        f2->SetParams({p});
+        b.Append(f2->Block(), [&] {
+            auto* p_mat = b.Access(ty.ptr<function, mat3x4<f32>>(), p, 0_u);
+            b.ir.SetName(p_mat, "p_mat");
+            b.Return(f2, b.Call(f1, p_mat));
+        });
+    }
+
+    auto* f3 = b.Function("f3", ty.f32());
+    {
+        auto* p = b.FunctionParam("p", ty.ptr<function>(ty.array(Inner, 4)));
+        f3->SetParams({p});
+        b.Append(f3->Block(), [&] {
+            auto* p_inner = b.Access(ty.ptr<function>(Inner), p, 3_i);
+            b.ir.SetName(p_inner, "p_inner");
+            b.Return(f3, b.Call(f2, p_inner));
+        });
+    }
+
+    auto* f4 = b.Function("f4", ty.f32());
+    {
+        auto* p = b.FunctionParam("p", ty.ptr<function>(Outer));
+        f4->SetParams({p});
+        b.Append(f4->Block(), [&] {
+            auto* access = b.Access(ty.ptr<function>(ty.array(Inner, 4)), p, 0_u);
+            b.Return(f4, b.Call(f3, access));
+        });
+    }
+
+    auto* fn_b = b.Function("b", ty.void_());
+    b.Append(fn_b->Block(), [&] {
+        auto F = b.Var("F", ty.ptr<function>(Outer));
+        b.Call(f4, F);
+        b.Return(fn_b);
+    });
+
+    auto* src = R"(
+Inner = struct @align(16) {
+  mat:mat3x4<f32> @offset(0)
+}
+
+Outer = struct @align(16) {
+  arr:array<Inner, 4> @offset(0)
+  mat:mat3x4<f32> @offset(192)
+}
+
+%f0 = func(%p:ptr<function, vec4<f32>, read_write>):f32 -> %b1 {
+  %b1 = block {
+    %3:f32 = load_vector_element %p, 0u
+    ret %3
+  }
+}
+%f1 = func(%p_1:ptr<function, mat3x4<f32>, read_write>):f32 -> %b2 {  # %p_1: 'p'
+  %b2 = block {
+    %res:ptr<function, f32, read_write> = var
+    %7:ptr<function, vec4<f32>, read_write> = access %p_1, 1i
+    %8:f32 = call %f0, %7
+    %9:f32 = load %res
+    %10:f32 = add %9, %8
+    store %res, %10
+    %p_vec:ptr<function, vec4<f32>, read_write> = access %p_1, 1i
+    %12:f32 = call %f0, %p_vec
+    %13:f32 = load %res
+    %14:f32 = add %13, %12
+    store %res, %14
+    %15:f32 = load %res
+    ret %15
+  }
+}
+%f2 = func(%p_2:ptr<function, Inner, read_write>):f32 -> %b3 {  # %p_2: 'p'
+  %b3 = block {
+    %p_mat:ptr<function, mat3x4<f32>, read_write> = access %p_2, 0u
+    %19:f32 = call %f1, %p_mat
+    ret %19
+  }
+}
+%f3 = func(%p_3:ptr<function, array<Inner, 4>, read_write>):f32 -> %b4 {  # %p_3: 'p'
+  %b4 = block {
+    %p_inner:ptr<function, Inner, read_write> = access %p_3, 3i
+    %23:f32 = call %f2, %p_inner
+    ret %23
+  }
+}
+%f4 = func(%p_4:ptr<function, Outer, read_write>):f32 -> %b5 {  # %p_4: 'p'
+  %b5 = block {
+    %26:ptr<function, array<Inner, 4>, read_write> = access %p_4, 0u
+    %27:f32 = call %f3, %26
+    ret %27
+  }
+}
+%b = func():void -> %b6 {
+  %b6 = block {
+    %F:ptr<function, Outer, read_write> = var
+    %30:f32 = call %f4, %F
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+Inner = struct @align(16) {
+  mat:mat3x4<f32> @offset(0)
+}
+
+Outer = struct @align(16) {
+  arr:array<Inner, 4> @offset(0)
+  mat:mat3x4<f32> @offset(192)
+}
+
+%f0_P_arr_X_mat_X = func(%p_root:ptr<function, Outer, read_write>, %p_indices:array<u32, 2>):f32 -> %b1 {
+  %b1 = block {
+    %4:u32 = access %p_indices, 0u
+    %5:u32 = access %p_indices, 1u
+    %6:ptr<function, vec4<f32>, read_write> = access %p_root, 0u, %4, 0u, %5
+    %7:f32 = load_vector_element %6, 0u
+    ret %7
+  }
+}
+%f1_P_arr_X_mat = func(%p_root_1:ptr<function, Outer, read_write>, %p_indices_1:array<u32, 1>):f32 -> %b2 {  # %p_root_1: 'p_root', %p_indices_1: 'p_indices'
+  %b2 = block {
+    %11:u32 = access %p_indices_1, 0u
+    %res:ptr<function, f32, read_write> = var
+    %13:u32 = convert 1i
+    %14:array<u32, 2> = construct %11, %13
+    %15:f32 = call %f0_P_arr_X_mat_X, %p_root_1, %14
+    %16:f32 = load %res
+    %17:f32 = add %16, %15
+    store %res, %17
+    %18:u32 = convert 1i
+    %19:array<u32, 2> = construct %11, %18
+    %20:f32 = call %f0_P_arr_X_mat_X, %p_root_1, %19
+    %21:f32 = load %res
+    %22:f32 = add %21, %20
+    store %res, %22
+    %23:f32 = load %res
+    ret %23
+  }
+}
+%f2_P_arr_X = func(%p_root_2:ptr<function, Outer, read_write>, %p_indices_2:array<u32, 1>):f32 -> %b3 {  # %p_root_2: 'p_root', %p_indices_2: 'p_indices'
+  %b3 = block {
+    %27:u32 = access %p_indices_2, 0u
+    %28:array<u32, 1> = construct %27
+    %29:f32 = call %f1_P_arr_X_mat, %p_root_2, %28
+    ret %29
+  }
+}
+%f3_P_arr = func(%p_root_3:ptr<function, Outer, read_write>):f32 -> %b4 {  # %p_root_3: 'p_root'
+  %b4 = block {
+    %32:u32 = convert 3i
+    %33:array<u32, 1> = construct %32
+    %34:f32 = call %f2_P_arr_X, %p_root_3, %33
+    ret %34
+  }
+}
+%f4_P = func(%p_root_4:ptr<function, Outer, read_write>):f32 -> %b5 {  # %p_root_4: 'p_root'
+  %b5 = block {
+    %37:f32 = call %f3_P_arr, %p_root_4
+    ret %37
+  }
+}
+%b = func():void -> %b6 {
+  %b6 = block {
+    %F:ptr<function, Outer, read_write> = var
+    %40:f32 = call %f4_P, %F
+    ret
+  }
+}
+)";
+
+    Run(DirectVariableAccess, kTransformFunction);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_FunctionAS, Disabled_CallChaining) {
+    auto* Inner =
+        ty.Struct(mod.symbols.New("Inner"), {
+                                                {mod.symbols.Register("mat"), ty.mat3x4<f32>()},
+                                            });
+    auto* Outer =
+        ty.Struct(mod.symbols.New("Outer"), {
+                                                {mod.symbols.Register("arr"), ty.array(Inner, 4)},
+                                                {mod.symbols.Register("mat"), ty.mat3x4<f32>()},
+                                            });
+
+    auto* f0 = b.Function("f0", ty.f32());
+    {
+        auto* p = b.FunctionParam("p", ty.ptr<function, vec4<f32>>());
+        f0->SetParams({p});
+        b.Append(f0->Block(), [&] { b.Return(f0, b.LoadVectorElement(p, 0_u)); });
+    }
+
+    auto* f1 = b.Function("f1", ty.f32());
+    {
+        auto* p = b.FunctionParam("p", ty.ptr<function, mat3x4<f32>>());
+        f1->SetParams({p});
+        b.Append(f1->Block(), [&] {
+            auto* res = b.Var<function, f32>("res");
+            {
+                // res += f0(&(*p)[1]);
+                auto* call_0 = b.Call(f0, b.Access(ty.ptr<function, vec4<f32>>(), p, 1_i));
+                b.Store(res, b.Add(ty.f32(), b.Load(res), call_0));
+            }
+            {
+                // let p_vec = &(*p)[1];
+                // res += f0(p_vec);
+                auto* p_vec = b.Access(ty.ptr<function, vec4<f32>>(), p, 1_i);
+                b.ir.SetName(p_vec, "p_vec");
+                auto* call_0 = b.Call(f0, p_vec);
+                b.Store(res, b.Add(ty.f32(), b.Load(res), call_0));
+            }
+            b.Return(f1, b.Load(res));
+        });
+    }
+
+    auto* f2 = b.Function("f2", ty.f32());
+    {
+        auto* p = b.FunctionParam("p", ty.ptr<function>(Inner));
+        f2->SetParams({p});
+        b.Append(f2->Block(), [&] {
+            auto* p_mat = b.Access(ty.ptr<function, mat3x4<f32>>(), p, 0_u);
+            b.ir.SetName(p_mat, "p_mat");
+            b.Return(f2, b.Call(f1, p_mat));
+        });
+    }
+
+    auto* f3 = b.Function("f3", ty.f32());
+    {
+        auto* p = b.FunctionParam("p", ty.ptr<function>(ty.array(Inner, 4)));
+        f3->SetParams({p});
+        b.Append(f3->Block(), [&] {
+            auto* p_inner = b.Access(ty.ptr<function>(Inner), p, 3_i);
+            b.ir.SetName(p_inner, "p_inner");
+            b.Return(f3, b.Call(f2, p_inner));
+        });
+    }
+
+    auto* f4 = b.Function("f4", ty.f32());
+    {
+        auto* p = b.FunctionParam("p", ty.ptr<function>(Outer));
+        f4->SetParams({p});
+        b.Append(f4->Block(), [&] {
+            auto* access = b.Access(ty.ptr<function>(ty.array(Inner, 4)), p, 0_u);
+            b.Return(f4, b.Call(f3, access));
+        });
+    }
+
+    auto* fn_b = b.Function("b", ty.void_());
+    b.Append(fn_b->Block(), [&] {
+        auto F = b.Var("F", ty.ptr<function>(Outer));
+        b.Call(f4, F);
+        b.Return(fn_b);
+    });
+
+    auto* src = R"(
+Inner = struct @align(16) {
+  mat:mat3x4<f32> @offset(0)
+}
+
+Outer = struct @align(16) {
+  arr:array<Inner, 4> @offset(0)
+  mat:mat3x4<f32> @offset(192)
+}
+
+%f0 = func(%p:ptr<function, vec4<f32>, read_write>):f32 -> %b1 {
+  %b1 = block {
+    %3:f32 = load_vector_element %p, 0u
+    ret %3
+  }
+}
+%f1 = func(%p_1:ptr<function, mat3x4<f32>, read_write>):f32 -> %b2 {  # %p_1: 'p'
+  %b2 = block {
+    %res:ptr<function, f32, read_write> = var
+    %7:ptr<function, vec4<f32>, read_write> = access %p_1, 1i
+    %8:f32 = call %f0, %7
+    %9:f32 = load %res
+    %10:f32 = add %9, %8
+    store %res, %10
+    %p_vec:ptr<function, vec4<f32>, read_write> = access %p_1, 1i
+    %12:f32 = call %f0, %p_vec
+    %13:f32 = load %res
+    %14:f32 = add %13, %12
+    store %res, %14
+    %15:f32 = load %res
+    ret %15
+  }
+}
+%f2 = func(%p_2:ptr<function, Inner, read_write>):f32 -> %b3 {  # %p_2: 'p'
+  %b3 = block {
+    %p_mat:ptr<function, mat3x4<f32>, read_write> = access %p_2, 0u
+    %19:f32 = call %f1, %p_mat
+    ret %19
+  }
+}
+%f3 = func(%p_3:ptr<function, array<Inner, 4>, read_write>):f32 -> %b4 {  # %p_3: 'p'
+  %b4 = block {
+    %p_inner:ptr<function, Inner, read_write> = access %p_3, 3i
+    %23:f32 = call %f2, %p_inner
+    ret %23
+  }
+}
+%f4 = func(%p_4:ptr<function, Outer, read_write>):f32 -> %b5 {  # %p_4: 'p'
+  %b5 = block {
+    %26:ptr<function, array<Inner, 4>, read_write> = access %p_4, 0u
+    %27:f32 = call %f3, %26
+    ret %27
+  }
+}
+%b = func():void -> %b6 {
+  %b6 = block {
+    %F:ptr<function, Outer, read_write> = var
+    %30:f32 = call %f4, %F
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(DirectVariableAccess, DirectVariableAccessOptions{});
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_FunctionAS, Enabled_CallChaining2) {
+    auto* T3 = ty.vec4<i32>();
+    auto* T2 = ty.array(T3, 5);
+    auto* T1 = ty.array(T2, 5);
+    auto* T = ty.array(T1, 5);
+
+    auto* f2 = b.Function("f2", T3);
+    {
+        auto* p = b.FunctionParam("p", ty.ptr<function>(T2));
+        f2->SetParams({p});
+        b.Append(f2->Block(), [&] {
+            b.Return(f2, b.Load(b.Access<ptr<function, vec4<i32>, read_write>>(p, 3_u)));
+        });
+    }
+
+    auto* f1 = b.Function("f1", T3);
+    {
+        auto* p = b.FunctionParam("p", ty.ptr<function>(T1));
+        f1->SetParams({p});
+        b.Append(f1->Block(),
+                 [&] { b.Return(f1, b.Call(f2, b.Access(ty.ptr<function>(T2), p, 2_u))); });
+    }
+
+    auto* f0 = b.Function("f0", T3);
+    {
+        auto* p = b.FunctionParam("p", ty.ptr<function>(T));
+        f0->SetParams({p});
+        b.Append(f0->Block(),
+                 [&] { b.Return(f0, b.Call(f1, b.Access(ty.ptr<function>(T1), p, 1_u))); });
+    }
+
+    auto* main = b.Function("main", ty.void_());
+    b.Append(main->Block(), [&] {
+        auto* F = b.Var("F", ty.ptr<function>(T));
+        b.Call(f0, F);
+        b.Return(main);
+    });
+
+    auto* src = R"(
+%f2 = func(%p:ptr<function, array<vec4<i32>, 5>, read_write>):vec4<i32> -> %b1 {
+  %b1 = block {
+    %3:ptr<function, vec4<i32>, read_write> = access %p, 3u
+    %4:vec4<i32> = load %3
+    ret %4
+  }
+}
+%f1 = func(%p_1:ptr<function, array<array<vec4<i32>, 5>, 5>, read_write>):vec4<i32> -> %b2 {  # %p_1: 'p'
+  %b2 = block {
+    %7:ptr<function, array<vec4<i32>, 5>, read_write> = access %p_1, 2u
+    %8:vec4<i32> = call %f2, %7
+    ret %8
+  }
+}
+%f0 = func(%p_2:ptr<function, array<array<array<vec4<i32>, 5>, 5>, 5>, read_write>):vec4<i32> -> %b3 {  # %p_2: 'p'
+  %b3 = block {
+    %11:ptr<function, array<array<vec4<i32>, 5>, 5>, read_write> = access %p_2, 1u
+    %12:vec4<i32> = call %f1, %11
+    ret %12
+  }
+}
+%main = func():void -> %b4 {
+  %b4 = block {
+    %F:ptr<function, array<array<array<vec4<i32>, 5>, 5>, 5>, read_write> = var
+    %15:vec4<i32> = call %f0, %F
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%f2_P_X_X = func(%p_root:ptr<function, array<array<array<vec4<i32>, 5>, 5>, 5>, read_write>, %p_indices:array<u32, 2>):vec4<i32> -> %b1 {
+  %b1 = block {
+    %4:u32 = access %p_indices, 0u
+    %5:u32 = access %p_indices, 1u
+    %6:ptr<function, array<vec4<i32>, 5>, read_write> = access %p_root, %4, %5
+    %7:ptr<function, vec4<i32>, read_write> = access %6, 3u
+    %8:vec4<i32> = load %7
+    ret %8
+  }
+}
+%f1_P_X = func(%p_root_1:ptr<function, array<array<array<vec4<i32>, 5>, 5>, 5>, read_write>, %p_indices_1:array<u32, 1>):vec4<i32> -> %b2 {  # %p_root_1: 'p_root', %p_indices_1: 'p_indices'
+  %b2 = block {
+    %12:u32 = access %p_indices_1, 0u
+    %13:array<u32, 2> = construct %12, 2u
+    %14:vec4<i32> = call %f2_P_X_X, %p_root_1, %13
+    ret %14
+  }
+}
+%f0_P = func(%p_root_2:ptr<function, array<array<array<vec4<i32>, 5>, 5>, 5>, read_write>):vec4<i32> -> %b3 {  # %p_root_2: 'p_root'
+  %b3 = block {
+    %17:array<u32, 1> = construct 1u
+    %18:vec4<i32> = call %f1_P_X, %p_root_2, %17
+    ret %18
+  }
+}
+%main = func():void -> %b4 {
+  %b4 = block {
+    %F:ptr<function, array<array<array<vec4<i32>, 5>, 5>, 5>, read_write> = var
+    %21:vec4<i32> = call %f0_P, %F
+    ret
+  }
+}
+)";
+
+    Run(DirectVariableAccess, kTransformFunction);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_FunctionAS, Disabled_CallChaining2) {
+    auto* T3 = ty.vec4<i32>();
+    auto* T2 = ty.array(T3, 5);
+    auto* T1 = ty.array(T2, 5);
+    auto* T = ty.array(T1, 5);
+
+    auto* f2 = b.Function("f2", T3);
+    {
+        auto* p = b.FunctionParam("p", ty.ptr<function>(T2));
+        f2->SetParams({p});
+        b.Append(f2->Block(), [&] {
+            b.Return(f2, b.Load(b.Access<ptr<function, vec4<i32>, read_write>>(p, 3_u)));
+        });
+    }
+
+    auto* f1 = b.Function("f1", T3);
+    {
+        auto* p = b.FunctionParam("p", ty.ptr<function>(T1));
+        f1->SetParams({p});
+        b.Append(f1->Block(),
+                 [&] { b.Return(f1, b.Call(f2, b.Access(ty.ptr<function>(T2), p, 2_u))); });
+    }
+
+    auto* f0 = b.Function("f0", T3);
+    {
+        auto* p = b.FunctionParam("p", ty.ptr<function>(T));
+        f0->SetParams({p});
+        b.Append(f0->Block(),
+                 [&] { b.Return(f0, b.Call(f1, b.Access(ty.ptr<function>(T1), p, 1_u))); });
+    }
+
+    auto* main = b.Function("main", ty.void_());
+    b.Append(main->Block(), [&] {
+        auto* F = b.Var("F", ty.ptr<function>(T));
+        b.Call(f0, F);
+        b.Return(main);
+    });
+
+    auto* src = R"(
+%f2 = func(%p:ptr<function, array<vec4<i32>, 5>, read_write>):vec4<i32> -> %b1 {
+  %b1 = block {
+    %3:ptr<function, vec4<i32>, read_write> = access %p, 3u
+    %4:vec4<i32> = load %3
+    ret %4
+  }
+}
+%f1 = func(%p_1:ptr<function, array<array<vec4<i32>, 5>, 5>, read_write>):vec4<i32> -> %b2 {  # %p_1: 'p'
+  %b2 = block {
+    %7:ptr<function, array<vec4<i32>, 5>, read_write> = access %p_1, 2u
+    %8:vec4<i32> = call %f2, %7
+    ret %8
+  }
+}
+%f0 = func(%p_2:ptr<function, array<array<array<vec4<i32>, 5>, 5>, 5>, read_write>):vec4<i32> -> %b3 {  # %p_2: 'p'
+  %b3 = block {
+    %11:ptr<function, array<array<vec4<i32>, 5>, 5>, read_write> = access %p_2, 1u
+    %12:vec4<i32> = call %f1, %11
+    ret %12
+  }
+}
+%main = func():void -> %b4 {
+  %b4 = block {
+    %F:ptr<function, array<array<array<vec4<i32>, 5>, 5>, 5>, read_write> = var
+    %15:vec4<i32> = call %f0, %F
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(DirectVariableAccess, DirectVariableAccessOptions{});
+
+    EXPECT_EQ(expect, str());
+}
+
 }  // namespace function_as_tests
 
 ////////////////////////////////////////////////////////////////////////////////
diff --git a/src/tint/lang/core/ir/validator.cc b/src/tint/lang/core/ir/validator.cc
index 5546f2c..2aa16f8 100644
--- a/src/tint/lang/core/ir/validator.cc
+++ b/src/tint/lang/core/ir/validator.cc
@@ -598,6 +598,12 @@
         AddError(call, UserCall::kFunctionOperandOffset,
                  InstError(call, "call target is not part of the module"));
     }
+
+    if (call->Target()->Stage() != Function::PipelineStage::kUndefined) {
+        AddError(call, UserCall::kFunctionOperandOffset,
+                 InstError(call, "call target must not have a pipeline stage"));
+    }
+
     auto args = call->Args();
     auto params = call->Target()->Params();
     if (args.Length() != params.Length()) {
diff --git a/src/tint/lang/core/ir/validator_test.cc b/src/tint/lang/core/ir/validator_test.cc
index d81f679..3c2733a 100644
--- a/src/tint/lang/core/ir/validator_test.cc
+++ b/src/tint/lang/core/ir/validator_test.cc
@@ -179,6 +179,42 @@
 )");
 }
 
+TEST_F(IR_ValidatorTest, CallToEntryPointFunction) {
+    auto* f = b.Function("f", ty.void_());
+    auto* g = b.Function("g", ty.void_(), Function::PipelineStage::kCompute);
+
+    b.Append(f->Block(), [&] {
+        b.Call(g);
+        b.Return(f);
+    });
+    b.Append(g->Block(), [&] { b.Return(g); });
+
+    auto res = ir::Validate(mod);
+    ASSERT_NE(res, Success);
+    EXPECT_EQ(res.Failure().reason.str(),
+              R"(:3:20 error: call: call target must not have a pipeline stage
+    %2:void = call %g
+                   ^^
+
+:2:3 note: In block
+  %b1 = block {
+  ^^^^^^^^^^^
+
+note: # Disassembly
+%f = func():void -> %b1 {
+  %b1 = block {
+    %2:void = call %g
+    ret
+  }
+}
+%g = @compute func():void -> %b2 {
+  %b2 = block {
+    ret
+  }
+}
+)");
+}
+
 TEST_F(IR_ValidatorTest, CallToFunctionTooFewArguments) {
     auto* g = b.Function("g", ty.void_());
     g->SetParams({b.FunctionParam<i32>(), b.FunctionParam<i32>()});
diff --git a/src/tint/lang/wgsl/ast/transform/direct_variable_access.cc b/src/tint/lang/wgsl/ast/transform/direct_variable_access.cc
index df9df7d..c73dead 100644
--- a/src/tint/lang/wgsl/ast/transform/direct_variable_access.cc
+++ b/src/tint/lang/wgsl/ast/transform/direct_variable_access.cc
@@ -84,16 +84,13 @@
 /// DynamicIndex is used by DirectVariableAccess::State::AccessOp to indicate an array, matrix or
 /// vector index.
 struct DynamicIndex {
-    /// The index of the expression in DirectVariableAccess::State::AccessChain::dynamic_indices
-    size_t slot = 0;
-
     /// @return a hash code for this object
-    size_t HashCode() const { return Hash(slot); }
+    size_t HashCode() const { return 42 /* empty struct: any number will do */; }
 };
 
 /// Inequality operator for DynamicIndex
-bool operator!=(const DynamicIndex& a, const DynamicIndex& b) {
-    return a.slot != b.slot;
+bool operator!=(const DynamicIndex&, const DynamicIndex&) {
+    return false;  // empty struct: two DynamicIndex objects are always equal
 }
 
 /// AccessOp describes a single access in an access chain.
@@ -484,7 +481,7 @@
                 // Store the index expression into AccessChain::dynamic_indices, append a
                 // DynamicIndex to the chain, and move the chain to the index accessor expression.
                 if (auto* chain = take_chain(a->Object())) {
-                    chain->ops.Push(DynamicIndex{chain->dynamic_indices.Length()});
+                    chain->ops.Push(DynamicIndex{});
                     chain->dynamic_indices.Push(a->Index());
                 }
             },
@@ -1025,20 +1022,23 @@
             // Chain starts with a pointer parameter.
             // Replace this with the variant's incoming shape. This will bring the expression up to
             // the incoming pointer.
+            size_t next_dyn_idx_from_indices = 0;
             auto indices =
                 clone_state->current_variant->ptr_param_symbols.Find(root_param)->indices;
             for (auto param_access : incoming_shape->ops) {
-                chain_expr = BuildAccessExpr(chain_expr, param_access, [&](size_t i) {
-                    return b.IndexAccessor(indices, AInt(i));
+                chain_expr = BuildAccessExpr(chain_expr, param_access, [&] {
+                    return b.IndexAccessor(indices, AInt(next_dyn_idx_from_indices++));
                 });
             }
 
             // Now build the expression chain within the function.
 
             // For each access in the chain (excluding the pointer parameter)...
+            size_t next_dyn_idx_from_chain = 0;
             for (auto& op : chain->ops) {
-                chain_expr = BuildAccessExpr(chain_expr, op, [&](size_t i) {
-                    return BuildDynamicIndex(chain->dynamic_indices[i], false);
+                chain_expr = BuildAccessExpr(chain_expr, op, [&] {
+                    return BuildDynamicIndex(chain->dynamic_indices[next_dyn_idx_from_chain++],
+                                             false);
                 });
             }
 
@@ -1131,13 +1131,13 @@
     /// The returned expression will always be of a reference type.
     /// @param expr the input expression
     /// @param access the access to perform on the current expression
-    /// @param dynamic_index a function that obtains the i'th dynamic index
+    /// @param dynamic_index a function that obtains the next dynamic index
     const Expression* BuildAccessExpr(const Expression* expr,
                                       const AccessOp& access,
-                                      std::function<const Expression*(size_t)> dynamic_index) {
-        if (auto* dyn_idx = std::get_if<DynamicIndex>(&access)) {
+                                      std::function<const Expression*()> dynamic_index) {
+        if (std::holds_alternative<DynamicIndex>(access)) {
             /// The access uses a dynamic (runtime-expression) index.
-            auto* idx = dynamic_index(dyn_idx->slot);
+            auto* idx = dynamic_index();
             return b.IndexAccessor(expr, idx);
         }
 
diff --git a/src/tint/lang/wgsl/ast/transform/direct_variable_access_test.cc b/src/tint/lang/wgsl/ast/transform/direct_variable_access_test.cc
index cb5598f..10f6421 100644
--- a/src/tint/lang/wgsl/ast/transform/direct_variable_access_test.cc
+++ b/src/tint/lang/wgsl/ast/transform/direct_variable_access_test.cc
@@ -1035,12 +1035,6 @@
 alias U_arr_X_mat_X = array<u32, 2u>;
 
 fn f0_U_arr_X_mat_X(p : U_arr_X_mat_X) -> f32 {
-  return U.arr[p[0]].mat[p[0]].x;
-}
-
-alias U_arr_X_mat_X_1 = array<u32, 2u>;
-
-fn f0_U_arr_X_mat_X_1(p : U_arr_X_mat_X_1) -> f32 {
   return U.arr[p[0]].mat[p[1]].x;
 }
 
@@ -1054,11 +1048,11 @@
     res += f0_U_mat_X(U_mat_X(1));
   }
   {
-    res += f0_U_arr_X_mat_X_1(U_arr_X_mat_X_1(2, 1));
+    res += f0_U_arr_X_mat_X(U_arr_X_mat_X(2, 1));
   }
   {
     let p_vec = &(U.arr[2].mat[1]);
-    res += f0_U_arr_X_mat_X_1(U_arr_X_mat_X_1(2, 1));
+    res += f0_U_arr_X_mat_X(U_arr_X_mat_X(2, 1));
   }
   return res;
 }
@@ -1075,11 +1069,11 @@
     res += f0_U_arr_X_mat_X(U_arr_X_mat_X(p[0u], 1));
   }
   {
-    res += f0_U_arr_X_mat_X_1(U_arr_X_mat_X_1(2, 1));
+    res += f0_U_arr_X_mat_X(U_arr_X_mat_X(2, 1));
   }
   {
     let p_vec = &(U.arr[2].mat[1]);
-    res += f0_U_arr_X_mat_X_1(U_arr_X_mat_X_1(2, 1));
+    res += f0_U_arr_X_mat_X(U_arr_X_mat_X(2, 1));
   }
   return res;
 }
@@ -1198,12 +1192,6 @@
 alias U_arr_X_mat_X = array<u32, 2u>;
 
 fn f0_U_arr_X_mat_X(p : U_arr_X_mat_X) -> f32 {
-  return (&(U.arr[p[0]].mat[p[0]])).x;
-}
-
-alias U_arr_X_mat_X_1 = array<u32, 2u>;
-
-fn f0_U_arr_X_mat_X_1(p : U_arr_X_mat_X_1) -> f32 {
   return (&(U.arr[p[0]].mat[p[1]])).x;
 }
 
@@ -1217,11 +1205,11 @@
     res += f0_U_mat_X(U_mat_X(1));
   }
   {
-    res += f0_U_arr_X_mat_X_1(U_arr_X_mat_X_1(2, 1));
+    res += f0_U_arr_X_mat_X(U_arr_X_mat_X(2, 1));
   }
   {
     let p_vec = &(U.arr[2].mat[1]);
-    res += f0_U_arr_X_mat_X_1(U_arr_X_mat_X_1(2, 1));
+    res += f0_U_arr_X_mat_X(U_arr_X_mat_X(2, 1));
   }
   return res;
 }
@@ -1238,11 +1226,11 @@
     res += f0_U_arr_X_mat_X(U_arr_X_mat_X(p[0u], 1));
   }
   {
-    res += f0_U_arr_X_mat_X_1(U_arr_X_mat_X_1(2, 1));
+    res += f0_U_arr_X_mat_X(U_arr_X_mat_X(2, 1));
   }
   {
     let p_vec = &(U.arr[2].mat[1]);
-    res += f0_U_arr_X_mat_X_1(U_arr_X_mat_X_1(2, 1));
+    res += f0_U_arr_X_mat_X(U_arr_X_mat_X(2, 1));
   }
   return res;
 }
@@ -1273,6 +1261,76 @@
     EXPECT_EQ(expect, str(got));
 }
 
+TEST_F(DirectVariableAccessUniformASTest, CallChaining2) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+alias T3 = vec4i;
+alias T2 = array<T3, 5>;
+alias T1 = array<T2, 5>;
+alias T = array<T1, 5>;
+
+@binding(0) @group(0) var<uniform> input : T;
+
+fn f2(p : ptr<uniform, T2>) -> T3 {
+  return (*p)[3];
+}
+
+fn f1(p : ptr<uniform, T1>) -> T3 {
+  return f2(&(*p)[2]);
+}
+
+fn f0(p : ptr<uniform, T>) -> T3 {
+  return f1(&(*p)[1]);
+}
+
+@compute @workgroup_size(1)
+fn main() {
+  f0(&input);
+}
+)";
+
+    auto* expect =
+        R"(
+enable chromium_experimental_full_ptr_parameters;
+
+alias T3 = vec4i;
+
+alias T2 = array<T3, 5>;
+
+alias T1 = array<T2, 5>;
+
+alias T = array<T1, 5>;
+
+@binding(0) @group(0) var<uniform> input : T;
+
+alias input_X_X = array<u32, 2u>;
+
+fn f2_input_X_X(p : input_X_X) -> T3 {
+  return input[p[0]][p[1]][3];
+}
+
+alias input_X = array<u32, 1u>;
+
+fn f1_input_X(p : input_X) -> T3 {
+  return f2_input_X_X(input_X_X(p[0u], 2));
+}
+
+fn f0_input() -> T3 {
+  return f1_input_X(input_X(1));
+}
+
+@compute @workgroup_size(1)
+fn main() {
+  f0_input();
+}
+)";
+
+    auto got = Run<DirectVariableAccess>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
 }  // namespace uniform_as_tests
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -1492,12 +1550,6 @@
 alias S_arr_X_mat_X = array<u32, 2u>;
 
 fn f0_S_arr_X_mat_X(p : S_arr_X_mat_X) -> f32 {
-  return S.arr[p[0]].mat[p[0]].x;
-}
-
-alias S_arr_X_mat_X_1 = array<u32, 2u>;
-
-fn f0_S_arr_X_mat_X_1(p : S_arr_X_mat_X_1) -> f32 {
   return S.arr[p[0]].mat[p[1]].x;
 }
 
@@ -1511,11 +1563,11 @@
     res += f0_S_mat_X(S_mat_X(1));
   }
   {
-    res += f0_S_arr_X_mat_X_1(S_arr_X_mat_X_1(2, 1));
+    res += f0_S_arr_X_mat_X(S_arr_X_mat_X(2, 1));
   }
   {
     let p_vec = &(S.arr[2].mat[1]);
-    res += f0_S_arr_X_mat_X_1(S_arr_X_mat_X_1(2, 1));
+    res += f0_S_arr_X_mat_X(S_arr_X_mat_X(2, 1));
   }
   return res;
 }
@@ -1532,11 +1584,11 @@
     res += f0_S_arr_X_mat_X(S_arr_X_mat_X(p[0u], 1));
   }
   {
-    res += f0_S_arr_X_mat_X_1(S_arr_X_mat_X_1(2, 1));
+    res += f0_S_arr_X_mat_X(S_arr_X_mat_X(2, 1));
   }
   {
     let p_vec = &(S.arr[2].mat[1]);
-    res += f0_S_arr_X_mat_X_1(S_arr_X_mat_X_1(2, 1));
+    res += f0_S_arr_X_mat_X(S_arr_X_mat_X(2, 1));
   }
   return res;
 }
@@ -1655,12 +1707,6 @@
 alias S_arr_X_mat_X = array<u32, 2u>;
 
 fn f0_S_arr_X_mat_X(p : S_arr_X_mat_X) -> f32 {
-  return (&(S.arr[p[0]].mat[p[0]])).x;
-}
-
-alias S_arr_X_mat_X_1 = array<u32, 2u>;
-
-fn f0_S_arr_X_mat_X_1(p : S_arr_X_mat_X_1) -> f32 {
   return (&(S.arr[p[0]].mat[p[1]])).x;
 }
 
@@ -1674,11 +1720,11 @@
     res += f0_S_mat_X(S_mat_X(1));
   }
   {
-    res += f0_S_arr_X_mat_X_1(S_arr_X_mat_X_1(2, 1));
+    res += f0_S_arr_X_mat_X(S_arr_X_mat_X(2, 1));
   }
   {
     let p_vec = &(S.arr[2].mat[1]);
-    res += f0_S_arr_X_mat_X_1(S_arr_X_mat_X_1(2, 1));
+    res += f0_S_arr_X_mat_X(S_arr_X_mat_X(2, 1));
   }
   return res;
 }
@@ -1695,11 +1741,11 @@
     res += f0_S_arr_X_mat_X(S_arr_X_mat_X(p[0u], 1));
   }
   {
-    res += f0_S_arr_X_mat_X_1(S_arr_X_mat_X_1(2, 1));
+    res += f0_S_arr_X_mat_X(S_arr_X_mat_X(2, 1));
   }
   {
     let p_vec = &(S.arr[2].mat[1]);
-    res += f0_S_arr_X_mat_X_1(S_arr_X_mat_X_1(2, 1));
+    res += f0_S_arr_X_mat_X(S_arr_X_mat_X(2, 1));
   }
   return res;
 }
@@ -1730,6 +1776,76 @@
     EXPECT_EQ(expect, str(got));
 }
 
+TEST_F(DirectVariableAccessStorageASTest, CallChaining2) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+alias T3 = vec4i;
+alias T2 = array<T3, 5>;
+alias T1 = array<T2, 5>;
+alias T = array<T1, 5>;
+
+@binding(0) @group(0) var<storage> input : T;
+
+fn f2(p : ptr<storage, T2>) -> T3 {
+  return (*p)[3];
+}
+
+fn f1(p : ptr<storage, T1>) -> T3 {
+  return f2(&(*p)[2]);
+}
+
+fn f0(p : ptr<storage, T>) -> T3 {
+  return f1(&(*p)[1]);
+}
+
+@compute @workgroup_size(1)
+fn main() {
+  f0(&input);
+}
+)";
+
+    auto* expect =
+        R"(
+enable chromium_experimental_full_ptr_parameters;
+
+alias T3 = vec4i;
+
+alias T2 = array<T3, 5>;
+
+alias T1 = array<T2, 5>;
+
+alias T = array<T1, 5>;
+
+@binding(0) @group(0) var<storage> input : T;
+
+alias input_X_X = array<u32, 2u>;
+
+fn f2_input_X_X(p : input_X_X) -> T3 {
+  return input[p[0]][p[1]][3];
+}
+
+alias input_X = array<u32, 1u>;
+
+fn f1_input_X(p : input_X) -> T3 {
+  return f2_input_X_X(input_X_X(p[0u], 2));
+}
+
+fn f0_input() -> T3 {
+  return f1_input_X(input_X(1));
+}
+
+@compute @workgroup_size(1)
+fn main() {
+  f0_input();
+}
+)";
+
+    auto got = Run<DirectVariableAccess>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
 }  // namespace storage_as_tests
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -1899,12 +2015,6 @@
 alias W_arr_X_mat_X = array<u32, 2u>;
 
 fn f0_W_arr_X_mat_X(p : W_arr_X_mat_X) -> f32 {
-  return W.arr[p[0]].mat[p[0]].x;
-}
-
-alias W_arr_X_mat_X_1 = array<u32, 2u>;
-
-fn f0_W_arr_X_mat_X_1(p : W_arr_X_mat_X_1) -> f32 {
   return W.arr[p[0]].mat[p[1]].x;
 }
 
@@ -1918,11 +2028,11 @@
     res += f0_W_mat_X(W_mat_X(1));
   }
   {
-    res += f0_W_arr_X_mat_X_1(W_arr_X_mat_X_1(2, 1));
+    res += f0_W_arr_X_mat_X(W_arr_X_mat_X(2, 1));
   }
   {
     let p_vec = &(W.arr[2].mat[1]);
-    res += f0_W_arr_X_mat_X_1(W_arr_X_mat_X_1(2, 1));
+    res += f0_W_arr_X_mat_X(W_arr_X_mat_X(2, 1));
   }
   return res;
 }
@@ -1939,11 +2049,11 @@
     res += f0_W_arr_X_mat_X(W_arr_X_mat_X(p[0u], 1));
   }
   {
-    res += f0_W_arr_X_mat_X_1(W_arr_X_mat_X_1(2, 1));
+    res += f0_W_arr_X_mat_X(W_arr_X_mat_X(2, 1));
   }
   {
     let p_vec = &(W.arr[2].mat[1]);
-    res += f0_W_arr_X_mat_X_1(W_arr_X_mat_X_1(2, 1));
+    res += f0_W_arr_X_mat_X(W_arr_X_mat_X(2, 1));
   }
   return res;
 }
@@ -2062,12 +2172,6 @@
 alias W_arr_X_mat_X = array<u32, 2u>;
 
 fn f0_W_arr_X_mat_X(p : W_arr_X_mat_X) -> f32 {
-  return (&(W.arr[p[0]].mat[p[0]])).x;
-}
-
-alias W_arr_X_mat_X_1 = array<u32, 2u>;
-
-fn f0_W_arr_X_mat_X_1(p : W_arr_X_mat_X_1) -> f32 {
   return (&(W.arr[p[0]].mat[p[1]])).x;
 }
 
@@ -2081,11 +2185,11 @@
     res += f0_W_mat_X(W_mat_X(1));
   }
   {
-    res += f0_W_arr_X_mat_X_1(W_arr_X_mat_X_1(2, 1));
+    res += f0_W_arr_X_mat_X(W_arr_X_mat_X(2, 1));
   }
   {
     let p_vec = &(W.arr[2].mat[1]);
-    res += f0_W_arr_X_mat_X_1(W_arr_X_mat_X_1(2, 1));
+    res += f0_W_arr_X_mat_X(W_arr_X_mat_X(2, 1));
   }
   return res;
 }
@@ -2102,11 +2206,11 @@
     res += f0_W_arr_X_mat_X(W_arr_X_mat_X(p[0u], 1));
   }
   {
-    res += f0_W_arr_X_mat_X_1(W_arr_X_mat_X_1(2, 1));
+    res += f0_W_arr_X_mat_X(W_arr_X_mat_X(2, 1));
   }
   {
     let p_vec = &(W.arr[2].mat[1]);
-    res += f0_W_arr_X_mat_X_1(W_arr_X_mat_X_1(2, 1));
+    res += f0_W_arr_X_mat_X(W_arr_X_mat_X(2, 1));
   }
   return res;
 }
@@ -2137,6 +2241,81 @@
     EXPECT_EQ(expect, str(got));
 }
 
+TEST_F(DirectVariableAccessWorkgroupASTest, CallChaining2) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+alias T3 = vec4i;
+alias T2 = array<T3, 5>;
+alias T1 = array<T2, 5>;
+alias T = array<T1, 5>;
+
+@binding(0) @group(0) var<storage, read> input : T;
+
+fn f2(p : ptr<workgroup, T2>) -> T3 {
+  return (*p)[3];
+}
+
+fn f1(p : ptr<workgroup, T1>) -> T3 {
+  return f2(&(*p)[2]);
+}
+
+fn f0(p : ptr<workgroup, T>) -> T3 {
+  return f1(&(*p)[1]);
+}
+
+var<workgroup> W : T;
+@compute @workgroup_size(1)
+fn main() {
+  W = input;
+  f0(&W);
+}
+)";
+
+    auto* expect =
+        R"(
+enable chromium_experimental_full_ptr_parameters;
+
+alias T3 = vec4i;
+
+alias T2 = array<T3, 5>;
+
+alias T1 = array<T2, 5>;
+
+alias T = array<T1, 5>;
+
+@binding(0) @group(0) var<storage, read> input : T;
+
+alias W_X_X = array<u32, 2u>;
+
+fn f2_W_X_X(p : W_X_X) -> T3 {
+  return W[p[0]][p[1]][3];
+}
+
+alias W_X = array<u32, 1u>;
+
+fn f1_W_X(p : W_X) -> T3 {
+  return f2_W_X_X(W_X_X(p[0u], 2));
+}
+
+fn f0_W() -> T3 {
+  return f1_W_X(W_X(1));
+}
+
+var<workgroup> W : T;
+
+@compute @workgroup_size(1)
+fn main() {
+  W = input;
+  f0_W();
+}
+)";
+
+    auto got = Run<DirectVariableAccess>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
 }  // namespace workgroup_as_tests
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -2534,18 +2713,12 @@
 alias F_arr_X_mat_X = array<u32, 2u>;
 
 fn f0_F_arr_X_mat_X(p_base : ptr<private, Outer>, p_indices : F_arr_X_mat_X) -> f32 {
-  return (*(p_base)).arr[p_indices[0]].mat[p_indices[0]].x;
-}
-
-alias F_arr_X_mat_X_1 = array<u32, 2u>;
-
-fn f0_F_arr_X_mat_X_1(p_base : ptr<private, Outer>, p_indices : F_arr_X_mat_X_1) -> f32 {
   return (*(p_base)).arr[p_indices[0]].mat[p_indices[1]].x;
 }
 
 alias F_mat_X_1 = array<u32, 1u>;
 
-alias F_arr_X_mat_X_2 = array<u32, 2u>;
+alias F_arr_X_mat_X_1 = array<u32, 2u>;
 
 fn f1_F_mat(p : ptr<private, Outer>) -> f32 {
   var res : f32;
@@ -2557,34 +2730,34 @@
     res += f0_F_mat_X(p, F_mat_X_1(1));
   }
   {
-    res += f0_F_arr_X_mat_X_1(&(P), F_arr_X_mat_X_2(2, 1));
+    res += f0_F_arr_X_mat_X(&(P), F_arr_X_mat_X_1(2, 1));
   }
   {
     let p_vec = &(P.arr[2].mat[1]);
-    res += f0_F_arr_X_mat_X_1(&(P), F_arr_X_mat_X_2(2, 1));
+    res += f0_F_arr_X_mat_X(&(P), F_arr_X_mat_X_1(2, 1));
   }
   return res;
 }
 
 alias F_arr_X_mat = array<u32, 1u>;
 
-alias F_arr_X_mat_X_3 = array<u32, 2u>;
+alias F_arr_X_mat_X_2 = array<u32, 2u>;
 
 fn f1_F_arr_X_mat(p_base : ptr<private, Outer>, p_indices : F_arr_X_mat) -> f32 {
   var res : f32;
   {
-    res += f0_F_arr_X_mat_X(p_base, F_arr_X_mat_X_3(p_indices[0u], 1));
+    res += f0_F_arr_X_mat_X(p_base, F_arr_X_mat_X_2(p_indices[0u], 1));
   }
   {
     let p_vec = &((*(p_base)).arr[p_indices[0]].mat[1]);
-    res += f0_F_arr_X_mat_X(p_base, F_arr_X_mat_X_3(p_indices[0u], 1));
+    res += f0_F_arr_X_mat_X(p_base, F_arr_X_mat_X_2(p_indices[0u], 1));
   }
   {
-    res += f0_F_arr_X_mat_X_1(&(P), F_arr_X_mat_X_2(2, 1));
+    res += f0_F_arr_X_mat_X(&(P), F_arr_X_mat_X_1(2, 1));
   }
   {
     let p_vec = &(P.arr[2].mat[1]);
-    res += f0_F_arr_X_mat_X_1(&(P), F_arr_X_mat_X_2(2, 1));
+    res += f0_F_arr_X_mat_X(&(P), F_arr_X_mat_X_1(2, 1));
   }
   return res;
 }
@@ -2619,6 +2792,80 @@
     EXPECT_EQ(expect, str(got));
 }
 
+TEST_F(DirectVariableAccessPrivateASTest, Enabled_CallChaining2) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+alias T3 = vec4i;
+alias T2 = array<T3, 5>;
+alias T1 = array<T2, 5>;
+alias T = array<T1, 5>;
+
+fn f2(p : ptr<private, T2>) -> T3 {
+  return (*p)[3];
+}
+
+fn f1(p : ptr<private, T1>) -> T3 {
+  return f2(&(*p)[2]);
+}
+
+fn f0(p : ptr<private, T>) -> T3 {
+  return f1(&(*p)[1]);
+}
+
+var<private> P : T;
+
+@compute @workgroup_size(1)
+fn main() {
+  f0(&P);
+}
+)";
+
+    auto* expect =
+        R"(
+enable chromium_experimental_full_ptr_parameters;
+
+alias T3 = vec4i;
+
+alias T2 = array<T3, 5>;
+
+alias T1 = array<T2, 5>;
+
+alias T = array<T1, 5>;
+
+alias F_X_X = array<u32, 2u>;
+
+fn f2_F_X_X(p_base : ptr<private, array<array<array<vec4<i32>, 5u>, 5u>, 5u>>, p_indices : F_X_X) -> T3 {
+  return (*(p_base))[p_indices[0]][p_indices[1]][3];
+}
+
+alias F_X = array<u32, 1u>;
+
+alias F_X_X_1 = array<u32, 2u>;
+
+fn f1_F_X(p_base : ptr<private, array<array<array<vec4<i32>, 5u>, 5u>, 5u>>, p_indices : F_X) -> T3 {
+  return f2_F_X_X(p_base, F_X_X_1(p_indices[0u], 2));
+}
+
+alias F_X_1 = array<u32, 1u>;
+
+fn f0_F(p : ptr<private, array<array<array<vec4<i32>, 5u>, 5u>, 5u>>) -> T3 {
+  return f1_F_X(p, F_X_1(1));
+}
+
+var<private> P : T;
+
+@compute @workgroup_size(1)
+fn main() {
+  f0_F(&(P));
+}
+)";
+
+    auto got = Run<DirectVariableAccess>(src, EnablePrivate());
+
+    EXPECT_EQ(expect, str(got));
+}
+
 TEST_F(DirectVariableAccessPrivateASTest, Disabled_CallChaining) {
     auto* src = R"(
 enable chromium_experimental_full_ptr_parameters;
@@ -2917,6 +3164,187 @@
     EXPECT_EQ(expect, str(got));
 }
 
+TEST_F(DirectVariableAccessFunctionASTest, Enabled_CallChaining) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+struct Inner {
+  mat : mat3x4<f32>,
+};
+
+alias InnerArr = array<Inner, 4>;
+
+struct Outer {
+  arr : InnerArr,
+  mat : mat3x4<f32>,
+};
+
+fn f0(p : ptr<function, vec4<f32>>) -> f32 {
+  return (*p).x;
+}
+
+fn f1(p : ptr<function, mat3x4<f32>>) -> f32 {
+  var res : f32;
+  {
+    // call f0() with inline usage of p
+    res += f0(&(*p)[1]);
+  }
+  {
+    // call f0() with pointer-let usage of p
+    let p_vec = &(*p)[1];
+    res += f0(p_vec);
+  }
+  return res;
+}
+
+fn f2(p : ptr<function, Inner>) -> f32 {
+  let p_mat = &(*p).mat;
+  return f1(p_mat);
+}
+
+fn f3(p : ptr<function, InnerArr>) -> f32 {
+  let p_inner = &(*p)[3];
+  return f2(p_inner);
+}
+
+fn f4(p : ptr<function, Outer>) -> f32 {
+  return f3(&(*p).arr);
+}
+
+fn b() {
+  var S : Outer;
+  f4(&S);
+}
+)";
+
+    auto* expect =
+        R"(
+enable chromium_experimental_full_ptr_parameters;
+
+struct Inner {
+  mat : mat3x4<f32>,
+}
+
+alias InnerArr = array<Inner, 4>;
+
+struct Outer {
+  arr : InnerArr,
+  mat : mat3x4<f32>,
+}
+
+fn f0(p : ptr<function, vec4<f32>>) -> f32 {
+  return (*(p)).x;
+}
+
+fn f1(p : ptr<function, mat3x4<f32>>) -> f32 {
+  var res : f32;
+  {
+    res += f0(&((*(p))[1]));
+  }
+  {
+    let p_vec = &((*(p))[1]);
+    res += f0(p_vec);
+  }
+  return res;
+}
+
+fn f2(p : ptr<function, Inner>) -> f32 {
+  let p_mat = &((*(p)).mat);
+  return f1(p_mat);
+}
+
+fn f3(p : ptr<function, InnerArr>) -> f32 {
+  let p_inner = &((*(p))[3]);
+  return f2(p_inner);
+}
+
+fn f4(p : ptr<function, Outer>) -> f32 {
+  return f3(&((*(p)).arr));
+}
+
+fn b() {
+  var S : Outer;
+  f4(&(S));
+}
+)";
+
+    auto got = Run<DirectVariableAccess>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(DirectVariableAccessFunctionASTest, Enabled_CallChaining2) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+alias T3 = vec4i;
+alias T2 = array<T3, 5>;
+alias T1 = array<T2, 5>;
+alias T = array<T1, 5>;
+
+fn f2(p : ptr<function, T2>) -> T3 {
+  return (*p)[3];
+}
+
+fn f1(p : ptr<function, T1>) -> T3 {
+  return f2(&(*p)[2]);
+}
+
+fn f0(p : ptr<function, T>) -> T3 {
+  return f1(&(*p)[1]);
+}
+
+@compute @workgroup_size(1)
+fn main() {
+  var v : T;
+  f0(&v);
+}
+)";
+
+    auto* expect =
+        R"(
+enable chromium_experimental_full_ptr_parameters;
+
+alias T3 = vec4i;
+
+alias T2 = array<T3, 5>;
+
+alias T1 = array<T2, 5>;
+
+alias T = array<T1, 5>;
+
+alias F_X_X = array<u32, 2u>;
+
+fn f2_F_X_X(p_base : ptr<function, array<array<array<vec4<i32>, 5u>, 5u>, 5u>>, p_indices : F_X_X) -> T3 {
+  return (*(p_base))[p_indices[0]][p_indices[1]][3];
+}
+
+alias F_X = array<u32, 1u>;
+
+alias F_X_X_1 = array<u32, 2u>;
+
+fn f1_F_X(p_base : ptr<function, array<array<array<vec4<i32>, 5u>, 5u>, 5u>>, p_indices : F_X) -> T3 {
+  return f2_F_X_X(p_base, F_X_X_1(p_indices[0u], 2));
+}
+
+alias F_X_1 = array<u32, 1u>;
+
+fn f0_F(p : ptr<function, array<array<array<vec4<i32>, 5u>, 5u>, 5u>>) -> T3 {
+  return f1_F_X(p, F_X_1(1));
+}
+
+@compute @workgroup_size(1)
+fn main() {
+  var v : T;
+  f0_F(&(v));
+}
+)";
+
+    auto got = Run<DirectVariableAccess>(src, EnableFunction());
+
+    EXPECT_EQ(expect, str(got));
+}
+
 TEST_F(DirectVariableAccessFunctionASTest, Disabled_Param_ptr_i32_Via_struct_read) {
     auto* src = R"(
 enable chromium_experimental_full_ptr_parameters;
@@ -3249,7 +3677,7 @@
 alias S_X_X_X_X = array<u32, 4u>;
 
 fn a_S_X_X_X_X(pre : i32, i : S_X_X_X_X, post : i32) -> i32 {
-  return S[i[0]][i[0]][i[1]][i[2]];
+  return S[i[0]][i[1]][i[2]][i[3]];
 }
 
 alias S_X = array<u32, 1u>;
diff --git a/src/tint/lang/wgsl/reader/parser/additive_expression_test.cc b/src/tint/lang/wgsl/reader/parser/additive_expression_test.cc
index 782d922..0317b9a 100644
--- a/src/tint/lang/wgsl/reader/parser/additive_expression_test.cc
+++ b/src/tint/lang/wgsl/reader/parser/additive_expression_test.cc
@@ -33,15 +33,15 @@
 TEST_F(WGSLParserTest, AdditiveExpression_Parses_Plus) {
     auto p = parser("a + b");
     auto lhs = p->unary_expression();
-    auto e = p->expect_additive_expression_post_unary_expression(lhs.value);
+    auto e = p->expect_additive_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_FALSE(e.errored);
     EXPECT_FALSE(p->has_error()) << p->error();
     ASSERT_NE(e.value, nullptr);
 
     EXPECT_EQ(e->source.range.begin.line, 1u);
-    EXPECT_EQ(e->source.range.begin.column, 3u);
+    EXPECT_EQ(e->source.range.begin.column, 1u);
     EXPECT_EQ(e->source.range.end.line, 1u);
-    EXPECT_EQ(e->source.range.end.column, 4u);
+    EXPECT_EQ(e->source.range.end.column, 6u);
 
     ASSERT_TRUE(e->Is<ast::BinaryExpression>());
     auto* rel = e->As<ast::BinaryExpression>();
@@ -59,7 +59,7 @@
 TEST_F(WGSLParserTest, AdditiveExpression_Parses_Minus) {
     auto p = parser("a - b");
     auto lhs = p->unary_expression();
-    auto e = p->expect_additive_expression_post_unary_expression(lhs.value);
+    auto e = p->expect_additive_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_FALSE(e.errored);
     EXPECT_FALSE(p->has_error()) << p->error();
     ASSERT_NE(e.value, nullptr);
@@ -80,7 +80,7 @@
 TEST_F(WGSLParserTest, AdditiveExpression_Parses_MinusMinus) {
     auto p = parser("a--b");
     auto lhs = p->unary_expression();
-    auto e = p->expect_additive_expression_post_unary_expression(lhs.value);
+    auto e = p->expect_additive_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_FALSE(e.errored);
     EXPECT_FALSE(p->has_error()) << p->error();
     ASSERT_NE(e.value, nullptr);
@@ -105,7 +105,7 @@
 TEST_F(WGSLParserTest, AdditiveExpression_Parses_MultipleOps) {
     auto p = parser("a - b + c - d");
     auto lhs = p->unary_expression();
-    auto e = p->expect_additive_expression_post_unary_expression(lhs.value);
+    auto e = p->expect_additive_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_FALSE(e.errored);
     EXPECT_FALSE(p->has_error()) << p->error();
     ASSERT_NE(e.value, nullptr);
@@ -151,7 +151,7 @@
 TEST_F(WGSLParserTest, AdditiveExpression_Parses_MultipleOps_MixedMultiplication) {
     auto p = parser("a - b * c - d");
     auto lhs = p->unary_expression();
-    auto e = p->expect_additive_expression_post_unary_expression(lhs.value);
+    auto e = p->expect_additive_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_FALSE(e.errored);
     EXPECT_FALSE(p->has_error()) << p->error();
     ASSERT_NE(e.value, nullptr);
@@ -197,7 +197,7 @@
 TEST_F(WGSLParserTest, AdditiveExpression_InvalidRHS) {
     auto p = parser("a + if (a) {}");
     auto lhs = p->unary_expression();
-    auto e = p->expect_additive_expression_post_unary_expression(lhs.value);
+    auto e = p->expect_additive_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_TRUE(e.errored);
     EXPECT_EQ(e.value, nullptr);
     EXPECT_TRUE(p->has_error());
@@ -207,7 +207,7 @@
 TEST_F(WGSLParserTest, AdditiveExpression_NoMatch_ReturnsLHS) {
     auto p = parser("a true");
     auto lhs = p->unary_expression();
-    auto e = p->expect_additive_expression_post_unary_expression(lhs.value);
+    auto e = p->expect_additive_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_FALSE(e.errored);
     EXPECT_FALSE(p->has_error()) << p->error();
     ASSERT_NE(e.value, nullptr);
diff --git a/src/tint/lang/wgsl/reader/parser/bitwise_expression_test.cc b/src/tint/lang/wgsl/reader/parser/bitwise_expression_test.cc
index 2a3fc71..4660dff 100644
--- a/src/tint/lang/wgsl/reader/parser/bitwise_expression_test.cc
+++ b/src/tint/lang/wgsl/reader/parser/bitwise_expression_test.cc
@@ -33,7 +33,7 @@
 TEST_F(WGSLParserTest, BitwiseExpr_NoOp) {
     auto p = parser("a true");
     auto lhs = p->unary_expression();
-    auto e = p->bitwise_expression_post_unary_expression(lhs.value);
+    auto e = p->bitwise_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_FALSE(e.matched);
     EXPECT_FALSE(e.errored);
     EXPECT_FALSE(p->has_error()) << p->error();
@@ -43,16 +43,16 @@
 TEST_F(WGSLParserTest, BitwiseExpr_Or_Parses) {
     auto p = parser("a | true");
     auto lhs = p->unary_expression();
-    auto e = p->bitwise_expression_post_unary_expression(lhs.value);
+    auto e = p->bitwise_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_TRUE(e.matched);
     EXPECT_FALSE(e.errored);
     EXPECT_FALSE(p->has_error()) << p->error();
     ASSERT_NE(e.value, nullptr);
 
     EXPECT_EQ(e->source.range.begin.line, 1u);
-    EXPECT_EQ(e->source.range.begin.column, 3u);
+    EXPECT_EQ(e->source.range.begin.column, 1u);
     EXPECT_EQ(e->source.range.end.line, 1u);
-    EXPECT_EQ(e->source.range.end.column, 4u);
+    EXPECT_EQ(e->source.range.end.column, 9u);
 
     ASSERT_TRUE(e->Is<ast::BinaryExpression>());
     auto* rel = e->As<ast::BinaryExpression>();
@@ -69,7 +69,7 @@
 TEST_F(WGSLParserTest, BitwiseExpr_Or_Parses_Multiple) {
     auto p = parser("a | true | b");
     auto lhs = p->unary_expression();
-    auto e = p->bitwise_expression_post_unary_expression(lhs.value);
+    auto e = p->bitwise_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_TRUE(e.matched);
     EXPECT_FALSE(e.errored);
     EXPECT_FALSE(p->has_error()) << p->error();
@@ -103,7 +103,7 @@
 TEST_F(WGSLParserTest, BitwiseExpr_Or_InvalidRHS) {
     auto p = parser("true | if (a) {}");
     auto lhs = p->unary_expression();
-    auto e = p->bitwise_expression_post_unary_expression(lhs.value);
+    auto e = p->bitwise_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_FALSE(e.matched);
     EXPECT_TRUE(e.errored);
     EXPECT_EQ(e.value, nullptr);
@@ -114,16 +114,16 @@
 TEST_F(WGSLParserTest, BitwiseExpr_Xor_Parses) {
     auto p = parser("a ^ true");
     auto lhs = p->unary_expression();
-    auto e = p->bitwise_expression_post_unary_expression(lhs.value);
+    auto e = p->bitwise_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_TRUE(e.matched);
     EXPECT_FALSE(e.errored);
     EXPECT_FALSE(p->has_error()) << p->error();
     ASSERT_NE(e.value, nullptr);
 
     EXPECT_EQ(e->source.range.begin.line, 1u);
-    EXPECT_EQ(e->source.range.begin.column, 3u);
+    EXPECT_EQ(e->source.range.begin.column, 1u);
     EXPECT_EQ(e->source.range.end.line, 1u);
-    EXPECT_EQ(e->source.range.end.column, 4u);
+    EXPECT_EQ(e->source.range.end.column, 9u);
 
     ASSERT_TRUE(e->Is<ast::BinaryExpression>());
     auto* rel = e->As<ast::BinaryExpression>();
@@ -140,7 +140,7 @@
 TEST_F(WGSLParserTest, BitwiseExpr_Xor_Parses_Multiple) {
     auto p = parser("a ^ true ^ b");
     auto lhs = p->unary_expression();
-    auto e = p->bitwise_expression_post_unary_expression(lhs.value);
+    auto e = p->bitwise_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_TRUE(e.matched);
     EXPECT_FALSE(e.errored);
     EXPECT_FALSE(p->has_error()) << p->error();
@@ -173,7 +173,7 @@
 TEST_F(WGSLParserTest, BitwiseExpr_Xor_InvalidRHS) {
     auto p = parser("true ^ if (a) {}");
     auto lhs = p->unary_expression();
-    auto e = p->bitwise_expression_post_unary_expression(lhs.value);
+    auto e = p->bitwise_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_FALSE(e.matched);
     EXPECT_TRUE(e.errored);
     EXPECT_EQ(e.value, nullptr);
@@ -184,16 +184,16 @@
 TEST_F(WGSLParserTest, BitwiseExpr_And_Parses) {
     auto p = parser("a & true");
     auto lhs = p->unary_expression();
-    auto e = p->bitwise_expression_post_unary_expression(lhs.value);
+    auto e = p->bitwise_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_TRUE(e.matched);
     EXPECT_FALSE(e.errored);
     EXPECT_FALSE(p->has_error()) << p->error();
     ASSERT_NE(e.value, nullptr);
 
     EXPECT_EQ(e->source.range.begin.line, 1u);
-    EXPECT_EQ(e->source.range.begin.column, 3u);
+    EXPECT_EQ(e->source.range.begin.column, 1u);
     EXPECT_EQ(e->source.range.end.line, 1u);
-    EXPECT_EQ(e->source.range.end.column, 4u);
+    EXPECT_EQ(e->source.range.end.column, 9u);
 
     ASSERT_TRUE(e->Is<ast::BinaryExpression>());
     auto* rel = e->As<ast::BinaryExpression>();
@@ -210,7 +210,7 @@
 TEST_F(WGSLParserTest, BitwiseExpr_And_Parses_Multiple) {
     auto p = parser("a & true & b");
     auto lhs = p->unary_expression();
-    auto e = p->bitwise_expression_post_unary_expression(lhs.value);
+    auto e = p->bitwise_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_TRUE(e.matched);
     EXPECT_FALSE(e.errored);
     EXPECT_FALSE(p->has_error()) << p->error();
@@ -243,7 +243,7 @@
 TEST_F(WGSLParserTest, BitwiseExpr_And_Parses_AndAnd) {
     auto p = parser("a & true &&b");
     auto lhs = p->unary_expression();
-    auto e = p->bitwise_expression_post_unary_expression(lhs.value);
+    auto e = p->bitwise_expression_post_unary_expression(lhs.value, lhs->source);
     // bitwise_expression_post_unary_expression returns before parsing '&&'
 
     EXPECT_TRUE(e.matched);
@@ -264,7 +264,7 @@
 TEST_F(WGSLParserTest, BitwiseExpr_And_InvalidRHS) {
     auto p = parser("true & if (a) {}");
     auto lhs = p->unary_expression();
-    auto e = p->bitwise_expression_post_unary_expression(lhs.value);
+    auto e = p->bitwise_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_FALSE(e.matched);
     EXPECT_TRUE(e.errored);
     EXPECT_EQ(e.value, nullptr);
diff --git a/src/tint/lang/wgsl/reader/parser/expression_test.cc b/src/tint/lang/wgsl/reader/parser/expression_test.cc
index 2b6c7a2..74d78df 100644
--- a/src/tint/lang/wgsl/reader/parser/expression_test.cc
+++ b/src/tint/lang/wgsl/reader/parser/expression_test.cc
@@ -50,9 +50,9 @@
     ASSERT_NE(e.value, nullptr);
 
     EXPECT_EQ(e->source.range.begin.line, 1u);
-    EXPECT_EQ(e->source.range.begin.column, 3u);
+    EXPECT_EQ(e->source.range.begin.column, 1u);
     EXPECT_EQ(e->source.range.end.line, 1u);
-    EXPECT_EQ(e->source.range.end.column, 5u);
+    EXPECT_EQ(e->source.range.end.column, 10u);
 
     ASSERT_TRUE(e->Is<ast::BinaryExpression>());
     auto* rel = e->As<ast::BinaryExpression>();
@@ -117,9 +117,9 @@
     ASSERT_NE(e.value, nullptr);
 
     EXPECT_EQ(e->source.range.begin.line, 1u);
-    EXPECT_EQ(e->source.range.begin.column, 3u);
+    EXPECT_EQ(e->source.range.begin.column, 1u);
     EXPECT_EQ(e->source.range.end.line, 1u);
-    EXPECT_EQ(e->source.range.end.column, 5u);
+    EXPECT_EQ(e->source.range.end.column, 10u);
 
     ASSERT_TRUE(e->Is<ast::BinaryExpression>());
     auto* rel = e->As<ast::BinaryExpression>();
@@ -180,7 +180,11 @@
     EXPECT_TRUE(e.errored);
     EXPECT_EQ(e.value, nullptr);
     EXPECT_TRUE(p->has_error());
-    EXPECT_EQ(p->error(), "1:3: mixing '&&' and '||' requires parenthesis");
+    EXPECT_EQ(p->builder().Diagnostics().str(),
+              R"(test.wgsl:1:3 error: mixing '&&' and '||' requires parenthesis
+a && true || b
+  ^^^^^^^^^^
+)");
 }
 
 TEST_F(WGSLParserTest, Expression_Mixing_AndWithOr) {
@@ -190,7 +194,11 @@
     EXPECT_TRUE(e.errored);
     EXPECT_EQ(e.value, nullptr);
     EXPECT_TRUE(p->has_error());
-    EXPECT_EQ(p->error(), "1:3: mixing '||' and '&&' requires parenthesis");
+    EXPECT_EQ(p->builder().Diagnostics().str(),
+              R"(test.wgsl:1:3 error: mixing '||' and '&&' requires parenthesis
+a || true && b
+  ^^^^^^^^^^
+)");
 }
 
 TEST_F(WGSLParserTest, Expression_Bitwise) {
@@ -281,7 +289,7 @@
     EXPECT_TRUE(e.errored);
     EXPECT_TRUE(p->has_error());
     EXPECT_EQ(e.value, nullptr);
-    EXPECT_EQ(p->error(), R"(1:7: mixing '&&' and '||' requires parenthesis)");
+    EXPECT_EQ(p->error(), R"(1:3: mixing '&&' and '||' requires parenthesis)");
 }
 
 TEST_F(WGSLParserTest, Expression_SubtractionNoSpace) {
diff --git a/src/tint/lang/wgsl/reader/parser/math_expression_test.cc b/src/tint/lang/wgsl/reader/parser/math_expression_test.cc
index 04e1165..4ae6f8e 100644
--- a/src/tint/lang/wgsl/reader/parser/math_expression_test.cc
+++ b/src/tint/lang/wgsl/reader/parser/math_expression_test.cc
@@ -33,7 +33,7 @@
 TEST_F(WGSLParserTest, MathExpression_Parses_Multiplicative) {
     auto p = parser("a * b");
     auto lhs = p->unary_expression();
-    auto e = p->expect_math_expression_post_unary_expression(lhs.value);
+    auto e = p->expect_math_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_FALSE(e.errored);
     EXPECT_FALSE(p->has_error()) << p->error();
     ASSERT_NE(e.value, nullptr);
@@ -54,7 +54,7 @@
 TEST_F(WGSLParserTest, MathExpression_Parses_Mixed_MultiplicativeStart) {
     auto p = parser("a * b + c");
     auto lhs = p->unary_expression();
-    auto e = p->expect_math_expression_post_unary_expression(lhs.value);
+    auto e = p->expect_math_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_FALSE(e.errored);
     EXPECT_FALSE(p->has_error()) << p->error();
     ASSERT_NE(e.value, nullptr);
@@ -89,7 +89,7 @@
 TEST_F(WGSLParserTest, MathExpression_Parses_Additive) {
     auto p = parser("a + b");
     auto lhs = p->unary_expression();
-    auto e = p->expect_math_expression_post_unary_expression(lhs.value);
+    auto e = p->expect_math_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_FALSE(e.errored);
     EXPECT_FALSE(p->has_error()) << p->error();
     ASSERT_NE(e.value, nullptr);
@@ -110,7 +110,7 @@
 TEST_F(WGSLParserTest, MathExpression_Parses_Mixed_AdditiveStart) {
     auto p = parser("a + b * c");
     auto lhs = p->unary_expression();
-    auto e = p->expect_math_expression_post_unary_expression(lhs.value);
+    auto e = p->expect_math_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_FALSE(e.errored);
     EXPECT_FALSE(p->has_error()) << p->error();
     ASSERT_NE(e.value, nullptr);
@@ -145,7 +145,7 @@
 TEST_F(WGSLParserTest, MathExpression_NoMatch_ReturnLHS) {
     auto p = parser("a if");
     auto lhs = p->unary_expression();
-    auto e = p->expect_math_expression_post_unary_expression(lhs.value);
+    auto e = p->expect_math_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_FALSE(e.errored);
     EXPECT_FALSE(p->has_error()) << p->error();
     ASSERT_NE(e.value, nullptr);
@@ -155,7 +155,7 @@
 TEST_F(WGSLParserTest, MathExpression_InvalidRHS) {
     auto p = parser("a * if");
     auto lhs = p->unary_expression();
-    auto e = p->expect_math_expression_post_unary_expression(lhs.value);
+    auto e = p->expect_math_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_TRUE(e.errored);
     EXPECT_TRUE(p->has_error());
     ASSERT_EQ(e.value, nullptr);
diff --git a/src/tint/lang/wgsl/reader/parser/multiplicative_expression_test.cc b/src/tint/lang/wgsl/reader/parser/multiplicative_expression_test.cc
index b41347c..d0e11b7 100644
--- a/src/tint/lang/wgsl/reader/parser/multiplicative_expression_test.cc
+++ b/src/tint/lang/wgsl/reader/parser/multiplicative_expression_test.cc
@@ -33,7 +33,7 @@
 TEST_F(WGSLParserTest, MultiplicativeExpression_Parses_Multiply) {
     auto p = parser("a * b");
     auto lhs = p->unary_expression();
-    auto e = p->expect_multiplicative_expression_post_unary_expression(lhs.value);
+    auto e = p->expect_multiplicative_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_FALSE(e.errored);
     EXPECT_FALSE(p->has_error()) << p->error();
     ASSERT_NE(e.value, nullptr);
@@ -54,7 +54,7 @@
 TEST_F(WGSLParserTest, MultiplicativeExpression_Parses_Multiply_UnaryIndirect) {
     auto p = parser("a **b");
     auto lhs = p->unary_expression();
-    auto e = p->expect_multiplicative_expression_post_unary_expression(lhs.value);
+    auto e = p->expect_multiplicative_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_FALSE(e.errored);
     EXPECT_FALSE(p->has_error()) << p->error();
     ASSERT_NE(e.value, nullptr);
@@ -79,7 +79,7 @@
 TEST_F(WGSLParserTest, MultiplicativeExpression_Parses_Divide) {
     auto p = parser("a / b");
     auto lhs = p->unary_expression();
-    auto e = p->expect_multiplicative_expression_post_unary_expression(lhs.value);
+    auto e = p->expect_multiplicative_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_FALSE(e.errored);
     EXPECT_FALSE(p->has_error()) << p->error();
     ASSERT_NE(e.value, nullptr);
@@ -100,7 +100,7 @@
 TEST_F(WGSLParserTest, MultiplicativeExpression_Parses_Modulo) {
     auto p = parser("a % b");
     auto lhs = p->unary_expression();
-    auto e = p->expect_multiplicative_expression_post_unary_expression(lhs.value);
+    auto e = p->expect_multiplicative_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_FALSE(e.errored);
     EXPECT_FALSE(p->has_error()) << p->error();
     ASSERT_NE(e.value, nullptr);
@@ -121,7 +121,7 @@
 TEST_F(WGSLParserTest, MultiplicativeExpression_Parses_Grouping) {
     auto p = parser("a * b / c % d * e");
     auto lhs = p->unary_expression();
-    auto e = p->expect_multiplicative_expression_post_unary_expression(lhs.value);
+    auto e = p->expect_multiplicative_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_FALSE(e.errored);
     EXPECT_FALSE(p->has_error()) << p->error();
     ASSERT_NE(e.value, nullptr);
@@ -178,7 +178,7 @@
 TEST_F(WGSLParserTest, MultiplicativeExpression_InvalidRHS) {
     auto p = parser("a * if (a) {}");
     auto lhs = p->unary_expression();
-    auto e = p->expect_multiplicative_expression_post_unary_expression(lhs.value);
+    auto e = p->expect_multiplicative_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_TRUE(e.errored);
     EXPECT_EQ(e.value, nullptr);
     ASSERT_TRUE(p->has_error());
@@ -188,7 +188,7 @@
 TEST_F(WGSLParserTest, MultiplicativeExpression_NoMatch_ReturnsLHS) {
     auto p = parser("a + b");
     auto lhs = p->unary_expression();
-    auto e = p->expect_multiplicative_expression_post_unary_expression(lhs.value);
+    auto e = p->expect_multiplicative_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_FALSE(e.errored);
     EXPECT_FALSE(p->has_error()) << p->error();
     ASSERT_NE(e.value, nullptr);
diff --git a/src/tint/lang/wgsl/reader/parser/parser.cc b/src/tint/lang/wgsl/reader/parser/parser.cc
index e3698f9..104f37d 100644
--- a/src/tint/lang/wgsl/reader/parser/parser.cc
+++ b/src/tint/lang/wgsl/reader/parser/parser.cc
@@ -173,7 +173,11 @@
 
     /// @returns the Source that returns the combined source from start to the current last token's
     /// source.
-    tint::Source Source() const {
+    tint::Source operator()() const { return *this; }
+
+    /// @returns the Source that returns the combined source from start to the current last token's
+    /// source.
+    operator tint::Source() const {
         auto end = parser_->last_source().End();
         if (end < start_) {
             end = start_;
@@ -181,10 +185,6 @@
         return Source::Combine(start_, end);
     }
 
-    /// Implicit conversion to Source that returns the combined source from start to the current
-    /// last token's source.
-    operator tint::Source() const { return Source(); }
-
   private:
     Parser* parser_;
     tint::Source start_;
@@ -447,7 +447,7 @@
             return Failure::kErrored;
         }
 
-        builder_.AST().AddEnable(create<ast::Enable>(decl_source.Source(), std::move(extensions)));
+        builder_.AST().AddEnable(create<ast::Enable>(decl_source(), std::move(extensions)));
         return kSuccess;
     });
 }
@@ -508,8 +508,7 @@
             return Failure::kErrored;
         }
 
-        builder_.AST().AddRequires(
-            create<ast::Requires>(decl_source.Source(), std::move(features)));
+        builder_.AST().AddRequires(create<ast::Requires>(decl_source(), std::move(features)));
         return kSuccess;
     });
 }
@@ -857,8 +856,8 @@
 // type_alias_decl
 //   : ALIAS IDENT EQUAL type_specifier
 Maybe<const ast::Alias*> Parser::type_alias_decl() {
-    Source source;
-    if (!match(Token::Type::kAlias, &source)) {
+    MultiTokenSource source(this);
+    if (!match(Token::Type::kAlias)) {
         return Failure::kNoMatch;
     }
 
@@ -881,7 +880,7 @@
         return add_error(peek(), "invalid type alias");
     }
 
-    return builder_.ty.alias(make_source_range_from(source), name.value, type.value);
+    return builder_.ty.alias(source(), name.value, type.value);
 }
 
 // type_specifier
@@ -894,7 +893,7 @@
     }
 
     if (!peek_is(Token::Type::kTemplateArgsLeft)) {
-        return builder_.ty(builder_.Ident(source.Source(), ident.to_str()));
+        return builder_.ty(builder_.Ident(source(), ident.to_str()));
     }
 
     auto args = expect_template_arg_block("type template arguments", [&] {
@@ -904,7 +903,7 @@
     if (args.errored) {
         return Failure::kErrored;
     }
-    return builder_.ty(builder_.Ident(source.Source(), ident.to_str(), std::move(args.value)));
+    return builder_.ty(builder_.Ident(source(), ident.to_str(), std::move(args.value)));
 }
 
 template <typename ENUM>
@@ -1042,8 +1041,8 @@
 // const_assert_statement
 //   : STATIC_ASSERT expression
 Maybe<const ast::ConstAssert*> Parser::const_assert_statement() {
-    Source start;
-    if (!match(Token::Type::kConstAssert, &start)) {
+    MultiTokenSource source(this);
+    if (!match(Token::Type::kConstAssert)) {
         return Failure::kNoMatch;
     }
 
@@ -1055,8 +1054,7 @@
         return add_error(peek(), "unable to parse condition expression");
     }
 
-    Source source = make_source_range_from(start);
-    return create<ast::ConstAssert>(source, condition.value);
+    return create<ast::ConstAssert>(source(), condition.value);
 }
 
 // function_decl
@@ -1213,16 +1211,14 @@
 //   : attribute* BRACE_LEFT statement* BRACE_RIGHT
 Expect<ast::BlockStatement*> Parser::expect_compound_statement(AttributeList& attrs,
                                                                std::string_view use) {
-    auto source_start = peek().source();
+    MultiTokenSource source(this);
     auto stmts =
         expect_brace_block(use, [&]() -> Expect<StatementList> { return expect_statements(); });
-    auto source_end = last_source();
     if (stmts.errored) {
         return Failure::kErrored;
     }
     TINT_DEFER(attrs.Clear());
-    return create<ast::BlockStatement>(Source::Combine(source_start, source_end), stmts.value,
-                                       std::move(attrs));
+    return create<ast::BlockStatement>(source(), stmts.value, std::move(attrs));
 }
 
 // paren_expression
@@ -1454,14 +1450,14 @@
 //   | LET optionally_typed_ident EQUAL expression
 //   | CONST optionally_typed_ident EQUAL expression
 Maybe<const ast::VariableDeclStatement*> Parser::variable_statement() {
-    auto decl_source_range = make_source_range();
+    MultiTokenSource decl_source_range(this);
     if (match(Token::Type::kConst)) {
         auto typed_ident = expect_optionally_typed_ident("'const' declaration");
         if (typed_ident.errored) {
             return Failure::kErrored;
         }
 
-        auto decl_source = decl_source_range.Source();
+        auto decl_source = decl_source_range();
 
         if (!expect("'const' declaration", Token::Type::kEqual)) {
             return Failure::kErrored;
@@ -1489,7 +1485,7 @@
             return Failure::kErrored;
         }
 
-        auto decl_source = decl_source_range.Source();
+        auto decl_source = decl_source_range();
 
         if (!expect("'let' declaration", Token::Type::kEqual)) {
             return Failure::kErrored;
@@ -1519,7 +1515,7 @@
         return Failure::kNoMatch;
     }
 
-    auto decl_source = decl_source_range.Source();
+    auto decl_source = decl_source_range();
 
     const ast::Expression* initializer = nullptr;
     if (match(Token::Type::kEqual)) {
@@ -1779,7 +1775,7 @@
     }
 
     Maybe<const ast::BlockStatement*> continuing(Failure::kErrored);
-    auto body_start = peek().source();
+    MultiTokenSource body_source(this);
     auto body = expect_brace_block("loop", [&]() -> Maybe<StatementList> {
         auto stmts = expect_statements();
         if (stmts.errored) {
@@ -1795,13 +1791,10 @@
     if (body.errored) {
         return Failure::kErrored;
     }
-    auto body_end = last_source();
 
     TINT_DEFER(attrs.Clear());
     return create<ast::LoopStatement>(
-        source,
-        create<ast::BlockStatement>(Source::Combine(body_start, body_end), body.value,
-                                    std::move(body_attrs.value)),
+        source, create<ast::BlockStatement>(body_source(), body.value, std::move(body_attrs.value)),
         continuing.value, std::move(attrs));
 }
 
@@ -2017,7 +2010,7 @@
         return Failure::kErrored;
     }
 
-    auto source_start = peek().source();
+    MultiTokenSource source(this);
     auto body = expect_brace_block("", [&]() -> Expect<StatementList> {
         StatementList stmts;
 
@@ -2047,10 +2040,8 @@
     if (body.errored) {
         return Failure::kErrored;
     }
-    auto source_end = last_source();
 
-    return create<ast::BlockStatement>(Source::Combine(source_start, source_end), body.value,
-                                       std::move(attrs.value));
+    return create<ast::BlockStatement>(source(), body.value, std::move(attrs.value));
 }
 
 // continuing_statement
@@ -2109,9 +2100,9 @@
                 return expect_expression_list("template argument list",
                                               Token::Type::kTemplateArgsRight);
             });
-            ident = builder_.Ident(source.Source(), t.to_str(), std::move(tmpl_args.value));
+            ident = builder_.Ident(source(), t.to_str(), std::move(tmpl_args.value));
         } else {
-            ident = builder_.Ident(source.Source(), t.to_str());
+            ident = builder_.Ident(source(), t.to_str());
         }
 
         if (peek_is(Token::Type::kParenLeft)) {
@@ -2120,7 +2111,7 @@
                 return Failure::kErrored;
             }
 
-            return builder_.Call(source.Source(), ident, std::move(params.value));
+            return builder_.Call(source(), ident, std::move(params.value));
         }
 
         return builder_.Expr(ident);
@@ -2162,7 +2153,7 @@
                     return Failure::kErrored;
                 }
 
-                return create<ast::IndexAccessorExpression>(source.Source(), prefix, param.value);
+                return create<ast::IndexAccessorExpression>(source(), prefix, param.value);
             });
 
             if (res.errored) {
@@ -2178,7 +2169,7 @@
                 return Failure::kErrored;
             }
 
-            prefix = builder_.MemberAccessor(source.Source(), prefix, ident.value);
+            prefix = builder_.MemberAccessor(source(), prefix, ident.value);
             continue;
         }
 
@@ -2205,7 +2196,9 @@
 //   | OR unary_expression (OR unary_expression)*
 //   | XOR unary_expression (XOR unary_expression)*
 Maybe<const ast::Expression*> Parser::bitwise_expression_post_unary_expression(
-    const ast::Expression* lhs) {
+    const ast::Expression* lhs,
+    const Source& lhs_source) {
+    MultiTokenSource source(this, lhs_source);
     auto& t = peek();
 
     std::optional<core::BinaryOp> op;
@@ -2234,7 +2227,7 @@
                                          std::string(t.to_name()) + " expression");
         }
 
-        lhs = create<ast::BinaryExpression>(t.source(), *op, lhs, rhs.value);
+        lhs = create<ast::BinaryExpression>(source(), *op, lhs, rhs.value);
 
         if (!match(t.type())) {
             return lhs;
@@ -2264,7 +2257,9 @@
 // multiplicative_expression.post.unary_expression
 //   : (multiplicative_operator unary_expression)*
 Expect<const ast::Expression*> Parser::expect_multiplicative_expression_post_unary_expression(
-    const ast::Expression* lhs) {
+    const ast::Expression* lhs,
+    const Source& lhs_source) {
+    MultiTokenSource source(this, lhs_source);
     while (continue_parsing()) {
         auto& t = peek();
 
@@ -2285,7 +2280,7 @@
                                          std::string(t.to_name()) + " expression");
         }
 
-        lhs = create<ast::BinaryExpression>(t.source(), op.value, lhs, rhs.value);
+        lhs = create<ast::BinaryExpression>(source(), op.value, lhs, rhs.value);
     }
     return Failure::kErrored;
 }
@@ -2320,7 +2315,9 @@
 // This is `( additive_operator unary_expression ( multiplicative_operator unary_expression )* )*`
 // split apart.
 Expect<const ast::Expression*> Parser::expect_additive_expression_post_unary_expression(
-    const ast::Expression* lhs) {
+    const ast::Expression* lhs,
+    const Source& lhs_source) {
+    MultiTokenSource source(this, lhs_source);
     while (continue_parsing()) {
         auto& t = peek();
 
@@ -2341,14 +2338,14 @@
                                          std::string(t.to_name()) + " expression");
         }
 
-        // The multiplicative binds tigher, so pass the unary into that and build that expression
-        // before creating the additve expression.
-        auto rhs = expect_multiplicative_expression_post_unary_expression(unary.value);
+        // The multiplicative binds tighter, so pass the unary into that and build that expression
+        // before creating the additive expression.
+        auto rhs = expect_multiplicative_expression_post_unary_expression(unary.value, lhs_source);
         if (rhs.errored) {
             return Failure::kErrored;
         }
 
-        lhs = create<ast::BinaryExpression>(t.source(), op.value, lhs, rhs.value);
+        lhs = create<ast::BinaryExpression>(source(), op.value, lhs, rhs.value);
     }
     return Failure::kErrored;
 }
@@ -2359,18 +2356,22 @@
 // This is `( multiplicative_operator unary_expression )* ( additive_operator unary_expression (
 // multiplicative_operator unary_expression )* )*` split apart.
 Expect<const ast::Expression*> Parser::expect_math_expression_post_unary_expression(
-    const ast::Expression* lhs) {
-    auto rhs = expect_multiplicative_expression_post_unary_expression(lhs);
+    const ast::Expression* lhs,
+    const Source& lhs_source) {
+    MultiTokenSource source(this, lhs_source);
+
+    auto rhs = expect_multiplicative_expression_post_unary_expression(lhs, source);
     if (rhs.errored) {
         return Failure::kErrored;
     }
 
-    return expect_additive_expression_post_unary_expression(rhs.value);
+    return expect_additive_expression_post_unary_expression(rhs.value, source());
 }
 
 // shift_expression
 //   : unary_expression shift_expression.post.unary_expression
 Maybe<const ast::Expression*> Parser::shift_expression() {
+    MultiTokenSource source(this);
     auto lhs = unary_expression();
     if (lhs.errored) {
         return Failure::kErrored;
@@ -2378,7 +2379,7 @@
     if (!lhs.matched) {
         return Failure::kNoMatch;
     }
-    return expect_shift_expression_post_unary_expression(lhs.value);
+    return expect_shift_expression_post_unary_expression(lhs.value, source);
 }
 
 // shift_expression.post.unary_expression
@@ -2389,7 +2390,10 @@
 // Note, add the `math_expression.post.unary_expression` is added here to make
 // implementation simpler.
 Expect<const ast::Expression*> Parser::expect_shift_expression_post_unary_expression(
-    const ast::Expression* lhs) {
+    const ast::Expression* lhs,
+    const Source& lhs_source) {
+    MultiTokenSource source(this, lhs_source);
+
     auto& t = peek();
     if (match(Token::Type::kShiftLeft) || match(Token::Type::kShiftRight)) {
         std::string name;
@@ -2411,15 +2415,16 @@
             return add_error(rhs_start,
                              std::string("unable to parse right side of ") + name + " expression");
         }
-        return create<ast::BinaryExpression>(t.source(), *op, lhs, rhs.value);
+        return create<ast::BinaryExpression>(source(), *op, lhs, rhs.value);
     }
 
-    return expect_math_expression_post_unary_expression(lhs);
+    return expect_math_expression_post_unary_expression(lhs, source);
 }
 
 // relational_expression
 //   : unary_expression relational_expression.post.unary_expression
 Maybe<const ast::Expression*> Parser::relational_expression() {
+    MultiTokenSource source(this);
     auto lhs = unary_expression();
     if (lhs.errored) {
         return Failure::kErrored;
@@ -2427,7 +2432,7 @@
     if (!lhs.matched) {
         return Failure::kNoMatch;
     }
-    return expect_relational_expression_post_unary_expression(lhs.value);
+    return expect_relational_expression_post_unary_expression(lhs.value, source);
 }
 
 // relational_expression.post.unary_expression
@@ -2441,8 +2446,11 @@
 //
 // Note, a `shift_expression` element was added to simplify many of the right sides
 Expect<const ast::Expression*> Parser::expect_relational_expression_post_unary_expression(
-    const ast::Expression* lhs) {
-    auto lhs_result = expect_shift_expression_post_unary_expression(lhs);
+    const ast::Expression* lhs,
+    const Source& lhs_source) {
+    MultiTokenSource source(this, lhs_source);
+
+    auto lhs_result = expect_shift_expression_post_unary_expression(lhs, source);
     if (lhs_result.errored) {
         return Failure::kErrored;
     }
@@ -2486,7 +2494,7 @@
                                       std::string(tok_op.to_name()) + " expression");
     }
 
-    return create<ast::BinaryExpression>(tok_op.source(), *op, lhs, rhs.value);
+    return create<ast::BinaryExpression>(source(), *op, lhs, rhs.value);
 }
 
 Expect<const ast::Expression*> Parser::expect_expression(std::string_view use) {
@@ -2560,6 +2568,8 @@
 //
 // Note, a `relational_expression` element was added to simplify many of the right sides
 Maybe<const ast::Expression*> Parser::expression() {
+    MultiTokenSource source(this);
+    Source first_op;
     auto expr = [&]() -> Maybe<const ast::Expression*> {
         auto lhs = unary_expression();
         if (lhs.errored) {
@@ -2569,7 +2579,9 @@
             return Failure::kNoMatch;
         }
 
-        auto bitwise = bitwise_expression_post_unary_expression(lhs.value);
+        first_op = peek().source();
+
+        auto bitwise = bitwise_expression_post_unary_expression(lhs.value, source);
         if (bitwise.errored) {
             return Failure::kErrored;
         }
@@ -2577,7 +2589,7 @@
             return bitwise.value;
         }
 
-        auto relational = expect_relational_expression_post_unary_expression(lhs.value);
+        auto relational = expect_relational_expression_post_unary_expression(lhs.value, source);
         if (relational.errored) {
             return Failure::kErrored;
         }
@@ -2608,7 +2620,7 @@
                                                  std::string(t.to_name()) + " expression");
                 }
 
-                ret = create<ast::BinaryExpression>(t.source(), op, ret, rhs.value);
+                ret = create<ast::BinaryExpression>(source(), op, ret, rhs.value);
             }
         }
         return ret;
@@ -2620,9 +2632,9 @@
         // after this then it _must_ be a different one, and hence an error.
         if (auto* lhs = expr->As<ast::BinaryExpression>()) {
             if (auto& n = peek(); n.IsBinaryOperator()) {
-                auto source = Source::Combine(expr->source, n.source());
-                add_error(source, std::string("mixing '") + ast::Operator(lhs->op) + "' and '" +
-                                      std::string(n.to_name()) + "' requires parenthesis");
+                add_error(Source::Combine(first_op, n.source()),
+                          std::string("mixing '") + ast::Operator(lhs->op) + "' and '" +
+                              std::string(n.to_name()) + "' requires parenthesis");
                 return Failure::kErrored;
             }
         }
@@ -2656,10 +2668,11 @@
 // The `primary_expression component_or_swizzle_specifier ?` is moved out into a
 // `singular_expression`
 Maybe<const ast::Expression*> Parser::unary_expression() {
-    auto& t = peek();
+    MultiTokenSource source(this);
 
+    auto& t = peek();
     if (match(Token::Type::kPlusPlus) || match(Token::Type::kMinusMinus)) {
-        add_error(t.source(),
+        add_error(source,
                   "prefix increment and decrement operators are reserved for a "
                   "future WGSL version");
         return Failure::kErrored;
@@ -2700,7 +2713,7 @@
             peek(), "unable to parse right side of " + std::string(t.to_name()) + " expression");
     }
 
-    return create<ast::UnaryOpExpression>(t.source(), op, expr.value);
+    return create<ast::UnaryOpExpression>(source(), op, expr.value);
 }
 
 // compound_assignment_operator
@@ -3500,12 +3513,4 @@
     return result;
 }
 
-Parser::MultiTokenSource Parser::make_source_range() {
-    return MultiTokenSource(this);
-}
-
-Parser::MultiTokenSource Parser::make_source_range_from(const Source& start) {
-    return MultiTokenSource(this, start);
-}
-
 }  // namespace tint::wgsl::reader
diff --git a/src/tint/lang/wgsl/reader/parser/parser.h b/src/tint/lang/wgsl/reader/parser/parser.h
index 0e6c152..58f231e 100644
--- a/src/tint/lang/wgsl/reader/parser/parser.h
+++ b/src/tint/lang/wgsl/reader/parser/parser.h
@@ -586,43 +586,55 @@
                                                           Token::Type terminator);
     /// Parses the `bitwise_expression.post.unary_expression` grammar element
     /// @param lhs the left side of the expression
+    /// @param lhs_source the source span for the left side of the expression
     /// @returns the parsed expression or nullptr
     Maybe<const ast::Expression*> bitwise_expression_post_unary_expression(
-        const ast::Expression* lhs);
+        const ast::Expression* lhs,
+        const Source& lhs_source);
     /// Parse the `multiplicative_operator` grammar element
     /// @returns the parsed operator if successful
     Maybe<core::BinaryOp> multiplicative_operator();
     /// Parses multiplicative elements
     /// @param lhs the left side of the expression
+    /// @param lhs_source the source span for the left side of the expression
     /// @returns the parsed expression or `lhs` if no match
     Expect<const ast::Expression*> expect_multiplicative_expression_post_unary_expression(
-        const ast::Expression* lhs);
+        const ast::Expression* lhs,
+        const Source& lhs_source);
     /// Parses additive elements
     /// @param lhs the left side of the expression
+    /// @param lhs_source the source span for the left side of the expression
     /// @returns the parsed expression or `lhs` if no match
     Expect<const ast::Expression*> expect_additive_expression_post_unary_expression(
-        const ast::Expression* lhs);
+        const ast::Expression* lhs,
+        const Source& lhs_source);
     /// Parses math elements
     /// @param lhs the left side of the expression
+    /// @param lhs_source the source span for the left side of the expression
     /// @returns the parsed expression or `lhs` if no match
     Expect<const ast::Expression*> expect_math_expression_post_unary_expression(
-        const ast::Expression* lhs);
+        const ast::Expression* lhs,
+        const Source& lhs_source);
     /// Parses a `unary_expression shift.post.unary_expression`
     /// @returns the parsed expression or nullptr
     Maybe<const ast::Expression*> shift_expression();
     /// Parses a `shift_expression.post.unary_expression` grammar element
     /// @param lhs the left side of the expression
+    /// @param lhs_source the source span for the left side of the expression
     /// @returns the parsed expression or `lhs` if no match
     Expect<const ast::Expression*> expect_shift_expression_post_unary_expression(
-        const ast::Expression* lhs);
+        const ast::Expression* lhs,
+        const Source& lhs_source);
     /// Parses a `unary_expression relational_expression.post.unary_expression`
     /// @returns the parsed expression or nullptr
     Maybe<const ast::Expression*> relational_expression();
     /// Parses a `relational_expression.post.unary_expression` grammar element
     /// @param lhs the left side of the expression
+    /// @param lhs_source the source span for the left side of the expression
     /// @returns the parsed expression or `lhs` if no match
     Expect<const ast::Expression*> expect_relational_expression_post_unary_expression(
-        const ast::Expression* lhs);
+        const ast::Expression* lhs,
+        const Source& lhs_source);
     /// Parse the `additive_operator` grammar element
     /// @returns the parsed operator if successful
     Maybe<core::BinaryOp> additive_operator();
@@ -881,8 +893,6 @@
     Maybe<const ast::Statement*> for_header_continuing();
 
     class MultiTokenSource;
-    MultiTokenSource make_source_range();
-    MultiTokenSource make_source_range_from(const Source& start);
 
     /// Creates a new `ast::Node` owned by the Module. When the Module is
     /// destructed, the `ast::Node` will also be destructed.
diff --git a/src/tint/lang/wgsl/reader/parser/relational_expression_test.cc b/src/tint/lang/wgsl/reader/parser/relational_expression_test.cc
index e2122ca..97b0d91 100644
--- a/src/tint/lang/wgsl/reader/parser/relational_expression_test.cc
+++ b/src/tint/lang/wgsl/reader/parser/relational_expression_test.cc
@@ -33,15 +33,15 @@
 TEST_F(WGSLParserTest, RelationalExpression_PostUnary_Parses_LessThan) {
     auto p = parser("a < true");
     auto lhs = p->unary_expression();
-    auto e = p->expect_relational_expression_post_unary_expression(lhs.value);
+    auto e = p->expect_relational_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_FALSE(e.errored);
     EXPECT_FALSE(p->has_error()) << p->error();
     ASSERT_NE(e.value, nullptr);
 
     EXPECT_EQ(e->source.range.begin.line, 1u);
-    EXPECT_EQ(e->source.range.begin.column, 3u);
+    EXPECT_EQ(e->source.range.begin.column, 1u);
     EXPECT_EQ(e->source.range.end.line, 1u);
-    EXPECT_EQ(e->source.range.end.column, 4u);
+    EXPECT_EQ(e->source.range.end.column, 9u);
 
     ASSERT_TRUE(e->Is<ast::BinaryExpression>());
     auto* rel = e->As<ast::BinaryExpression>();
@@ -58,15 +58,15 @@
 TEST_F(WGSLParserTest, RelationalExpression_PostUnary_Parses_GreaterThan) {
     auto p = parser("a > true");
     auto lhs = p->unary_expression();
-    auto e = p->expect_relational_expression_post_unary_expression(lhs.value);
+    auto e = p->expect_relational_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_FALSE(e.errored);
     EXPECT_FALSE(p->has_error()) << p->error();
     ASSERT_NE(e.value, nullptr);
 
     EXPECT_EQ(e->source.range.begin.line, 1u);
-    EXPECT_EQ(e->source.range.begin.column, 3u);
+    EXPECT_EQ(e->source.range.begin.column, 1u);
     EXPECT_EQ(e->source.range.end.line, 1u);
-    EXPECT_EQ(e->source.range.end.column, 4u);
+    EXPECT_EQ(e->source.range.end.column, 9u);
 
     ASSERT_TRUE(e->Is<ast::BinaryExpression>());
     auto* rel = e->As<ast::BinaryExpression>();
@@ -83,15 +83,15 @@
 TEST_F(WGSLParserTest, RelationalExpression_PostUnary_Parses_LessThanEqual) {
     auto p = parser("a <= true");
     auto lhs = p->unary_expression();
-    auto e = p->expect_relational_expression_post_unary_expression(lhs.value);
+    auto e = p->expect_relational_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_FALSE(e.errored);
     EXPECT_FALSE(p->has_error()) << p->error();
     ASSERT_NE(e.value, nullptr);
 
     EXPECT_EQ(e->source.range.begin.line, 1u);
-    EXPECT_EQ(e->source.range.begin.column, 3u);
+    EXPECT_EQ(e->source.range.begin.column, 1u);
     EXPECT_EQ(e->source.range.end.line, 1u);
-    EXPECT_EQ(e->source.range.end.column, 5u);
+    EXPECT_EQ(e->source.range.end.column, 10u);
 
     ASSERT_TRUE(e->Is<ast::BinaryExpression>());
     auto* rel = e->As<ast::BinaryExpression>();
@@ -108,15 +108,15 @@
 TEST_F(WGSLParserTest, RelationalExpression_PostUnary_Parses_GreaterThanEqual) {
     auto p = parser("a >= true");
     auto lhs = p->unary_expression();
-    auto e = p->expect_relational_expression_post_unary_expression(lhs.value);
+    auto e = p->expect_relational_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_FALSE(e.errored);
     EXPECT_FALSE(p->has_error()) << p->error();
     ASSERT_NE(e.value, nullptr);
 
     EXPECT_EQ(e->source.range.begin.line, 1u);
-    EXPECT_EQ(e->source.range.begin.column, 3u);
+    EXPECT_EQ(e->source.range.begin.column, 1u);
     EXPECT_EQ(e->source.range.end.line, 1u);
-    EXPECT_EQ(e->source.range.end.column, 5u);
+    EXPECT_EQ(e->source.range.end.column, 10u);
 
     ASSERT_TRUE(e->Is<ast::BinaryExpression>());
     auto* rel = e->As<ast::BinaryExpression>();
@@ -133,15 +133,15 @@
 TEST_F(WGSLParserTest, RelationalExpression_PostUnary_Parses_Equal) {
     auto p = parser("a == true");
     auto lhs = p->unary_expression();
-    auto e = p->expect_relational_expression_post_unary_expression(lhs.value);
+    auto e = p->expect_relational_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_FALSE(e.errored);
     EXPECT_FALSE(p->has_error()) << p->error();
     ASSERT_NE(e.value, nullptr);
 
     EXPECT_EQ(e->source.range.begin.line, 1u);
-    EXPECT_EQ(e->source.range.begin.column, 3u);
+    EXPECT_EQ(e->source.range.begin.column, 1u);
     EXPECT_EQ(e->source.range.end.line, 1u);
-    EXPECT_EQ(e->source.range.end.column, 5u);
+    EXPECT_EQ(e->source.range.end.column, 10u);
 
     ASSERT_TRUE(e->Is<ast::BinaryExpression>());
     auto* rel = e->As<ast::BinaryExpression>();
@@ -158,15 +158,15 @@
 TEST_F(WGSLParserTest, RelationalExpression_PostUnary_Parses_NotEqual) {
     auto p = parser("a != true");
     auto lhs = p->unary_expression();
-    auto e = p->expect_relational_expression_post_unary_expression(lhs.value);
+    auto e = p->expect_relational_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_FALSE(e.errored);
     EXPECT_FALSE(p->has_error()) << p->error();
     ASSERT_NE(e.value, nullptr);
 
     EXPECT_EQ(e->source.range.begin.line, 1u);
-    EXPECT_EQ(e->source.range.begin.column, 3u);
+    EXPECT_EQ(e->source.range.begin.column, 1u);
     EXPECT_EQ(e->source.range.end.line, 1u);
-    EXPECT_EQ(e->source.range.end.column, 5u);
+    EXPECT_EQ(e->source.range.end.column, 10u);
 
     ASSERT_TRUE(e->Is<ast::BinaryExpression>());
     auto* rel = e->As<ast::BinaryExpression>();
@@ -183,7 +183,7 @@
 TEST_F(WGSLParserTest, RelationalExpression_PostUnary_InvalidRHS) {
     auto p = parser("true < if (a) {}");
     auto lhs = p->unary_expression();
-    auto e = p->expect_relational_expression_post_unary_expression(lhs.value);
+    auto e = p->expect_relational_expression_post_unary_expression(lhs.value, lhs->source);
     ASSERT_TRUE(p->has_error());
     EXPECT_EQ(e.value, nullptr);
     EXPECT_EQ(p->error(), "1:8: unable to parse right side of < expression");
@@ -192,7 +192,7 @@
 TEST_F(WGSLParserTest, RelationalExpression_PostUnary_NoMatch_ReturnsLHS) {
     auto p = parser("a true");
     auto lhs = p->unary_expression();
-    auto e = p->expect_relational_expression_post_unary_expression(lhs.value);
+    auto e = p->expect_relational_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_FALSE(e.errored);
     EXPECT_FALSE(p->has_error()) << p->error();
     ASSERT_NE(e.value, nullptr);
@@ -247,9 +247,9 @@
     ASSERT_NE(e.value, nullptr);
 
     EXPECT_EQ(e->source.range.begin.line, 1u);
-    EXPECT_EQ(e->source.range.begin.column, 3u);
+    EXPECT_EQ(e->source.range.begin.column, 1u);
     EXPECT_EQ(e->source.range.end.line, 1u);
-    EXPECT_EQ(e->source.range.end.column, 5u);
+    EXPECT_EQ(e->source.range.end.column, 10u);
 
     ASSERT_TRUE(e->Is<ast::BinaryExpression>());
     auto* rel = e->As<ast::BinaryExpression>();
@@ -272,9 +272,9 @@
     ASSERT_NE(e.value, nullptr);
 
     EXPECT_EQ(e->source.range.begin.line, 1u);
-    EXPECT_EQ(e->source.range.begin.column, 3u);
+    EXPECT_EQ(e->source.range.begin.column, 1u);
     EXPECT_EQ(e->source.range.end.line, 1u);
-    EXPECT_EQ(e->source.range.end.column, 5u);
+    EXPECT_EQ(e->source.range.end.column, 10u);
 
     ASSERT_TRUE(e->Is<ast::BinaryExpression>());
     auto* rel = e->As<ast::BinaryExpression>();
diff --git a/src/tint/lang/wgsl/reader/parser/shift_expression_test.cc b/src/tint/lang/wgsl/reader/parser/shift_expression_test.cc
index 77ec7db..011b131 100644
--- a/src/tint/lang/wgsl/reader/parser/shift_expression_test.cc
+++ b/src/tint/lang/wgsl/reader/parser/shift_expression_test.cc
@@ -33,15 +33,15 @@
 TEST_F(WGSLParserTest, ShiftExpression_PostUnary_Parses_ShiftLeft) {
     auto p = parser("a << true");
     auto lhs = p->unary_expression();
-    auto e = p->expect_shift_expression_post_unary_expression(lhs.value);
+    auto e = p->expect_shift_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_FALSE(e.errored);
     EXPECT_FALSE(p->has_error()) << p->error();
     ASSERT_NE(e.value, nullptr);
 
     EXPECT_EQ(e->source.range.begin.line, 1u);
-    EXPECT_EQ(e->source.range.begin.column, 3u);
+    EXPECT_EQ(e->source.range.begin.column, 1u);
     EXPECT_EQ(e->source.range.end.line, 1u);
-    EXPECT_EQ(e->source.range.end.column, 5u);
+    EXPECT_EQ(e->source.range.end.column, 10u);
 
     ASSERT_TRUE(e->Is<ast::BinaryExpression>());
     auto* rel = e->As<ast::BinaryExpression>();
@@ -58,15 +58,15 @@
 TEST_F(WGSLParserTest, ShiftExpression_PostUnary_Parses_ShiftRight) {
     auto p = parser("a >> true");
     auto lhs = p->unary_expression();
-    auto e = p->expect_shift_expression_post_unary_expression(lhs.value);
+    auto e = p->expect_shift_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_FALSE(e.errored);
     EXPECT_FALSE(p->has_error()) << p->error();
     ASSERT_NE(e.value, nullptr);
 
     EXPECT_EQ(e->source.range.begin.line, 1u);
-    EXPECT_EQ(e->source.range.begin.column, 3u);
+    EXPECT_EQ(e->source.range.begin.column, 1u);
     EXPECT_EQ(e->source.range.end.line, 1u);
-    EXPECT_EQ(e->source.range.end.column, 5u);
+    EXPECT_EQ(e->source.range.end.column, 10u);
 
     ASSERT_TRUE(e->Is<ast::BinaryExpression>());
     auto* rel = e->As<ast::BinaryExpression>();
@@ -83,7 +83,7 @@
 TEST_F(WGSLParserTest, ShiftExpression_PostUnary_Parses_Additive) {
     auto p = parser("a + b");
     auto lhs = p->unary_expression();
-    auto e = p->expect_shift_expression_post_unary_expression(lhs.value);
+    auto e = p->expect_shift_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_FALSE(e.errored);
     EXPECT_FALSE(p->has_error()) << p->error();
     ASSERT_NE(e.value, nullptr);
@@ -104,7 +104,7 @@
 TEST_F(WGSLParserTest, ShiftExpression_PostUnary_Parses_Multiplicative) {
     auto p = parser("a * b");
     auto lhs = p->unary_expression();
-    auto e = p->expect_shift_expression_post_unary_expression(lhs.value);
+    auto e = p->expect_shift_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_FALSE(e.errored);
     EXPECT_FALSE(p->has_error()) << p->error();
     ASSERT_NE(e.value, nullptr);
@@ -125,7 +125,7 @@
 TEST_F(WGSLParserTest, ShiftExpression_PostUnary_InvalidSpaceLeft) {
     auto p = parser("a < < true");
     auto lhs = p->unary_expression();
-    auto e = p->expect_shift_expression_post_unary_expression(lhs.value);
+    auto e = p->expect_shift_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_FALSE(e.errored);
     ASSERT_NE(e.value, nullptr);
     EXPECT_FALSE(e.value->Is<ast::BinaryExpression>());
@@ -134,7 +134,7 @@
 TEST_F(WGSLParserTest, ShiftExpression_PostUnary_InvalidSpaceRight) {
     auto p = parser("a > > true");
     auto lhs = p->unary_expression();
-    auto e = p->expect_shift_expression_post_unary_expression(lhs.value);
+    auto e = p->expect_shift_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_FALSE(e.errored);
     ASSERT_NE(e.value, nullptr);
     EXPECT_FALSE(e.value->Is<ast::BinaryExpression>());
@@ -143,7 +143,7 @@
 TEST_F(WGSLParserTest, ShiftExpression_PostUnary_InvalidRHS) {
     auto p = parser("a << if (a) {}");
     auto lhs = p->unary_expression();
-    auto e = p->expect_shift_expression_post_unary_expression(lhs.value);
+    auto e = p->expect_shift_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_TRUE(e.errored);
     EXPECT_TRUE(p->has_error());
     EXPECT_EQ(e.value, nullptr);
@@ -153,7 +153,7 @@
 TEST_F(WGSLParserTest, ShiftExpression_PostUnary_NoOr_ReturnsLHS) {
     auto p = parser("a true");
     auto lhs = p->unary_expression();
-    auto e = p->expect_shift_expression_post_unary_expression(lhs.value);
+    auto e = p->expect_shift_expression_post_unary_expression(lhs.value, lhs->source);
     EXPECT_FALSE(e.errored);
     EXPECT_FALSE(p->has_error()) << p->error();
     ASSERT_NE(e.value, nullptr);
diff --git a/src/tint/lang/wgsl/reader/parser/struct_member_attribute_test.cc b/src/tint/lang/wgsl/reader/parser/struct_member_attribute_test.cc
index 50b7908..f4169ac 100644
--- a/src/tint/lang/wgsl/reader/parser/struct_member_attribute_test.cc
+++ b/src/tint/lang/wgsl/reader/parser/struct_member_attribute_test.cc
@@ -233,7 +233,11 @@
     EXPECT_EQ(attr.value, nullptr);
     EXPECT_TRUE(p->has_error());
 
-    EXPECT_EQ(p->error(), "1:9: mixing '+' and '<<' requires parenthesis");
+    EXPECT_EQ(p->builder().Diagnostics().str(),
+              R"(test.wgsl:1:9 error: mixing '+' and '<<' requires parenthesis
+align(4 + 5 << 6)
+        ^^^^^^
+)");
 }
 
 TEST_F(WGSLParserTest, Attribute_Index) {
diff --git a/src/tint/lang/wgsl/resolver/uniformity.cc b/src/tint/lang/wgsl/resolver/uniformity.cc
index b3dc470..709da02 100644
--- a/src/tint/lang/wgsl/resolver/uniformity.cc
+++ b/src/tint/lang/wgsl/resolver/uniformity.cc
@@ -1232,10 +1232,18 @@
                     node->AddEdge(cf);
 
                     auto* current_value = current_function_->variables.Get(param);
-                    if (param->Type()->Is<core::type::Pointer>()) {
+                    if (auto* ptr = param->Type()->As<core::type::Pointer>()) {
                         if (load_rule) {
-                            // We are loading from the pointer, so add an edge to its contents.
-                            node->AddEdge(current_value);
+                            if (ptr->AddressSpace() == core::AddressSpace::kFunction ||
+                                ptr->Access() == core::Access::kRead) {
+                                // We are loading from a pointer to a function-scope variable or an
+                                // immutable module-scope variable, so add an edge to its contents.
+                                node->AddEdge(current_value);
+                            } else {
+                                // We are loading from a pointer to a mutable module-scope variable,
+                                // which always has non-uniform contents.
+                                node->AddEdge(current_function_->may_be_non_uniform);
+                            }
                         } else {
                             // This is a pointer parameter that we are not loading from, so add an
                             // edge to the pointer value itself.
diff --git a/src/tint/lang/wgsl/resolver/uniformity_test.cc b/src/tint/lang/wgsl/resolver/uniformity_test.cc
index 422c711..cbab7db 100644
--- a/src/tint/lang/wgsl/resolver/uniformity_test.cc
+++ b/src/tint/lang/wgsl/resolver/uniformity_test.cc
@@ -2543,9 +2543,9 @@
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
-test:7:34 note: control flow depends on possibly non-uniform value
+test:7:7 note: control flow depends on possibly non-uniform value
   if ((non_uniform_global == 42) && false) {
-                                 ^^
+      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
 test:7:8 note: reading from read_write storage buffer 'non_uniform_global' may result in a non-uniform value
   if ((non_uniform_global == 42) && false) {
@@ -2601,9 +2601,9 @@
     workgroupBarrier();
     ^^^^^^^^^^^^^^^^
 
-test:7:34 note: control flow depends on possibly non-uniform value
+test:7:7 note: control flow depends on possibly non-uniform value
   if ((non_uniform_global == 42) || true) {
-                                 ^^
+      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
 test:7:8 note: reading from read_write storage buffer 'non_uniform_global' may result in a non-uniform value
   if ((non_uniform_global == 42) || true) {
@@ -3750,7 +3750,7 @@
 
 test:12:7 note: possibly non-uniform value passed via pointer here
   bar(&v);
-      ^
+      ^^
 
 test:11:11 note: reading from read_write storage buffer 'non_uniform' may result in a non-uniform value
   var v = non_uniform;
@@ -3816,14 +3816,6 @@
 test:7:8 note: parameter 'p' of 'bar' may be non-uniform
   if (*p == 0) {
        ^
-
-test:13:7 note: possibly non-uniform value passed via pointer here
-  bar(&non_uniform);
-      ^
-
-test:4:48 note: reading from read_write storage buffer 'non_uniform' may result in a non-uniform value
-@group(0) @binding(0) var<storage, read_write> non_uniform : i32;
-                                               ^^^^^^^^^^^
 )");
 }
 
@@ -3854,9 +3846,9 @@
   if (0 == bar(&non_uniform)) {
   ^^
 
-test:4:48 note: reading from read_write storage buffer 'non_uniform' may result in a non-uniform value
-@group(0) @binding(0) var<storage, read_write> non_uniform : i32;
-                                               ^^^^^^^^^^^
+test:11:12 note: return value of 'bar' may be non-uniform
+  if (0 == bar(&non_uniform)) {
+           ^^^^^^^^^^^^^^^^^
 )");
 }
 
@@ -3893,7 +3885,7 @@
 
 test:13:7 note: possibly non-uniform value passed via pointer here
   bar(&v);
-      ^
+      ^^
 
 test:12:11 note: reading from read_write storage buffer 'non_uniform' may result in a non-uniform value
   var v = non_uniform;
@@ -3934,7 +3926,7 @@
 
 test:12:7 note: possibly non-uniform value passed via pointer here
   bar(&v);
-      ^
+      ^^
 
 test:11:11 note: reading from read_write storage buffer 'non_uniform' may result in a non-uniform value
   var v = non_uniform;
@@ -4439,7 +4431,7 @@
 
 test:10:7 note: contents of pointer may become non-uniform after calling 'bar'
   bar(&v);
-      ^
+      ^^
 )");
 }
 
@@ -4703,7 +4695,7 @@
 
 test:16:7 note: contents of pointer may become non-uniform after calling 'bar'
   bar(&v);
-      ^
+      ^^
 )");
 }
 
@@ -4740,7 +4732,7 @@
 
 test:14:7 note: contents of pointer may become non-uniform after calling 'bar'
   bar(&v);
-      ^
+      ^^
 )");
 }
 
@@ -4777,7 +4769,7 @@
 
 test:14:7 note: contents of pointer may become non-uniform after calling 'bar'
   bar(&v);
-      ^
+      ^^
 )");
 }
 
@@ -4822,7 +4814,7 @@
 
 test:22:7 note: contents of pointer may become non-uniform after calling 'bar'
   bar(&v);
-      ^
+      ^^
 )");
 }
 
@@ -4861,7 +4853,7 @@
 
 test:16:7 note: contents of pointer may become non-uniform after calling 'bar'
   bar(&v);
-      ^
+      ^^
 )");
 }
 
@@ -5164,7 +5156,7 @@
 
 test:12:11 note: contents of pointer may become non-uniform after calling 'bar'
   bar(&a, &b);
-          ^
+          ^^
 )");
 }
 
@@ -5257,7 +5249,7 @@
 
 test:12:11 note: contents of pointer may become non-uniform after calling 'bar'
   bar(&a, &b);
-          ^
+          ^^
 )");
 }
 
@@ -5361,6 +5353,128 @@
 note: reading from module-scope private variable 'non_uniform_global' may result in a non-uniform value)");
 }
 
+TEST_F(UniformityAnalysisTest, AssignUniformToPrivatePointerParameter_StillNonUniform) {
+    std::string src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+var<private> non_uniform : i32;
+
+fn bar(p : ptr<private, i32>) {
+  *p = 0;
+  if (*p == 0) {
+    workgroupBarrier();
+  }
+}
+
+fn foo() {
+  bar(&non_uniform);
+}
+)";
+
+    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 possibly non-uniform value
+  if (*p == 0) {
+  ^^
+
+test:8:8 note: parameter 'p' of 'bar' may be non-uniform
+  if (*p == 0) {
+       ^
+)");
+}
+
+TEST_F(UniformityAnalysisTest, AssignUniformToWorkgroupPointerParameter_StillNonUniform) {
+    std::string src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+var<workgroup> non_uniform : i32;
+
+fn bar(p : ptr<workgroup, i32>) {
+  *p = 0;
+  if (*p == 0) {
+    workgroupBarrier();
+  }
+}
+
+fn foo() {
+  bar(&non_uniform);
+}
+)";
+
+    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 possibly non-uniform value
+  if (*p == 0) {
+  ^^
+
+test:8:8 note: parameter 'p' of 'bar' may be non-uniform
+  if (*p == 0) {
+       ^
+)");
+}
+
+TEST_F(UniformityAnalysisTest, AssignUniformToStoragePointerParameter_StillNonUniform) {
+    std::string src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+@group(0) @binding(0) var<storage, read_write> non_uniform : i32;
+
+fn bar(p : ptr<storage, i32, read_write>) {
+  *p = 0;
+  if (*p == 0) {
+    workgroupBarrier();
+  }
+}
+
+fn foo() {
+  bar(&non_uniform);
+}
+)";
+
+    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 possibly non-uniform value
+  if (*p == 0) {
+  ^^
+
+test:8:8 note: parameter 'p' of 'bar' may be non-uniform
+  if (*p == 0) {
+       ^
+)");
+}
+
+TEST_F(UniformityAnalysisTest, LoadFromReadOnlyStoragePointerParameter_AlwaysUniform) {
+    std::string src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+@group(0) @binding(0) var<storage, read> non_uniform : i32;
+
+fn bar(p : ptr<storage, i32, read>) {
+  if (*p == 0) {
+    workgroupBarrier();
+  }
+}
+
+fn foo() {
+  bar(&non_uniform);
+}
+)";
+
+    RunTest(src, true);
+}
+
 ////////////////////////////////////////////////////////////////////////////////
 /// Tests to cover access to aggregate types.
 ////////////////////////////////////////////////////////////////////////////////
@@ -7944,7 +8058,7 @@
 
 test:15:9 note: possibly non-uniform value passed via pointer here
   v[bar(&f)] += 1;
-        ^
+        ^^
 
 test:14:11 note: reading from read_write storage buffer 'rw' may result in a non-uniform value
   var f = rw;
@@ -8020,7 +8134,7 @@
 
 test:15:9 note: possibly non-uniform value passed via pointer here
   v[bar(&f)]++;
-        ^
+        ^^
 
 test:14:11 note: reading from read_write storage buffer 'rw' may result in a non-uniform value
   var f = rw;
@@ -8093,11 +8207,11 @@
 
 test:19:22 note: possibly non-uniform value passed via pointer here
   arr[a(&i)] = arr[b(&i)];
-                     ^
+                     ^^
 
 test:19:9 note: contents of pointer may become non-uniform after calling 'a'
   arr[a(&i)] = arr[b(&i)];
-        ^
+        ^^
 )");
 }
 
@@ -8330,11 +8444,11 @@
 
 test:19:23 note: possibly non-uniform value passed via pointer here
   arr[a(&i)] += arr[b(&i)];
-                      ^
+                      ^^
 
 test:19:9 note: contents of pointer may become non-uniform after calling 'a'
   arr[a(&i)] += arr[b(&i)];
-        ^
+        ^^
 )");
 }
 
@@ -8468,9 +8582,9 @@
   let b = (non_uniform_global == 0) && (dpdx(1.0) == 0.0);
                                         ^^^^^^^^^
 
-test:5:37 note: control flow depends on possibly non-uniform value
+test:5:11 note: control flow depends on possibly non-uniform value
   let b = (non_uniform_global == 0) && (dpdx(1.0) == 0.0);
-                                    ^^
+          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
 test:5:12 note: reading from read_write storage buffer 'non_uniform_global' may result in a non-uniform value
   let b = (non_uniform_global == 0) && (dpdx(1.0) == 0.0);
@@ -8725,9 +8839,9 @@
   if (0 == bar(p)) {
   ^^
 
-test:4:48 note: reading from read_write storage buffer 'arr' may result in a non-uniform value
-@group(0) @binding(0) var<storage, read_write> arr : array<u32>;
-                                               ^^^
+test:19:12 note: return value of 'bar' may be non-uniform
+  if (0 == bar(p)) {
+           ^^^^^^
 )");
 }
 
@@ -8789,7 +8903,7 @@
     EXPECT_EQ(error_,
               R"(test:8:28 error: possibly non-uniform value passed here
   if (workgroupUniformLoad(&data[idx]) > 0) {
-                           ^
+                           ^^^^^^^^^^
 
 test:8:34 note: builtin 'idx' of 'main' may be non-uniform
   if (workgroupUniformLoad(&data[idx]) > 0) {
@@ -8829,7 +8943,7 @@
 
 test:14:11 note: possibly non-uniform value passed here
   if (foo(&data[idx]) > 0) {
-          ^
+          ^^^^^^^^^^
 
 test:14:17 note: builtin 'idx' of 'main' may be non-uniform
   if (foo(&data[idx]) > 0) {