[ir] Update DirectrVariableAccess to add `handle`.

This CL adds a `transform_handle` flag to DirectVariableAccess which
will also move handle objects into their local functions from
parameters.

Change-Id: I14c95f12d2f049e7f6bd17f68407f4165ffe4601
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/209196
Reviewed-by: James Price <jrprice@google.com>
Commit-Queue: dan sinclair <dsinclair@chromium.org>
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 dfc01e4..9ed4f09 100644
--- a/src/tint/lang/core/ir/transform/direct_variable_access.cc
+++ b/src/tint/lang/core/ir/transform/direct_variable_access.cc
@@ -103,9 +103,9 @@
 /// A AccessShape describes the static "path" from a root variable to an element within the
 /// variable.
 ///
-/// Functions that have pointer parameters which need transforming will be forked into one or more
-/// 'variants'. Each variant has different AccessShapes for the pointer parameters - the transform
-/// will only emit one variant when the shapes of the pointer parameter accesses match.
+/// Functions that have parameters which need transforming will be forked into one or more
+/// 'variants'. Each variant has different AccessShapes for the parameters - the transform will only
+/// emit one variant when the shapes of the parameter accesses match.
 ///
 /// Array accessors index expressions are held externally to the AccessShape, so
 /// AccessShape will be considered equal even if the array or matrix index values differ.
@@ -260,13 +260,15 @@
     /// transforming. These functions will be replaced with variants based on the access shapes.
     void GatherFnsThatNeedForking() {
         for (auto& fn : ir.functions) {
-            if (fn->Alive()) {
-                for (auto* param : fn->Params()) {
-                    if (ParamNeedsTransforming(param)) {
-                        need_forking.Add(fn, fn_info_allocator.Create());
-                        break;
-                    }
+            if (!fn->Alive()) {
+                continue;
+            }
+            for (auto* param : fn->Params()) {
+                if (!NeedsTransforming(param)) {
+                    continue;
                 }
+                need_forking.Add(fn, fn_info_allocator.Create());
+                break;
             }
         }
     }
@@ -311,7 +313,17 @@
             for (size_t i = 0, n = call->Args().Length(); i < n; i++) {
                 auto* arg = call->Args()[i];
                 auto* param = target->Params()[i];
-                if (ParamNeedsTransforming(param)) {
+
+                if (HandleNeedsTransforming(param)) {
+                    b.InsertBefore(call, [&] {
+                        // Get the handle chain for the pointer argument.
+                        auto chain = HandleChainFor(arg);
+                        // Record the parameter shape for the variant's signature.
+                        signature.Add(i, chain.shape);
+                    });
+                    // Record that this handle argument has been replaced.
+                    replaced_args.Push(arg);
+                } else if (ParamNeedsTransforming(param)) {
                     // This argument needs replacing with:
                     // * Nothing: root is a module-scope var and the access chain has no indicies.
                     // * A single pointer argument to the root variable: The root is a pointer
@@ -394,6 +406,35 @@
         }
     }
 
+    /// Walks the instructions that built `value` to obtain the root variable.
+    /// @param value the value to get the root for
+    /// @return an AccessChain
+    AccessChain HandleChainFor(Value* value) {
+        AccessChain chain;
+        TINT_ASSERT(value->Alive());
+
+        tint::Switch(
+            value,  //
+            [&](InstructionResult* res) {
+                auto* inst = res->Instruction()->As<Load>();
+                TINT_ASSERT(inst);
+
+                auto* var_res = inst->From()->As<InstructionResult>();
+                TINT_ASSERT(var_res);
+
+                auto* var = var_res->Instruction()->As<Var>();
+                TINT_ASSERT(var);
+                TINT_ASSERT(var->Block() == ir.root_block);
+
+                // Root handle is a module-scope 'var'
+                chain.shape.root = RootModuleScopeVar{var};
+                chain.root_ptr = var->Result(0);
+            },
+            TINT_ICE_ON_NO_MATCH);
+
+        return chain;
+    }
+
     /// Walks the instructions that built #value to obtain the root variable and the pointer
     /// accesses.
     /// @param value the pointer value to get the access chain for
@@ -500,7 +541,7 @@
             // For each parameter in the original function...
             for (size_t param_idx = 0; param_idx < old_params.Length(); param_idx++) {
                 auto* old_param = old_params[param_idx];
-                if (!ParamNeedsTransforming(old_param)) {
+                if (!NeedsTransforming(old_param)) {
                     // Parameter does not need transforming.
                     new_params.Push(old_param);
                     continue;
@@ -526,38 +567,46 @@
                     TINT_ICE() << "unhandled AccessShape root variant";
                 }
 
-                // Build the access indices parameter, if required.
-                ir::FunctionParam* indices_param = nullptr;
-                if (uint32_t n = shape->NumIndexAccesses(); n > 0) {
-                    // Indices are passed as an array of u32
-                    indices_param = b.FunctionParam(ty.array(ty.u32(), n));
-                    new_params.Push(indices_param);
+                if (ParamNeedsTransforming(old_param)) {
+                    // Build the access indices parameter, if required.
+                    ir::FunctionParam* indices_param = nullptr;
+                    if (uint32_t n = shape->NumIndexAccesses(); n > 0) {
+                        // Indices are passed as an array of u32
+                        indices_param = b.FunctionParam(ty.array(ty.u32(), n));
+                        new_params.Push(indices_param);
+                    }
+
+                    // Generate names for the new parameter(s) based on the replaced parameter name.
+                    if (auto param_name = ir.NameOf(old_param); param_name.IsValid()) {
+                        // Propagate old parameter name to the new parameters
+                        if (root_ptr_param) {
+                            ir.SetName(root_ptr_param, param_name.Name() + "_root");
+                        }
+                        if (indices_param) {
+                            ir.SetName(indices_param, param_name.Name() + "_indices");
+                        }
+                    }
+
+                    // Rebuild the pointer from the root pointer and accesses.
+                    uint32_t index_index = 0;
+                    auto chain = Transform(shape->ops, [&](const AccessOp& op) -> Value* {
+                        if (auto* m = std::get_if<MemberAccess>(&op)) {
+                            return b.Constant(u32(m->member->Index()));
+                        }
+                        auto* access = b.Access(ty.u32(), indices_param, u32(index_index++));
+                        return access->Result(0);
+                    });
+                    auto* access = b.Access(old_param->Type(), root_ptr, std::move(chain));
+
+                    // Replace the now removed parameter value with the access instruction
+                    old_param->ReplaceAllUsesWith(access->Result(0));
+                } else if (HandleNeedsTransforming(old_param)) {
+                    auto* load = b.Load(root_ptr);
+
+                    // Replace the now removed parameter value with the load instruction
+                    old_param->ReplaceAllUsesWith(load->Result(0));
                 }
 
-                // Generate names for the new parameter(s) based on the replaced parameter name.
-                if (auto param_name = ir.NameOf(old_param); param_name.IsValid()) {
-                    // Propagate old parameter name to the new parameters
-                    if (root_ptr_param) {
-                        ir.SetName(root_ptr_param, param_name.Name() + "_root");
-                    }
-                    if (indices_param) {
-                        ir.SetName(indices_param, param_name.Name() + "_indices");
-                    }
-                }
-
-                // Rebuild the pointer from the root pointer and accesses.
-                uint32_t index_index = 0;
-                auto chain = Transform(shape->ops, [&](const AccessOp& op) -> Value* {
-                    if (auto* m = std::get_if<MemberAccess>(&op)) {
-                        return b.Constant(u32(m->member->Index()));
-                    }
-                    auto* access = b.Access(ty.u32(), indices_param, u32(index_index++));
-                    return access->Result(0);
-                });
-                auto* access = b.Access(old_param->Type(), root_ptr, std::move(chain));
-
-                // Replace the now removed parameter value with the access instruction
-                old_param->ReplaceAllUsesWith(access->Result(0));
                 old_param->Destroy();
             }
 
@@ -583,6 +632,20 @@
         }
     }
 
+    /// @return true if @p param is a parameter that requires transforming, based on the
+    /// transform options.
+    /// @param param the function parameter
+    bool NeedsTransforming(FunctionParam* param) const {
+        return HandleNeedsTransforming(param) || ParamNeedsTransforming(param);
+    }
+
+    /// @return true if @p param is a handle parameter that requires transforming, based on the
+    /// transform options.
+    /// @param param the function parameter
+    bool HandleNeedsTransforming(FunctionParam* param) const {
+        return options.transform_handle && param->Type()->IsHandle();
+    }
+
     /// @return true if @p param is a pointer parameter that requires transforming, based on the
     /// address space and transform options.
     /// @param param the function parameter
@@ -622,6 +685,12 @@
                 [&](Let* let) {
                     TINT_DEFER(let->Destroy());
                     return let->Value();
+                },
+                [&](Load* load) {
+                    if (options.transform_handle) {
+                        TINT_DEFER(load->Destroy());
+                    }
+                    return nullptr;
                 });
         }
     }
diff --git a/src/tint/lang/core/ir/transform/direct_variable_access.h b/src/tint/lang/core/ir/transform/direct_variable_access.h
index ced1943..65f220f 100644
--- a/src/tint/lang/core/ir/transform/direct_variable_access.h
+++ b/src/tint/lang/core/ir/transform/direct_variable_access.h
@@ -44,23 +44,26 @@
     bool transform_private = false;
     /// If true, then 'function' sub-object pointer arguments will be transformed.
     bool transform_function = false;
+    /// If true, then 'handle' sub-object handle type arguments will be transformed.
+    bool transform_handle = false;
 
     /// Reflection for this class
     TINT_REFLECT(DirectVariableAccessOptions, transform_private, transform_function);
 };
 
-/// DirectVariableAccess is a transform that transforms pointer parameters in the 'storage',
+/// DirectVariableAccess is a transform that transforms parameters in the 'storage',
 /// 'uniform' and 'workgroup' address space so that they're accessed directly by the function,
-/// instead of being passed by pointer.
+/// instead of being passed by pointer. It will potentiall also transform `private`, `handle` or
+/// `function` parameters depending on provided options.
 ///
-/// DirectVariableAccess works by creating specializations of functions that have pointer
-/// parameters, one specialization for each pointer argument's unique access chain 'shape' from a
-/// unique variable. Calls to specialized functions are transformed so that the pointer arguments
-/// are replaced with an array of access-chain indicies, and if the pointer is in the 'function' or
-/// 'private' address space, also with a pointer to the root object. For more information, see the
-/// comments in src/tint/lang/wgsl/ast/transform/direct_variable_access.cc.
+/// DirectVariableAccess works by creating specializations of functions that have matching
+/// parameters, one specialization for each argument's unique access chain 'shape' from a unique
+/// variable. Calls to specialized functions are transformed so that the arguments are replaced with
+/// an array of access-chain indices, and if the parameter is in the 'function' or 'private'
+/// address space, also with a pointer to the root object.
 ///
 /// @param module the module to transform
+/// @param options the options
 /// @returns error diagnostics on failure
 Result<SuccessType> DirectVariableAccess(Module& module,
                                          const DirectVariableAccessOptions& options);
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 fd89e61..60dcedd 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
@@ -33,6 +33,8 @@
 #include "src/tint/lang/core/type/array.h"
 #include "src/tint/lang/core/type/matrix.h"
 #include "src/tint/lang/core/type/pointer.h"
+#include "src/tint/lang/core/type/sampled_texture.h"
+#include "src/tint/lang/core/type/sampler.h"
 #include "src/tint/lang/core/type/struct.h"
 
 namespace tint::core::ir::transform {
@@ -43,14 +45,22 @@
 
 namespace {
 
+static constexpr DirectVariableAccessOptions kTransformHandle = {
+    /* transform_private */ false,
+    /* transform_function */ false,
+    /* transform_handle */ true,
+};
+
 static constexpr DirectVariableAccessOptions kTransformPrivate = {
     /* transform_private */ true,
     /* transform_function */ false,
+    /* transform_handle */ false,
 };
 
 static constexpr DirectVariableAccessOptions kTransformFunction = {
     /* transform_private */ false,
     /* transform_function */ true,
+    /* transform_handle */ false,
 };
 
 }  // namespace
@@ -323,6 +333,80 @@
     EXPECT_EQ(expect, str());
 }
 
+TEST_F(IR_DirectVariableAccessTest_RemoveUncalled, HandleTexture_Disabled) {
+    b.Append(b.ir.root_block, [&] { b.Var<private_>("keep_me", 42_i); });
+
+    auto* f = b.Function("f", ty.i32());
+    auto* p = b.FunctionParam(
+        "p", ty.Get<type::SampledTexture>(core::type::TextureDimension::k1d, ty.f32()));
+    f->SetParams({
+        b.FunctionParam("pre", ty.i32()),
+        p,
+        b.FunctionParam("post", ty.i32()),
+    });
+    b.Append(f->Block(), [&] { b.Return(f, b.Constant(2_i)); });
+
+    auto* src = R"(
+$B1: {  # root
+  %keep_me:ptr<private, i32, read_write> = var, 42i
+}
+
+%f = func(%pre:i32, %p:texture_1d<f32>, %post:i32):i32 {
+  $B2: {
+    ret 2i
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(DirectVariableAccess, DirectVariableAccessOptions{});
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_RemoveUncalled, HandleTexture_Enabled) {
+    b.Append(b.ir.root_block, [&] { b.Var<private_>("keep_me", 42_i); });
+
+    auto* f = b.Function("f", ty.u32());
+    auto* p = b.FunctionParam(
+        "p", ty.Get<type::SampledTexture>(core::type::TextureDimension::k1d, ty.f32()));
+    f->SetParams({
+        b.FunctionParam("pre", ty.i32()),
+        p,
+        b.FunctionParam("post", ty.i32()),
+    });
+    b.Append(f->Block(),
+             [&] { b.Return(f, b.Call(ty.u32(), core::BuiltinFn::kTextureDimensions, p, 0_u)); });
+
+    auto* src = R"(
+$B1: {  # root
+  %keep_me:ptr<private, i32, read_write> = var, 42i
+}
+
+%f = func(%pre:i32, %p:texture_1d<f32>, %post:i32):u32 {
+  $B2: {
+    %6:u32 = textureDimensions %p, 0u
+    ret %6
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+$B1: {  # root
+  %keep_me:ptr<private, i32, read_write> = var, 42i
+}
+
+)";
+    Run(DirectVariableAccess, kTransformHandle);
+
+    EXPECT_EQ(expect, str());
+}
+
 }  // namespace remove_uncalled
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -4979,6 +5063,695 @@
 }  // namespace function_as_tests
 
 ////////////////////////////////////////////////////////////////////////////////
+// 'handle' address space
+////////////////////////////////////////////////////////////////////////////////
+namespace handle_as_tests {
+
+using IR_DirectVariableAccessTest_HandleAS = TransformTest;
+
+TEST_F(IR_DirectVariableAccessTest_HandleAS, Enabled_LocalTextureSampler) {
+    auto* tex =
+        b.Var("tex", handle,
+              ty.Get<core::type::SampledTexture>(core::type::TextureDimension::k2d, ty.f32()),
+              core::Access::kReadWrite);
+    tex->SetBindingPoint(0, 0);
+    b.ir.root_block->Append(tex);
+
+    auto* samp = b.Var("samp", handle, ty.sampler(), core::Access::kReadWrite);
+    samp->SetBindingPoint(0, 1);
+    b.ir.root_block->Append(samp);
+
+    auto* fn = b.Function("f", ty.void_());
+    b.Append(fn->Block(), [&] {
+        auto* t = b.Load(tex);
+        auto* s = b.Load(samp);
+
+        b.Let("p", b.Call(ty.vec4<f32>(), core::BuiltinFn::kTextureGather, 0_u, t, s,
+                          b.Splat(ty.vec2<f32>(), 0_f)));
+        b.Return(fn);
+    });
+
+    auto* src = R"(
+$B1: {  # root
+  %tex:ptr<handle, texture_2d<f32>, read_write> = var @binding_point(0, 0)
+  %samp:ptr<handle, sampler, read_write> = var @binding_point(0, 1)
+}
+
+%f = func():void {
+  $B2: {
+    %4:texture_2d<f32> = load %tex
+    %5:sampler = load %samp
+    %6:vec4<f32> = textureGather 0u, %4, %5, vec2<f32>(0.0f)
+    %p:vec4<f32> = let %6
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;  // Nothing changes
+
+    Run(DirectVariableAccess, kTransformHandle);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_HandleAS, Enabled_LocalTextureParamSampler) {
+    auto* tex =
+        b.Var("tex", handle,
+              ty.Get<core::type::SampledTexture>(core::type::TextureDimension::k2d, ty.f32()),
+              core::Access::kReadWrite);
+    tex->SetBindingPoint(0, 0);
+    b.ir.root_block->Append(tex);
+
+    auto* samp = b.Var("samp", handle, ty.sampler(), core::Access::kReadWrite);
+    samp->SetBindingPoint(0, 1);
+    b.ir.root_block->Append(samp);
+
+    auto* s = b.FunctionParam("s", ty.sampler());
+
+    auto* fn = b.Function("f", ty.void_());
+    fn->SetParams({s});
+    b.Append(fn->Block(), [&] {
+        auto* t = b.Load(tex);
+
+        b.Let("p", b.Call(ty.vec4<f32>(), core::BuiltinFn::kTextureGather, 0_u, t, s,
+                          b.Splat(ty.vec2<f32>(), 0_f)));
+        b.Return(fn);
+    });
+
+    auto* fn2 = b.Function("g", ty.void_());
+    b.Append(fn2->Block(), [&] {
+        auto* s2 = b.Load(samp);
+        b.Call(ty.void_(), fn, s2);
+        b.Return(fn2);
+    });
+
+    auto* src = R"(
+$B1: {  # root
+  %tex:ptr<handle, texture_2d<f32>, read_write> = var @binding_point(0, 0)
+  %samp:ptr<handle, sampler, read_write> = var @binding_point(0, 1)
+}
+
+%f = func(%s:sampler):void {
+  $B2: {
+    %5:texture_2d<f32> = load %tex
+    %6:vec4<f32> = textureGather 0u, %5, %s, vec2<f32>(0.0f)
+    %p:vec4<f32> = let %6
+    ret
+  }
+}
+%g = func():void {
+  $B3: {
+    %9:sampler = load %samp
+    %10:void = call %f, %9
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+$B1: {  # root
+  %tex:ptr<handle, texture_2d<f32>, read_write> = var @binding_point(0, 0)
+  %samp:ptr<handle, sampler, read_write> = var @binding_point(0, 1)
+}
+
+%f = func():void {
+  $B2: {
+    %4:sampler = load %samp
+    %5:texture_2d<f32> = load %tex
+    %6:vec4<f32> = textureGather 0u, %5, %4, vec2<f32>(0.0f)
+    %p:vec4<f32> = let %6
+    ret
+  }
+}
+%g = func():void {
+  $B3: {
+    %9:void = call %f
+    ret
+  }
+}
+)";
+
+    Run(DirectVariableAccess, kTransformHandle);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_HandleAS, Enabled_ParamTextureLocalSampler) {
+    auto* tex_ty = ty.Get<core::type::SampledTexture>(core::type::TextureDimension::k2d, ty.f32());
+    auto* tex = b.Var("tex", handle, tex_ty, core::Access::kReadWrite);
+    tex->SetBindingPoint(0, 0);
+    b.ir.root_block->Append(tex);
+
+    auto* samp = b.Var("samp", handle, ty.sampler(), core::Access::kReadWrite);
+    samp->SetBindingPoint(0, 1);
+    b.ir.root_block->Append(samp);
+
+    auto* t = b.FunctionParam("t", tex_ty);
+
+    auto* fn = b.Function("f", ty.void_());
+    fn->SetParams({t});
+    b.Append(fn->Block(), [&] {
+        auto* s = b.Load(samp);
+
+        b.Let("p", b.Call(ty.vec4<f32>(), core::BuiltinFn::kTextureGather, 0_u, t, s,
+                          b.Splat(ty.vec2<f32>(), 0_f)));
+        b.Return(fn);
+    });
+
+    auto* fn2 = b.Function("g", ty.void_());
+    b.Append(fn2->Block(), [&] {
+        auto* t2 = b.Load(tex);
+        b.Call(ty.void_(), fn, t2);
+        b.Return(fn2);
+    });
+
+    auto* src = R"(
+$B1: {  # root
+  %tex:ptr<handle, texture_2d<f32>, read_write> = var @binding_point(0, 0)
+  %samp:ptr<handle, sampler, read_write> = var @binding_point(0, 1)
+}
+
+%f = func(%t:texture_2d<f32>):void {
+  $B2: {
+    %5:sampler = load %samp
+    %6:vec4<f32> = textureGather 0u, %t, %5, vec2<f32>(0.0f)
+    %p:vec4<f32> = let %6
+    ret
+  }
+}
+%g = func():void {
+  $B3: {
+    %9:texture_2d<f32> = load %tex
+    %10:void = call %f, %9
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+$B1: {  # root
+  %tex:ptr<handle, texture_2d<f32>, read_write> = var @binding_point(0, 0)
+  %samp:ptr<handle, sampler, read_write> = var @binding_point(0, 1)
+}
+
+%f = func():void {
+  $B2: {
+    %4:texture_2d<f32> = load %tex
+    %5:sampler = load %samp
+    %6:vec4<f32> = textureGather 0u, %4, %5, vec2<f32>(0.0f)
+    %p:vec4<f32> = let %6
+    ret
+  }
+}
+%g = func():void {
+  $B3: {
+    %9:void = call %f
+    ret
+  }
+}
+)";
+
+    Run(DirectVariableAccess, kTransformHandle);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_HandleAS, Enabled_ParamTextureParamSampler) {
+    auto* tex_ty = ty.Get<core::type::SampledTexture>(core::type::TextureDimension::k2d, ty.f32());
+    auto* tex = b.Var("tex", handle, tex_ty, core::Access::kReadWrite);
+    tex->SetBindingPoint(0, 0);
+    b.ir.root_block->Append(tex);
+
+    auto* samp = b.Var("samp", handle, ty.sampler(), core::Access::kReadWrite);
+    samp->SetBindingPoint(0, 1);
+    b.ir.root_block->Append(samp);
+
+    auto* t = b.FunctionParam("t", tex_ty);
+    auto* s = b.FunctionParam("s", ty.sampler());
+
+    auto* fn = b.Function("f", ty.void_());
+    fn->SetParams({t, s});
+    b.Append(fn->Block(), [&] {
+        b.Let("p", b.Call(ty.vec4<f32>(), core::BuiltinFn::kTextureGather, 0_u, t, s,
+                          b.Splat(ty.vec2<f32>(), 0_f)));
+        b.Return(fn);
+    });
+
+    auto* fn2 = b.Function("g", ty.void_());
+    b.Append(fn2->Block(), [&] {
+        auto* s2 = b.Load(samp);
+        auto* t2 = b.Load(tex);
+        b.Call(ty.void_(), fn, t2, s2);
+        b.Return(fn2);
+    });
+
+    auto* src = R"(
+$B1: {  # root
+  %tex:ptr<handle, texture_2d<f32>, read_write> = var @binding_point(0, 0)
+  %samp:ptr<handle, sampler, read_write> = var @binding_point(0, 1)
+}
+
+%f = func(%t:texture_2d<f32>, %s:sampler):void {
+  $B2: {
+    %6:vec4<f32> = textureGather 0u, %t, %s, vec2<f32>(0.0f)
+    %p:vec4<f32> = let %6
+    ret
+  }
+}
+%g = func():void {
+  $B3: {
+    %9:sampler = load %samp
+    %10:texture_2d<f32> = load %tex
+    %11:void = call %f, %10, %9
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+$B1: {  # root
+  %tex:ptr<handle, texture_2d<f32>, read_write> = var @binding_point(0, 0)
+  %samp:ptr<handle, sampler, read_write> = var @binding_point(0, 1)
+}
+
+%f = func():void {
+  $B2: {
+    %4:texture_2d<f32> = load %tex
+    %5:sampler = load %samp
+    %6:vec4<f32> = textureGather 0u, %4, %5, vec2<f32>(0.0f)
+    %p:vec4<f32> = let %6
+    ret
+  }
+}
+%g = func():void {
+  $B3: {
+    %9:void = call %f
+    ret
+  }
+}
+)";
+
+    Run(DirectVariableAccess, kTransformHandle);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_HandleAS, Enabled_MultiFunction) {
+    auto* tex_ty = ty.Get<core::type::SampledTexture>(core::type::TextureDimension::k2d, ty.f32());
+    auto* tex = b.Var("tex", handle, tex_ty, core::Access::kReadWrite);
+    tex->SetBindingPoint(0, 0);
+    b.ir.root_block->Append(tex);
+
+    auto* samp = b.Var("samp", handle, ty.sampler(), core::Access::kReadWrite);
+    samp->SetBindingPoint(0, 1);
+    b.ir.root_block->Append(samp);
+
+    auto* t = b.FunctionParam("t", tex_ty);
+    auto* s = b.FunctionParam("s", ty.sampler());
+
+    auto* fn = b.Function("f", ty.void_());
+    fn->SetParams({t, s});
+    b.Append(fn->Block(), [&] {
+        b.Let("p", b.Call(ty.vec4<f32>(), core::BuiltinFn::kTextureGather, 0_u, t, s,
+                          b.Splat(ty.vec2<f32>(), 0_f)));
+        b.Return(fn);
+    });
+
+    auto* t2 = b.FunctionParam("t", tex_ty);
+    auto* fn2 = b.Function("g", ty.void_());
+    fn2->SetParams({t2});
+    b.Append(fn2->Block(), [&] {
+        auto* s2 = b.Load(samp);
+        b.Call(ty.void_(), fn, t2, s2);
+        b.Return(fn2);
+    });
+
+    auto* fn3 = b.Function("h", ty.void_());
+    b.Append(fn3->Block(), [&] {
+        auto* t3 = b.Load(tex);
+        b.Call(ty.void_(), fn2, t3);
+        b.Return(fn3);
+    });
+
+    auto* src = R"(
+$B1: {  # root
+  %tex:ptr<handle, texture_2d<f32>, read_write> = var @binding_point(0, 0)
+  %samp:ptr<handle, sampler, read_write> = var @binding_point(0, 1)
+}
+
+%f = func(%t:texture_2d<f32>, %s:sampler):void {
+  $B2: {
+    %6:vec4<f32> = textureGather 0u, %t, %s, vec2<f32>(0.0f)
+    %p:vec4<f32> = let %6
+    ret
+  }
+}
+%g = func(%t_1:texture_2d<f32>):void {  # %t_1: 't'
+  $B3: {
+    %10:sampler = load %samp
+    %11:void = call %f, %t_1, %10
+    ret
+  }
+}
+%h = func():void {
+  $B4: {
+    %13:texture_2d<f32> = load %tex
+    %14:void = call %g, %13
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+$B1: {  # root
+  %tex:ptr<handle, texture_2d<f32>, read_write> = var @binding_point(0, 0)
+  %samp:ptr<handle, sampler, read_write> = var @binding_point(0, 1)
+}
+
+%f = func():void {
+  $B2: {
+    %4:texture_2d<f32> = load %tex
+    %5:sampler = load %samp
+    %6:vec4<f32> = textureGather 0u, %4, %5, vec2<f32>(0.0f)
+    %p:vec4<f32> = let %6
+    ret
+  }
+}
+%g = func():void {
+  $B3: {
+    %9:void = call %f
+    ret
+  }
+}
+%h = func():void {
+  $B4: {
+    %11:void = call %g
+    ret
+  }
+}
+)";
+
+    Run(DirectVariableAccess, kTransformHandle);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_HandleAS, Disabled_MultiFunction) {
+    auto* tex_ty = ty.Get<core::type::SampledTexture>(core::type::TextureDimension::k2d, ty.f32());
+    auto* tex = b.Var("tex", handle, tex_ty, core::Access::kReadWrite);
+    tex->SetBindingPoint(0, 0);
+    b.ir.root_block->Append(tex);
+
+    auto* samp = b.Var("samp", handle, ty.sampler(), core::Access::kReadWrite);
+    samp->SetBindingPoint(0, 1);
+    b.ir.root_block->Append(samp);
+
+    auto* t = b.FunctionParam("t", tex_ty);
+    auto* s = b.FunctionParam("s", ty.sampler());
+
+    auto* fn = b.Function("f", ty.void_());
+    fn->SetParams({t, s});
+    b.Append(fn->Block(), [&] {
+        b.Let("p", b.Call(ty.vec4<f32>(), core::BuiltinFn::kTextureGather, 0_u, t, s,
+                          b.Splat(ty.vec2<f32>(), 0_f)));
+        b.Return(fn);
+    });
+
+    auto* t2 = b.FunctionParam("t", tex_ty);
+    auto* fn2 = b.Function("g", ty.void_());
+    fn2->SetParams({t2});
+    b.Append(fn2->Block(), [&] {
+        auto* s2 = b.Load(samp);
+        b.Call(ty.void_(), fn, t2, s2);
+        b.Return(fn2);
+    });
+
+    auto* fn3 = b.Function("h", ty.void_());
+    b.Append(fn3->Block(), [&] {
+        auto* t3 = b.Load(tex);
+        b.Call(ty.void_(), fn2, t3);
+        b.Return(fn3);
+    });
+
+    auto* src = R"(
+$B1: {  # root
+  %tex:ptr<handle, texture_2d<f32>, read_write> = var @binding_point(0, 0)
+  %samp:ptr<handle, sampler, read_write> = var @binding_point(0, 1)
+}
+
+%f = func(%t:texture_2d<f32>, %s:sampler):void {
+  $B2: {
+    %6:vec4<f32> = textureGather 0u, %t, %s, vec2<f32>(0.0f)
+    %p:vec4<f32> = let %6
+    ret
+  }
+}
+%g = func(%t_1:texture_2d<f32>):void {  # %t_1: 't'
+  $B3: {
+    %10:sampler = load %samp
+    %11:void = call %f, %t_1, %10
+    ret
+  }
+}
+%h = func():void {
+  $B4: {
+    %13:texture_2d<f32> = load %tex
+    %14:void = call %g, %13
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;  // No change
+
+    Run(DirectVariableAccess, DirectVariableAccessOptions{});
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_HandleAS, Enabled_DuplicateParam) {
+    auto* tex_ty = ty.Get<core::type::SampledTexture>(core::type::TextureDimension::k2d, ty.f32());
+    auto* tex = b.Var("tex", handle, tex_ty, core::Access::kReadWrite);
+    tex->SetBindingPoint(0, 0);
+    b.ir.root_block->Append(tex);
+
+    auto* samp = b.Var("samp", handle, ty.sampler(), core::Access::kReadWrite);
+    samp->SetBindingPoint(0, 1);
+    b.ir.root_block->Append(samp);
+
+    auto* t1 = b.FunctionParam("t1", tex_ty);
+    auto* t2 = b.FunctionParam("t2", tex_ty);
+
+    auto* fn = b.Function("f", ty.void_());
+    fn->SetParams({t1, t2});
+    b.Append(fn->Block(), [&] {
+        auto* s = b.Load(samp);
+
+        b.Let("p1", b.Call(ty.vec4<f32>(), core::BuiltinFn::kTextureGather, 0_u, t1, s,
+                           b.Splat(ty.vec2<f32>(), 0_f)));
+        b.Let("p2", b.Call(ty.vec4<f32>(), core::BuiltinFn::kTextureGather, 0_u, t2, s,
+                           b.Splat(ty.vec2<f32>(), 0_f)));
+        b.Return(fn);
+    });
+
+    auto* fn2 = b.Function("g", ty.void_());
+    b.Append(fn2->Block(), [&] {
+        auto* t3 = b.Load(tex);
+        auto* t4 = b.Load(tex);
+        b.Call(ty.void_(), fn, t3, t4);
+        b.Return(fn2);
+    });
+
+    auto* src = R"(
+$B1: {  # root
+  %tex:ptr<handle, texture_2d<f32>, read_write> = var @binding_point(0, 0)
+  %samp:ptr<handle, sampler, read_write> = var @binding_point(0, 1)
+}
+
+%f = func(%t1:texture_2d<f32>, %t2:texture_2d<f32>):void {
+  $B2: {
+    %6:sampler = load %samp
+    %7:vec4<f32> = textureGather 0u, %t1, %6, vec2<f32>(0.0f)
+    %p1:vec4<f32> = let %7
+    %9:vec4<f32> = textureGather 0u, %t2, %6, vec2<f32>(0.0f)
+    %p2:vec4<f32> = let %9
+    ret
+  }
+}
+%g = func():void {
+  $B3: {
+    %12:texture_2d<f32> = load %tex
+    %13:texture_2d<f32> = load %tex
+    %14:void = call %f, %12, %13
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+$B1: {  # root
+  %tex:ptr<handle, texture_2d<f32>, read_write> = var @binding_point(0, 0)
+  %samp:ptr<handle, sampler, read_write> = var @binding_point(0, 1)
+}
+
+%f = func():void {
+  $B2: {
+    %4:texture_2d<f32> = load %tex
+    %5:texture_2d<f32> = load %tex
+    %6:sampler = load %samp
+    %7:vec4<f32> = textureGather 0u, %4, %6, vec2<f32>(0.0f)
+    %p1:vec4<f32> = let %7
+    %9:vec4<f32> = textureGather 0u, %5, %6, vec2<f32>(0.0f)
+    %p2:vec4<f32> = let %9
+    ret
+  }
+}
+%g = func():void {
+  $B3: {
+    %12:void = call %f
+    ret
+  }
+}
+)";
+
+    Run(DirectVariableAccess, kTransformHandle);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_HandleAS, Enabled_Fork) {
+    auto* tex_ty = ty.Get<core::type::SampledTexture>(core::type::TextureDimension::k2d, ty.f32());
+    auto* tex1 = b.Var("tex1", handle, tex_ty, core::Access::kReadWrite);
+    tex1->SetBindingPoint(0, 0);
+    b.ir.root_block->Append(tex1);
+    auto* tex2 = b.Var("tex2", handle, tex_ty, core::Access::kReadWrite);
+    tex2->SetBindingPoint(0, 1);
+    b.ir.root_block->Append(tex2);
+
+    auto* samp = b.Var("samp", handle, ty.sampler(), core::Access::kReadWrite);
+    samp->SetBindingPoint(0, 2);
+    b.ir.root_block->Append(samp);
+
+    auto* t = b.FunctionParam("t", tex_ty);
+
+    auto* fn = b.Function("f", ty.void_());
+    fn->SetParams({t});
+    b.Append(fn->Block(), [&] {
+        auto* s = b.Load(samp);
+        b.Let("p", b.Call(ty.vec4<f32>(), core::BuiltinFn::kTextureGather, 0_u, t, s,
+                          b.Splat(ty.vec2<f32>(), 0_f)));
+        b.Return(fn);
+    });
+
+    auto* fn2 = b.Function("g", ty.void_());
+    b.Append(fn2->Block(), [&] {
+        auto* t2 = b.Load(tex1);
+        b.Call(ty.void_(), fn, t2);
+        b.Return(fn2);
+    });
+
+    auto* fn3 = b.Function("h", ty.void_());
+    b.Append(fn3->Block(), [&] {
+        auto* t2 = b.Load(tex2);
+        b.Call(ty.void_(), fn, t2);
+        b.Return(fn3);
+    });
+
+    auto* src = R"(
+$B1: {  # root
+  %tex1:ptr<handle, texture_2d<f32>, read_write> = var @binding_point(0, 0)
+  %tex2:ptr<handle, texture_2d<f32>, read_write> = var @binding_point(0, 1)
+  %samp:ptr<handle, sampler, read_write> = var @binding_point(0, 2)
+}
+
+%f = func(%t:texture_2d<f32>):void {
+  $B2: {
+    %6:sampler = load %samp
+    %7:vec4<f32> = textureGather 0u, %t, %6, vec2<f32>(0.0f)
+    %p:vec4<f32> = let %7
+    ret
+  }
+}
+%g = func():void {
+  $B3: {
+    %10:texture_2d<f32> = load %tex1
+    %11:void = call %f, %10
+    ret
+  }
+}
+%h = func():void {
+  $B4: {
+    %13:texture_2d<f32> = load %tex2
+    %14:void = call %f, %13
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+$B1: {  # root
+  %tex1:ptr<handle, texture_2d<f32>, read_write> = var @binding_point(0, 0)
+  %tex2:ptr<handle, texture_2d<f32>, read_write> = var @binding_point(0, 1)
+  %samp:ptr<handle, sampler, read_write> = var @binding_point(0, 2)
+}
+
+%f = func():void {
+  $B2: {
+    %5:texture_2d<f32> = load %tex1
+    %6:sampler = load %samp
+    %7:vec4<f32> = textureGather 0u, %5, %6, vec2<f32>(0.0f)
+    %p:vec4<f32> = let %7
+    ret
+  }
+}
+%f_1 = func():void {  # %f_1: 'f'
+  $B3: {
+    %10:texture_2d<f32> = load %tex2
+    %11:sampler = load %samp
+    %12:vec4<f32> = textureGather 0u, %10, %11, vec2<f32>(0.0f)
+    %p_1:vec4<f32> = let %12  # %p_1: 'p'
+    ret
+  }
+}
+%g = func():void {
+  $B4: {
+    %15:void = call %f
+    ret
+  }
+}
+%h = func():void {
+  $B5: {
+    %17:void = call %f_1
+    ret
+  }
+}
+)";
+
+    Run(DirectVariableAccess, kTransformHandle);
+
+    EXPECT_EQ(expect, str());
+}
+
+}  // namespace handle_as_tests
+
+////////////////////////////////////////////////////////////////////////////////
 // builtin function calls
 ////////////////////////////////////////////////////////////////////////////////
 namespace builtin_fn_calls {