[tint][ir] Add DirectVariableAccess IR transform

This is a reimplementation of the tint::ast::transform::DirectVariableAccess transform, for the new IR system.

Change-Id: Id400f13334f51535dba42be280af2cd2df113731
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/152400
Commit-Queue: Ben Clayton <bclayton@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: James Price <jrprice@google.com>
diff --git a/src/tint/lang/core/ir/builder.h b/src/tint/lang/core/ir/builder.h
index 93e7aed..64448cf 100644
--- a/src/tint/lang/core/ir/builder.h
+++ b/src/tint/lang/core/ir/builder.h
@@ -834,7 +834,7 @@
     /// @returns the instruction
     template <core::AddressSpace SPACE, typename T, core::Access ACCESS = core::Access::kReadWrite>
     ir::Var* Var(std::string_view name) {
-        return Var(ir.Types().ptr<SPACE, T, ACCESS>(), name);
+        return Var(name, ir.Types().ptr<SPACE, T, ACCESS>());
     }
 
     /// Creates a new `let` declaration
diff --git a/src/tint/lang/core/ir/transform/BUILD.bazel b/src/tint/lang/core/ir/transform/BUILD.bazel
index f381912..e4c3240 100644
--- a/src/tint/lang/core/ir/transform/BUILD.bazel
+++ b/src/tint/lang/core/ir/transform/BUILD.bazel
@@ -33,6 +33,7 @@
     "block_decorated_structs.cc",
     "builtin_polyfill.cc",
     "demote_to_helper.cc",
+    "direct_variable_access.cc",
     "multiplanar_external_texture.cc",
     "robustness.cc",
     "shader_io.cc",
@@ -47,6 +48,7 @@
     "block_decorated_structs.h",
     "builtin_polyfill.h",
     "demote_to_helper.h",
+    "direct_variable_access.h",
     "multiplanar_external_texture.h",
     "robustness.h",
     "shader_io.h",
@@ -89,12 +91,18 @@
     "block_decorated_structs_test.cc",
     "builtin_polyfill_test.cc",
     "demote_to_helper_test.cc",
+    "direct_variable_access_test.cc",
     "helper_test.h",
     "multiplanar_external_texture_test.cc",
     "robustness_test.cc",
     "std140_test.cc",
     "zero_init_workgroup_memory_test.cc",
-  ],
+  ] + select({
+    ":tint_build_wgsl_reader_and_tint_build_wgsl_writer": [
+      "direct_variable_access_wgsl_test.cc",
+    ],
+    "//conditions:default": [],
+  }),
   deps = [
     "//src/tint/api/common",
     "//src/tint/api/options",
@@ -104,6 +112,14 @@
     "//src/tint/lang/core/ir",
     "//src/tint/lang/core/ir/transform",
     "//src/tint/lang/core/type",
+    "//src/tint/lang/wgsl",
+    "//src/tint/lang/wgsl/ast",
+    "//src/tint/lang/wgsl/program",
+    "//src/tint/lang/wgsl/reader",
+    "//src/tint/lang/wgsl/reader/program_to_ir",
+    "//src/tint/lang/wgsl/sem",
+    "//src/tint/lang/wgsl/writer",
+    "//src/tint/lang/wgsl/writer/ir_to_program",
     "//src/tint/utils/containers",
     "//src/tint/utils/diagnostic",
     "//src/tint/utils/ice",
@@ -123,3 +139,21 @@
   visibility = ["//visibility:public"],
 )
 
+alias(
+  name = "tint_build_wgsl_reader",
+  actual = "//src/tint:tint_build_wgsl_reader_true",
+)
+
+alias(
+  name = "tint_build_wgsl_writer",
+  actual = "//src/tint:tint_build_wgsl_writer_true",
+)
+
+selects.config_setting_group(
+    name = "tint_build_wgsl_reader_and_tint_build_wgsl_writer",
+    match_all = [
+        ":tint_build_wgsl_reader",
+        ":tint_build_wgsl_writer",
+    ],
+)
+
diff --git a/src/tint/lang/core/ir/transform/BUILD.cmake b/src/tint/lang/core/ir/transform/BUILD.cmake
index c441513..1164934 100644
--- a/src/tint/lang/core/ir/transform/BUILD.cmake
+++ b/src/tint/lang/core/ir/transform/BUILD.cmake
@@ -40,6 +40,8 @@
   lang/core/ir/transform/builtin_polyfill.h
   lang/core/ir/transform/demote_to_helper.cc
   lang/core/ir/transform/demote_to_helper.h
+  lang/core/ir/transform/direct_variable_access.cc
+  lang/core/ir/transform/direct_variable_access.h
   lang/core/ir/transform/multiplanar_external_texture.cc
   lang/core/ir/transform/multiplanar_external_texture.h
   lang/core/ir/transform/robustness.cc
@@ -87,6 +89,7 @@
   lang/core/ir/transform/block_decorated_structs_test.cc
   lang/core/ir/transform/builtin_polyfill_test.cc
   lang/core/ir/transform/demote_to_helper_test.cc
+  lang/core/ir/transform/direct_variable_access_test.cc
   lang/core/ir/transform/helper_test.h
   lang/core/ir/transform/multiplanar_external_texture_test.cc
   lang/core/ir/transform/robustness_test.cc
@@ -103,6 +106,14 @@
   tint_lang_core_ir
   tint_lang_core_ir_transform
   tint_lang_core_type
+  tint_lang_wgsl
+  tint_lang_wgsl_ast
+  tint_lang_wgsl_program
+  tint_lang_wgsl_reader
+  tint_lang_wgsl_reader_program_to_ir
+  tint_lang_wgsl_sem
+  tint_lang_wgsl_writer
+  tint_lang_wgsl_writer_ir_to_program
   tint_utils_containers
   tint_utils_diagnostic
   tint_utils_ice
@@ -121,3 +132,9 @@
 tint_target_add_external_dependencies(tint_lang_core_ir_transform_test test
   "gtest"
 )
+
+if(TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
+  tint_target_add_sources(tint_lang_core_ir_transform_test test
+    "lang/core/ir/transform/direct_variable_access_wgsl_test.cc"
+  )
+endif(TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
diff --git a/src/tint/lang/core/ir/transform/BUILD.gn b/src/tint/lang/core/ir/transform/BUILD.gn
index b907ec2..9ee4ae0 100644
--- a/src/tint/lang/core/ir/transform/BUILD.gn
+++ b/src/tint/lang/core/ir/transform/BUILD.gn
@@ -45,6 +45,8 @@
     "builtin_polyfill.h",
     "demote_to_helper.cc",
     "demote_to_helper.h",
+    "direct_variable_access.cc",
+    "direct_variable_access.h",
     "multiplanar_external_texture.cc",
     "multiplanar_external_texture.h",
     "robustness.cc",
@@ -90,6 +92,7 @@
       "block_decorated_structs_test.cc",
       "builtin_polyfill_test.cc",
       "demote_to_helper_test.cc",
+      "direct_variable_access_test.cc",
       "helper_test.h",
       "multiplanar_external_texture_test.cc",
       "robustness_test.cc",
@@ -106,6 +109,14 @@
       "${tint_src_dir}/lang/core/ir",
       "${tint_src_dir}/lang/core/ir/transform",
       "${tint_src_dir}/lang/core/type",
+      "${tint_src_dir}/lang/wgsl",
+      "${tint_src_dir}/lang/wgsl/ast",
+      "${tint_src_dir}/lang/wgsl/program",
+      "${tint_src_dir}/lang/wgsl/reader",
+      "${tint_src_dir}/lang/wgsl/reader/program_to_ir",
+      "${tint_src_dir}/lang/wgsl/sem",
+      "${tint_src_dir}/lang/wgsl/writer",
+      "${tint_src_dir}/lang/wgsl/writer/ir_to_program",
       "${tint_src_dir}/utils/containers",
       "${tint_src_dir}/utils/diagnostic",
       "${tint_src_dir}/utils/ice",
@@ -120,5 +131,9 @@
       "${tint_src_dir}/utils/text",
       "${tint_src_dir}/utils/traits",
     ]
+
+    if (tint_build_wgsl_reader && tint_build_wgsl_writer) {
+      sources += [ "direct_variable_access_wgsl_test.cc" ]
+    }
   }
 }
diff --git a/src/tint/lang/core/ir/transform/direct_variable_access.cc b/src/tint/lang/core/ir/transform/direct_variable_access.cc
new file mode 100644
index 0000000..5939b82
--- /dev/null
+++ b/src/tint/lang/core/ir/transform/direct_variable_access.cc
@@ -0,0 +1,671 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/lang/core/ir/transform/direct_variable_access.h"
+
+#include <string>
+#include <utility>
+
+#include "src/tint/lang/core/ir/builder.h"
+#include "src/tint/lang/core/ir/clone_context.h"
+#include "src/tint/lang/core/ir/module.h"
+#include "src/tint/lang/core/ir/traverse.h"
+#include "src/tint/lang/core/ir/user_call.h"
+#include "src/tint/lang/core/ir/validator.h"
+#include "src/tint/lang/core/ir/var.h"
+#include "src/tint/utils/containers/reverse.h"
+
+using namespace tint::core::fluent_types;     // NOLINT
+using namespace tint::core::number_suffixes;  // NOLINT
+
+namespace tint::core::ir::transform {
+namespace {
+
+/// An access root, originating from a module-scope var.
+/// These roots are not passed by parameter, but instead the callee references the module-scope var
+/// directly.
+struct RootModuleScopeVar {
+    /// The module-scope var
+    Var* var = nullptr;
+
+    /// @return a hash value for this object
+    uint64_t HashCode() const { return Hash(var); }
+
+    /// Inequality operator
+    bool operator!=(const RootModuleScopeVar& other) const { return var != other.var; }
+};
+
+/// An access root, originating from a pointer parameter or function-scope var.
+/// These roots are passed by pointer parameter.
+struct RootPtrParameter {
+    /// The parameter pointer type
+    const type::Pointer* type = nullptr;
+
+    /// @return a hash value for this object
+    uint64_t HashCode() const { return Hash(type); }
+
+    /// Inequality operator
+    bool operator!=(const RootPtrParameter& other) const { return type != other.type; }
+};
+
+/// An access root. Either a RootModuleScopeVar or RootPtrParameter.
+using AccessRoot = std::variant<RootModuleScopeVar, RootPtrParameter>;
+
+/// MemberAccess is an access operator to a struct member.
+struct MemberAccess {
+    /// The member being accessed
+    const type::StructMember* member;
+
+    /// @return a hash member for this object
+    uint64_t HashCode() const { return Hash(member); }
+
+    /// Inequality operator
+    bool operator!=(const MemberAccess& other) const { return member != other.member; }
+};
+
+/// IndexAccess is an access operator to an array element or matrix column.
+/// The ordered list of indices is passed by parameter.
+struct IndexAccess {
+    /// @return a hash value for this object
+    uint64_t HashCode() const { return 42; }
+
+    /// Inequality operator
+    bool operator!=(const IndexAccess&) const { return false; }
+};
+
+/// An access operation. Either a MemberAccess or IndexAccess.
+using AccessOp = std::variant<MemberAccess, IndexAccess>;
+
+/// 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.
+///
+/// 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.
+///
+/// For example, consider the following:
+///
+/// ```
+/// struct A {
+///     x : array<i32, 8>,
+///     y : u32,
+/// };
+/// struct B {
+///     x : i32,
+///     y : array<A, 4>
+/// };
+/// var<workgroup> C : B;
+/// ```
+///
+/// The following AccessShape would describe the following:
+///
+/// +====================================+===============+=================================+
+/// | AccessShape                        | Type          |  Expression                     |
+/// +====================================+===============+=================================+
+/// | [ Var 'C', MemberAccess 'x' ]      | i32           |  C.x                            |
+/// +------------------------------------+---------------+---------------------------------+
+/// | [ Var 'C', MemberAccess 'y' ]      | array<A, 4>   |  C.y                            |
+/// +------------------------------------+---------------+---------------------------------+
+/// | [ Var 'C', MemberAccess 'y',       | A             |  C.y[indices[0]]                |
+/// |   IndexAccess ]                    |               |                                 |
+/// +------------------------------------+---------------+---------------------------------+
+/// | [ Var 'C', MemberAccess 'y',       | array<i32, 8> |  C.y[indices[0]].x              |
+/// |   IndexAccess, MemberAccess 'x' ]  |               |                                 |
+/// +------------------------------------+---------------+---------------------------------+
+/// | [ Var 'C', MemberAccess 'y',       | i32           |  C.y[indices[0]].x[indices[1]]  |
+/// |   IndexAccess, MemberAccess 'x',   |               |                                 |
+/// |   IndexAccess ]                    |               |                                 |
+/// +------------------------------------+---------------+---------------------------------+
+/// | [ Var 'C', MemberAccess 'y',       | u32           |  C.y[indices[0]].y              |
+/// |   IndexAccess, MemberAccess 'y' ]  |               |                                 |
+/// +------------------------------------+---------------+---------------------------------+
+///
+/// Where: `indices` is the AccessChain::indices.
+struct AccessShape {
+    /// The access root.
+    AccessRoot root;
+    /// The access operations.
+    Vector<AccessOp, 8> ops;
+
+    /// @returns the number of IndexAccess operations in #ops.
+    uint32_t NumIndexAccesses() const {
+        uint32_t count = 0;
+        for (auto& op : ops) {
+            if (std::holds_alternative<IndexAccess>(op)) {
+                count++;
+            }
+        }
+        return count;
+    }
+
+    /// @return a hash value for this object
+    uint64_t HashCode() const { return Hash(root, ops); }
+
+    /// Inequality operator
+    bool operator!=(const AccessShape& other) const {
+        return root != other.root || ops != other.ops;
+    }
+};
+
+/// AccessChain describes a chain of access expressions originating from a variable.
+struct AccessChain {
+    /// The shape of the access chain
+    AccessShape shape;
+    /// The originating pointer
+    Value* root_ptr = nullptr;
+    /// The array of dynamic indices
+    Vector<Value*, 8> indices;
+};
+
+/// A variant signature describes the access shape of all the function's pointer parameters.
+/// This is a map of pointer parameter index to access shape.
+using VariantSignature = Hashmap<size_t, AccessShape, 4>;
+
+/// FnInfo describes a function that has pointer parameters which need replacing.
+/// This function will be replaced by zero, one or many variants. Each variant will have a unique
+/// access shape for the function's the pointer arguments.
+struct FnInfo {
+    /// A map of variant signature to the variant's unique IR function.
+    Hashmap<VariantSignature, Function*, 4> variants_by_sig;
+    /// The order to emit the variants in the final module.
+    Vector<Function*, 4> ordered_variants;
+};
+
+/// FnVariant describes a unique variant of a function that has pointer parameters that need
+/// replacing.
+struct FnVariant {
+    /// The signature of the variant.
+    VariantSignature signature;
+    /// The IR function for this variant.
+    Function* fn = nullptr;
+    /// The function information of the original function that this variant is based off.
+    FnInfo* info = nullptr;
+};
+
+/// PIMPL state for the transform.
+struct State {
+    /// The IR module.
+    Module& ir;
+
+    /// The transform options
+    const DirectVariableAccessOptions& options;
+
+    /// The IR builder.
+    Builder b{ir};
+
+    /// The type manager.
+    core::type::Manager& ty{ir.Types()};
+
+    /// The symbol table.
+    SymbolTable& sym{ir.symbols};
+
+    /// The functions that have pointer parameters that need transforming.
+    /// These functions will be replaced with zero, one or many forked variants.
+    Hashmap<ir::Function*, FnInfo*, 8> need_forking{};
+
+    /// Queue of variants that need building
+    Vector<FnVariant, 8> variants_to_build{};
+
+    /// Allocator for FnInfo
+    BlockAllocator<FnInfo> fn_info_allocator{};
+
+    /// Process the module.
+    void Process() {
+        // Make a copy of all the functions in the IR module.
+        auto input_fns = ir.functions;
+
+        // Populate #need_forking
+        GatherFnsThatNeedForking();
+
+        // Transform the functions that make calls to #need_forking, which aren't in #need_forking
+        // themselves.
+        BuildRootFns();
+
+        // Build variants of the functions in #need_forking.
+        BuildFnVariants();
+
+        // Rebuild ir.functions.
+        EmitFunctions(input_fns);
+    }
+
+    /// Populates #need_forking with all the functions that have pointer parameters which need
+    /// transforming. These functions will be replaced with variants based on the access shapes.
+    void GatherFnsThatNeedForking() {
+        for (auto* fn : ir.functions) {
+            for (auto* param : fn->Params()) {
+                if (ParamNeedsTransforming(param)) {
+                    need_forking.Add(fn, fn_info_allocator.Create());
+                    break;
+                }
+            }
+        }
+    }
+
+    /// Adjusts the calls of all the functions that make calls to #need_forking, which aren't in
+    /// #need_forking themselves. This populates #variants_to_build with the called functions.
+    void BuildRootFns() {
+        for (auto* fn : ir.functions) {
+            if (!need_forking.Contains(fn)) {
+                TransformCalls(fn);
+            }
+        }
+    }
+
+    /// Applies the necessary transformations to all the pointer arguments of calls made to
+    /// functions in #need_forking. Also populates #variants_to_build with the variants of the
+    /// callee functions.
+    /// @param fn the function to transform
+    void TransformCalls(Function* fn) {
+        // For all the function calls in the function...
+        Traverse(fn->Block(), [&](UserCall* call) {
+            auto* target = call->Target();
+            auto target_info = need_forking.Get(target);
+            if (!target_info) {
+                // Not a call to a function in #need_forking. Leave alone.
+                return;
+            }
+
+            // Found a call to a function in #need_forking.
+            // This call needs transforming to call the generated variant.
+
+            // New arguments to the call. This includes transformed and untransformed arguments.
+            Vector<Value*, 8> new_args;
+
+            // Pointer arguments that are being replaced.
+            Vector<Value*, 8> replaced_args;
+
+            // Signature of the callee variant
+            VariantSignature signature;
+
+            // For each argument / parameter...
+            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)) {
+                    // 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
+                    //   parameter or a function-scoped variable, and the access chain has no
+                    //   indicies.
+                    // * A single indices array argument: The root is a module-scope var and the
+                    //   access chain has indices.
+                    // * Both a pointer argument and indices array argument: The root is a pointer
+                    //   parameter or a function-scoped variable and the access chain has indices.
+                    b.InsertBefore(call, [&] {
+                        // Get the access chain for the pointer argument.
+                        auto chain = AccessChainFor(arg);
+                        // If the root is not a module-scope variable, then pass this root pointer
+                        // as an argument.
+                        if (std::holds_alternative<RootPtrParameter>(chain.shape.root)) {
+                            new_args.Push(chain.root_ptr);
+                        }
+                        // If the chain access contains indices, then pass these as an array of u32.
+                        if (size_t array_len = chain.indices.Length(); array_len > 0) {
+                            auto* array = ty.array(ty.u32(), static_cast<uint32_t>(array_len));
+                            auto* indices = b.Construct(array, std::move(chain.indices));
+                            new_args.Push(indices->Result());
+                        }
+                        // Record the parameter shape for the variant's signature.
+                        signature.Add(i, chain.shape);
+                    });
+                    // Record that this pointer argument has been replaced.
+                    replaced_args.Push(arg);
+                } else {
+                    // Argument does not need transformation.
+                    // Push the existing argument to new_args.
+                    new_args.Push(arg);
+                }
+            }
+
+            // Replace the call's arguments with new_args.
+            call->SetArgs(std::move(new_args));
+
+            // Clean up instructions that provided the now unused argument values.
+            for (auto* old_arg : replaced_args) {
+                DeleteDeadInstructions(old_arg);
+            }
+
+            // Look to see if this callee signature already has a variant created.
+            auto* new_target = (*target_info)->variants_by_sig.GetOrCreate(signature, [&] {
+                // New signature.
+
+                // Clone the original function to seed the new variant.
+                auto* variant_fn = CloneContext{ir}.Clone(target);
+                (*target_info)->ordered_variants.Push(variant_fn);
+
+                // Build a unique name for the variant.
+                if (auto fn_name = ir.NameOf(variant_fn); fn_name.IsValid()) {
+                    StringStream variant_name;
+                    variant_name << fn_name.NameView();
+                    auto params = signature.Keys().Sort();
+                    for (auto param_idx : params) {
+                        variant_name << "_" << AccessShapeName(*signature.Get(param_idx));
+                    }
+                    ir.SetName(variant_fn, variant_name.str());
+                }
+
+                // Create an entry for the variant, and add it to the queue of variants that need to
+                // be built. We don't do this here to avoid unbounded stack usage.
+                variants_to_build.Push(FnVariant{/* signature */ signature,
+                                                 /* fn */ variant_fn,
+                                                 /* info */ *target_info});
+                return variant_fn;
+            });
+
+            // Repoint the target of the call to the variant.
+            call->SetTarget(new_target);
+        });
+    }
+
+    /// Builds all the variants in #variants_to_build by:
+    /// * Replacing the pointer parameters with zero, one or two parameters (root pointer, indices).
+    /// * Transforming any calls made by that variant to other functions found in #need_forking.
+    /// Note: The transformation of calls can add more variants to #variants_to_build.
+    /// BuildFnVariants() will continue to build variants until #variants_to_build is empty.
+    void BuildFnVariants() {
+        while (!variants_to_build.IsEmpty()) {
+            auto variant = variants_to_build.Pop();
+            BuildFnVariantParams(variant);
+            TransformCalls(variant.fn);
+        }
+    }
+
+    /// 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
+    /// @return an AccessChain
+    AccessChain AccessChainFor(Value* value) {
+        AccessChain chain;
+        while (value) {
+            TINT_ASSERT(value->Alive());
+            value = tint::Switch(
+                value,  //
+                [&](InstructionResult* res) {
+                    // value was emitted by an instruction
+                    auto* inst = res->Source();
+                    return tint::Switch(
+                        inst,
+                        [&](Access* access) {
+                            // The AccessOp of this access instruction
+                            Vector<AccessOp, 8> ops;
+                            // The ordered, non-member accesses performed by this access instruction
+                            Vector<Value*, 8> indices;
+                            // The pointee-type that each access is being performed on
+                            auto* obj_ty = access->Object()->Type()->UnwrapPtr();
+
+                            // For each access operation...
+                            for (auto idx : access->Indices()) {
+                                if (auto* str = obj_ty->As<type::Struct>()) {
+                                    // Struct type accesses must be constant, representing the index
+                                    // of the member being accessed.
+                                    TINT_ASSERT(idx->Is<Constant>());
+                                    auto i = idx->As<Constant>()->Value()->ValueAs<uint32_t>();
+                                    auto* member = str->Members()[i];
+                                    ops.Push(MemberAccess{member});
+                                    obj_ty = member->Type();
+                                    continue;
+                                }
+
+                                // Array or matrix access.
+                                // Convert index to u32 if it isn't already.
+                                if (!idx->Type()->Is<type::U32>()) {
+                                    idx = b.Convert(ty.u32(), idx)->Result();
+                                }
+
+                                ops.Push(IndexAccess{});
+                                indices.Push(idx);
+                                obj_ty = obj_ty->Elements().type;
+                            }
+
+                            // Push the ops and indices in reverse order to the chain. This is done
+                            // so we can continue to walk the IR values and push accesses (without
+                            // insertion) that bring us closer to the pointer root. These are
+                            // reversed again once the root variable is found.
+                            for (auto& op : Reverse(ops)) {
+                                chain.shape.ops.Push(op);
+                            }
+                            for (auto& idx : Reverse(indices)) {
+                                chain.indices.Push(idx);
+                            }
+
+                            TINT_ASSERT(obj_ty == access->Result()->Type()->UnwrapPtr());
+                            return access->Object();
+                        },
+                        [&](Var* var) {
+                            // A 'var' is a pointer root.
+                            if (var->Block() == ir.root_block) {
+                                // Root pointer is a module-scope 'var'
+                                chain.shape.root = RootModuleScopeVar{var};
+                            } else {
+                                // Root pointer is a function-scope 'var'
+                                chain.shape.root =
+                                    RootPtrParameter{var->Result()->Type()->As<type::Pointer>()};
+                            }
+                            chain.root_ptr = var->Result();
+                            return nullptr;
+                        },
+                        [&](Let* let) { return let->Value(); },
+                        [&](Default) {
+                            TINT_ICE() << "unhandled instruction type: "
+                                       << (inst ? inst->TypeInfo().name : "<null>");
+                            return nullptr;
+                        });
+                },
+                [&](FunctionParam* param) {
+                    // Root pointer is a parameter of the caller
+                    chain.shape.root = RootPtrParameter{param->Type()->As<type::Pointer>()};
+                    chain.root_ptr = param;
+                    return nullptr;
+                },
+                [&](Default) {
+                    TINT_ICE() << "unhandled value type: "
+                               << (value ? value->TypeInfo().name : "<null>");
+                    return nullptr;
+                });
+        }
+
+        // Reverse the chain's ops and indices. See above for why.
+        chain.shape.ops.Reverse();
+        chain.indices.Reverse();
+
+        return chain;
+    }
+
+    /// Replaces the pointer parameters that need transforming of the variant function @p variant.
+    /// Instructions are inserted at the top of the @p variant function block to reconstruct the
+    /// pointer parameters from the access chain using the root pointer and access ops.
+    /// @param variant the variant function to transform
+    void BuildFnVariantParams(const FnVariant& variant) {
+        // Insert new instructions at the top of the function block...
+        b.InsertBefore(variant.fn->Block()->Front(), [&] {
+            // The replacement parameters for the variant function
+            Vector<ir::FunctionParam*, 8> new_params;
+            const auto& old_params = variant.fn->Params();
+            // 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)) {
+                    // Parameter does not need transforming.
+                    new_params.Push(old_param);
+                    continue;
+                }
+
+                // Pointer parameter that needs transforming
+                // Grab the access shape of the pointer parameter from the signature
+                auto shape = variant.signature.Get(param_idx);
+                // The root pointer value
+                Value* root_ptr = nullptr;
+
+                // Build the root pointer parameter, if required.
+                FunctionParam* root_ptr_param = nullptr;
+                if (auto* ptr_param = std::get_if<RootPtrParameter>(&shape->root)) {
+                    // Root pointer is passed as a parameter
+                    root_ptr_param = b.FunctionParam(ptr_param->type);
+                    new_params.Push(root_ptr_param);
+                    root_ptr = root_ptr_param;
+                } else if (auto* global = std::get_if<RootModuleScopeVar>(&shape->root)) {
+                    // Root pointer is a module-scope var
+                    root_ptr = global->var->Result();
+                } else {
+                    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);
+                }
+
+                // 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();
+                });
+                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());
+                old_param->Destroy();
+            }
+
+            // Replace the function's parameters
+            variant.fn->SetParams(std::move(new_params));
+        });
+    }
+
+    /// Repopulates #ir.functions with the functions in #need_forking replaced with their generated
+    /// variants.
+    /// @param input_fns the content of #ir.functions before transformation began.
+    void EmitFunctions(VectorRef<Function*> input_fns) {
+        ir.functions.Clear();
+        for (auto* fn : input_fns) {
+            if (auto info = need_forking.Get(fn)) {
+                for (auto variant : (*info)->ordered_variants) {
+                    ir.functions.Push(variant);
+                }
+            } else {
+                ir.functions.Push(fn);
+            }
+        }
+    }
+
+    /// @returns a string describing the given AccessShape, used to suffix the generated function
+    /// variants.
+    std::string AccessShapeName(const AccessShape& shape) {
+        StringStream ss;
+
+        if (auto* global = std::get_if<RootModuleScopeVar>(&shape.root)) {
+            ss << ir.NameOf(global->var).NameView();
+        } else {
+            ss << "P";
+        }
+
+        for (auto& op : shape.ops) {
+            ss << "_";
+
+            if (std::holds_alternative<IndexAccess>(op)) {
+                /// The op uses an index taken from an array parameter.
+                ss << "X";
+                continue;
+            }
+
+            if (auto* access = std::get_if<MemberAccess>(&op); TINT_LIKELY(access)) {
+                ss << access->member->Name().NameView();
+                continue;
+            }
+
+            TINT_ICE() << "unhandled variant for access chain";
+            break;
+        }
+        return ss.str();
+    }
+
+    /// @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
+    bool ParamNeedsTransforming(FunctionParam* param) const {
+        if (auto* ptr = param->Type()->As<type::Pointer>()) {
+            switch (ptr->AddressSpace()) {
+                case core::AddressSpace::kStorage:
+                case core::AddressSpace::kUniform:
+                case core::AddressSpace::kWorkgroup:
+                    return true;
+                case core::AddressSpace::kFunction:
+                    return options.transform_function;
+                case core::AddressSpace::kPrivate:
+                    return options.transform_private;
+                default:
+                    break;
+            }
+        }
+        return false;
+    }
+
+    /// Walks the instructions that built @p value, deleting those that are no longer used.
+    /// @param value the pointer value that was used as a now replaced pointer argument.
+    void DeleteDeadInstructions(ir::Value* value) {
+        // While value has no uses...
+        while (value && value->Usages().Count() == 0) {
+            auto* inst_res = value->As<InstructionResult>();
+            if (!inst_res) {
+                return;  // Only instructions can be removed.
+            }
+            value = tint::Switch(
+                inst_res->Source(),  //
+                [&](Access* access) {
+                    TINT_DEFER(access->Destroy());
+                    return access->Object();
+                },
+                [&](Let* let) {
+                    TINT_DEFER(let->Destroy());
+                    return let->Value();
+                });
+        }
+    }
+};
+
+}  // namespace
+
+Result<SuccessType> DirectVariableAccess(Module& ir, const DirectVariableAccessOptions& options) {
+    auto result = ValidateAndDumpIfNeeded(ir, "DirectVariableAccess transform");
+    if (!result) {
+        return result;
+    }
+
+    State{ir, options}.Process();
+
+    return Success;
+}
+
+}  // namespace tint::core::ir::transform
diff --git a/src/tint/lang/core/ir/transform/direct_variable_access.h b/src/tint/lang/core/ir/transform/direct_variable_access.h
new file mode 100644
index 0000000..5f6917f
--- /dev/null
+++ b/src/tint/lang/core/ir/transform/direct_variable_access.h
@@ -0,0 +1,53 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_LANG_CORE_IR_TRANSFORM_DIRECT_VARIABLE_ACCESS_H_
+#define SRC_TINT_LANG_CORE_IR_TRANSFORM_DIRECT_VARIABLE_ACCESS_H_
+
+#include "src/tint/utils/result/result.h"
+
+// Forward declarations.
+namespace tint::core::ir {
+class Module;
+}
+
+namespace tint::core::ir::transform {
+
+/// DirectVariableAccessOptions adjusts the behaviour of the transform.
+struct DirectVariableAccessOptions {
+    /// If true, then 'private' sub-object pointer arguments will be transformed.
+    bool transform_private = false;
+    /// If true, then 'function' sub-object pointer arguments will be transformed.
+    bool transform_function = false;
+};
+
+/// DirectVariableAccess is a transform that transforms pointer parameters in the 'storage',
+/// 'uniform' and 'workgroup' address space so that they're accessed directly by the function,
+/// instead of being passed by pointer.
+///
+/// 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.
+///
+/// @param module the module to transform
+/// @returns error diagnostics on failure
+Result<SuccessType> DirectVariableAccess(Module& module,
+                                         const DirectVariableAccessOptions& options);
+
+}  // namespace tint::core::ir::transform
+
+#endif  // SRC_TINT_LANG_CORE_IR_TRANSFORM_DIRECT_VARIABLE_ACCESS_H_
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
new file mode 100644
index 0000000..5bccb80
--- /dev/null
+++ b/src/tint/lang/core/ir/transform/direct_variable_access_test.cc
@@ -0,0 +1,4823 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/lang/core/ir/transform/direct_variable_access.h"
+
+#include <utility>
+
+#include "src/tint/lang/core/ir/transform/helper_test.h"
+#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/struct.h"
+
+#include "src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.h"
+#include "src/tint/lang/wgsl/reader/reader.h"
+#include "src/tint/lang/wgsl/writer/ir_to_program/ir_to_program.h"
+#include "src/tint/lang/wgsl/writer/writer.h"
+
+namespace tint::core::ir::transform {
+namespace {
+
+using namespace tint::core::fluent_types;     // NOLINT
+using namespace tint::core::number_suffixes;  // NOLINT
+
+namespace {
+
+static constexpr DirectVariableAccessOptions kTransformPrivate = {
+    /* transform_private */ true,
+    /* transform_function */ false,
+};
+
+static constexpr DirectVariableAccessOptions kTransformFunction = {
+    /* transform_private */ false,
+    /* transform_function */ true,
+};
+
+}  // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+// remove uncalled
+////////////////////////////////////////////////////////////////////////////////
+namespace remove_uncalled {
+
+using IR_DirectVariableAccessTest_RemoveUncalled = TransformTest;
+
+TEST_F(IR_DirectVariableAccessTest_RemoveUncalled, PtrUniform) {
+    b.Append(b.ir.root_block, [&] { b.Var<private_>("keep_me", 42_i); });
+
+    auto* u = b.Function("u", ty.i32());
+    auto* p = b.FunctionParam("p", ty.ptr<uniform, i32, read>());
+    u->SetParams({
+        b.FunctionParam("pre", ty.i32()),
+        p,
+        b.FunctionParam("post", ty.i32()),
+    });
+    b.Append(u->Block(), [&] { b.Return(u, b.Load(p)); });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %keep_me:ptr<private, i32, read_write> = var, 42i
+}
+
+%u = func(%pre:i32, %p:ptr<uniform, i32, read>, %post:i32):i32 -> %b2 {
+  %b2 = block {
+    %6:i32 = load %p
+    ret %6
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %keep_me:ptr<private, i32, read_write> = var, 42i
+}
+
+)";
+
+    Run(DirectVariableAccess, DirectVariableAccessOptions{});
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_RemoveUncalled, PtrStorage) {
+    b.Append(b.ir.root_block, [&] { b.Var<private_>("keep_me", 42_i); });
+
+    auto* s = b.Function("s", ty.i32());
+    auto* p = b.FunctionParam("p", ty.ptr<storage, i32, read>());
+    s->SetParams({
+        b.FunctionParam("pre", ty.i32()),
+        p,
+        b.FunctionParam("post", ty.i32()),
+    });
+    b.Append(s->Block(), [&] { b.Return(s, b.Load(p)); });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %keep_me:ptr<private, i32, read_write> = var, 42i
+}
+
+%s = func(%pre:i32, %p:ptr<storage, i32, read>, %post:i32):i32 -> %b2 {
+  %b2 = block {
+    %6:i32 = load %p
+    ret %6
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %keep_me:ptr<private, i32, read_write> = var, 42i
+}
+
+)";
+
+    Run(DirectVariableAccess, DirectVariableAccessOptions{});
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_RemoveUncalled, PtrWorkgroup) {
+    b.Append(b.ir.root_block, [&] { b.Var<private_>("keep_me", 42_i); });
+
+    auto* w = b.Function("w", ty.i32());
+    auto* p = b.FunctionParam("p", ty.ptr<workgroup, i32>());
+    w->SetParams({
+        b.FunctionParam("pre", ty.i32()),
+        p,
+        b.FunctionParam("post", ty.i32()),
+    });
+    b.Append(w->Block(), [&] { b.Return(w, b.Load(p)); });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %keep_me:ptr<private, i32, read_write> = var, 42i
+}
+
+%w = func(%pre:i32, %p:ptr<workgroup, i32, read_write>, %post:i32):i32 -> %b2 {
+  %b2 = block {
+    %6:i32 = load %p
+    ret %6
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %keep_me:ptr<private, i32, read_write> = var, 42i
+}
+
+)";
+
+    Run(DirectVariableAccess, DirectVariableAccessOptions{});
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_RemoveUncalled, PtrPrivate_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.ptr<private_, i32>());
+    f->SetParams({
+        b.FunctionParam("pre", ty.i32()),
+        p,
+        b.FunctionParam("post", ty.i32()),
+    });
+    b.Append(f->Block(), [&] { b.Return(f, b.Load(p)); });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %keep_me:ptr<private, i32, read_write> = var, 42i
+}
+
+%f = func(%pre:i32, %p:ptr<private, i32, read_write>, %post:i32):i32 -> %b2 {
+  %b2 = block {
+    %6:i32 = load %p
+    ret %6
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(DirectVariableAccess, DirectVariableAccessOptions{});
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_RemoveUncalled, PtrPrivate_Enabled) {
+    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.ptr<private_, i32>());
+    f->SetParams({
+        b.FunctionParam("pre", ty.i32()),
+        p,
+        b.FunctionParam("post", ty.i32()),
+    });
+    b.Append(f->Block(), [&] { b.Return(f, b.Load(p)); });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %keep_me:ptr<private, i32, read_write> = var, 42i
+}
+
+%f = func(%pre:i32, %p:ptr<private, i32, read_write>, %post:i32):i32 -> %b2 {
+  %b2 = block {
+    %6:i32 = load %p
+    ret %6
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %keep_me:ptr<private, i32, read_write> = var, 42i
+}
+
+)";
+    Run(DirectVariableAccess, kTransformPrivate);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_RemoveUncalled, PtrFunction_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.ptr<function, i32>());
+    f->SetParams({
+        b.FunctionParam("pre", ty.i32()),
+        p,
+        b.FunctionParam("post", ty.i32()),
+    });
+    b.Append(f->Block(), [&] { b.Return(f, b.Load(p)); });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %keep_me:ptr<private, i32, read_write> = var, 42i
+}
+
+%f = func(%pre:i32, %p:ptr<function, i32, read_write>, %post:i32):i32 -> %b2 {
+  %b2 = block {
+    %6:i32 = load %p
+    ret %6
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(DirectVariableAccess, DirectVariableAccessOptions{});
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_RemoveUncalled, PtrFunction_Enabled) {
+    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.ptr<function, i32>());
+    f->SetParams({
+        b.FunctionParam("pre", ty.i32()),
+        p,
+        b.FunctionParam("post", ty.i32()),
+    });
+    b.Append(f->Block(), [&] { b.Return(f, b.Load(p)); });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %keep_me:ptr<private, i32, read_write> = var, 42i
+}
+
+%f = func(%pre:i32, %p:ptr<function, i32, read_write>, %post:i32):i32 -> %b2 {
+  %b2 = block {
+    %6:i32 = load %p
+    ret %6
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %keep_me:ptr<private, i32, read_write> = var, 42i
+}
+
+)";
+
+    Run(DirectVariableAccess, kTransformFunction);
+
+    EXPECT_EQ(expect, str());
+}
+
+}  // namespace remove_uncalled
+
+////////////////////////////////////////////////////////////////////////////////
+// pointer chains
+////////////////////////////////////////////////////////////////////////////////
+namespace pointer_chains_tests {
+
+using IR_DirectVariableAccessTest_PtrChains = TransformTest;
+
+TEST_F(IR_DirectVariableAccessTest_PtrChains, ConstantIndices) {
+    Var* U = nullptr;
+    b.Append(b.ir.root_block,
+             [&] {  //
+                 U = b.Var<uniform, array<array<array<vec4<i32>, 8>, 8>, 8>, read>("U");
+                 U->SetBindingPoint(0, 0);
+             });
+
+    auto* fn_a = b.Function("a", ty.vec4<i32>());
+    auto* fn_a_p = b.FunctionParam("p", ty.ptr<uniform, vec4<i32>, read>());
+    fn_a->SetParams({
+        b.FunctionParam("pre", ty.i32()),
+        fn_a_p,
+        b.FunctionParam("post", ty.i32()),
+    });
+    b.Append(fn_a->Block(), [&] { b.Return(fn_a, b.Load(fn_a_p)); });
+
+    auto* fn_b = b.Function("b", ty.void_());
+    b.Append(fn_b->Block(), [&] {
+        auto* p0 = b.Let("p0", U);
+        auto* p1 = b.Access(ty.ptr<uniform, array<array<vec4<i32>, 8>, 8>, read>(), p0, 1_i);
+        b.ir.SetName(p1, "p1");
+        auto* p2 = b.Access(ty.ptr<uniform, array<vec4<i32>, 8>, read>(), p1, 2_i);
+        b.ir.SetName(p2, "p2");
+        auto* p3 = b.Access(ty.ptr<uniform, vec4<i32>, read>(), p2, 3_i);
+        b.ir.SetName(p3, "p3");
+        b.Call(ty.vec4<i32>(), fn_a, 10_i, p3, 20_i);
+        b.Return(fn_b);
+    });
+
+    auto* fn_c = b.Function("c", ty.void_());
+    auto* fn_c_p =
+        b.FunctionParam("p", ty.ptr<uniform, array<array<array<vec4<i32>, 8>, 8>, 8>, read>());
+    fn_c->SetParams({fn_c_p});
+    b.Append(fn_c->Block(), [&] {
+        auto* p0 = b.Let("p0", fn_c_p);
+        auto* p1 = b.Access(ty.ptr<uniform, array<array<vec4<i32>, 8>, 8>, read>(), p0, 1_i);
+        b.ir.SetName(p1, "p1");
+        auto* p2 = b.Access(ty.ptr<uniform, array<vec4<i32>, 8>, read>(), p1, 2_i);
+        b.ir.SetName(p2, "p2");
+        auto* p3 = b.Access(ty.ptr<uniform, vec4<i32>, read>(), p2, 3_i);
+        b.ir.SetName(p3, "p3");
+        b.Call(ty.vec4<i32>(), fn_a, 10_i, p3, 20_i);
+        b.Return(fn_c);
+    });
+
+    auto* fn_d = b.Function("d", ty.void_());
+    b.Append(fn_d->Block(), [&] {
+        b.Call(ty.void_(), fn_c, U);
+        b.Return(fn_d);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %U:ptr<uniform, array<array<array<vec4<i32>, 8>, 8>, 8>, read> = var @binding_point(0, 0)
+}
+
+%a = func(%pre:i32, %p:ptr<uniform, vec4<i32>, read>, %post:i32):vec4<i32> -> %b2 {
+  %b2 = block {
+    %6:vec4<i32> = load %p
+    ret %6
+  }
+}
+%b = func():void -> %b3 {
+  %b3 = block {
+    %p0:ptr<uniform, array<array<array<vec4<i32>, 8>, 8>, 8>, read> = let %U
+    %p1:ptr<uniform, array<array<vec4<i32>, 8>, 8>, read> = access %p0, 1i
+    %p2:ptr<uniform, array<vec4<i32>, 8>, read> = access %p1, 2i
+    %p3:ptr<uniform, vec4<i32>, read> = access %p2, 3i
+    %12:vec4<i32> = call %a, 10i, %p3, 20i
+    ret
+  }
+}
+%c = func(%p_1:ptr<uniform, array<array<array<vec4<i32>, 8>, 8>, 8>, read>):void -> %b4 {  # %p_1: 'p'
+  %b4 = block {
+    %p0_1:ptr<uniform, array<array<array<vec4<i32>, 8>, 8>, 8>, read> = let %p_1  # %p0_1: 'p0'
+    %p1_1:ptr<uniform, array<array<vec4<i32>, 8>, 8>, read> = access %p0_1, 1i  # %p1_1: 'p1'
+    %p2_1:ptr<uniform, array<vec4<i32>, 8>, read> = access %p1_1, 2i  # %p2_1: 'p2'
+    %p3_1:ptr<uniform, vec4<i32>, read> = access %p2_1, 3i  # %p3_1: 'p3'
+    %19:vec4<i32> = call %a, 10i, %p3_1, 20i
+    ret
+  }
+}
+%d = func():void -> %b5 {
+  %b5 = block {
+    %21:void = call %c, %U
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect =
+        R"(
+%b1 = block {  # root
+  %U:ptr<uniform, array<array<array<vec4<i32>, 8>, 8>, 8>, read> = var @binding_point(0, 0)
+}
+
+%a_U_X_X_X = func(%pre:i32, %p_indices:array<u32, 3>, %post:i32):vec4<i32> -> %b2 {
+  %b2 = block {
+    %6:u32 = access %p_indices, 0u
+    %7:u32 = access %p_indices, 1u
+    %8:u32 = access %p_indices, 2u
+    %9:ptr<uniform, vec4<i32>, read> = access %U, %6, %7, %8
+    %10:vec4<i32> = load %9
+    ret %10
+  }
+}
+%b = func():void -> %b3 {
+  %b3 = block {
+    %12:u32 = convert 3i
+    %13:u32 = convert 2i
+    %14:u32 = convert 1i
+    %15:array<u32, 3> = construct %14, %13, %12
+    %16:vec4<i32> = call %a_U_X_X_X, 10i, %15, 20i
+    ret
+  }
+}
+%c_U = func():void -> %b4 {
+  %b4 = block {
+    %18:u32 = convert 3i
+    %19:u32 = convert 2i
+    %20:u32 = convert 1i
+    %21:array<u32, 3> = construct %20, %19, %18
+    %22:vec4<i32> = call %a_U_X_X_X, 10i, %21, 20i
+    ret
+  }
+}
+%d = func():void -> %b5 {
+  %b5 = block {
+    %24:void = call %c_U
+    ret
+  }
+}
+)";
+
+    Run(DirectVariableAccess, DirectVariableAccessOptions{});
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_PtrChains, DynamicIndices) {
+    Var* U = nullptr;
+    Var* i = nullptr;
+    b.Append(b.ir.root_block,
+             [&] {  //
+                 U = b.Var<uniform, array<array<array<vec4<i32>, 8>, 8>, 8>, read>("U");
+                 U->SetBindingPoint(0, 0);
+                 i = b.Var<private_, i32>("i");
+             });
+
+    auto* fn_first = b.Function("first", ty.i32());
+    auto* fn_second = b.Function("second", ty.i32());
+    auto* fn_third = b.Function("third", ty.i32());
+    for (auto fn : {fn_first, fn_second, fn_third}) {
+        b.Append(fn->Block(), [&] {
+            b.Store(i, b.Add(ty.i32(), b.Load(i), 1_i));
+            b.Return(fn, b.Load(i));
+        });
+    }
+
+    auto* fn_a = b.Function("a", ty.vec4<i32>());
+    auto* fn_a_p = b.FunctionParam("p", ty.ptr<uniform, vec4<i32>, read>());
+    fn_a->SetParams({
+        b.FunctionParam("pre", ty.i32()),
+        fn_a_p,
+        b.FunctionParam("post", ty.i32()),
+    });
+    b.Append(fn_a->Block(), [&] { b.Return(fn_a, b.Load(fn_a_p)); });
+
+    auto* fn_b = b.Function("b", ty.void_());
+    b.Append(fn_b->Block(), [&] {
+        auto* p0 = b.Let("p0", U);
+        auto* first = b.Call(fn_first);
+        auto* p1 = b.Access(ty.ptr<uniform, array<array<vec4<i32>, 8>, 8>, read>(), p0, first);
+        b.ir.SetName(p1, "p1");
+        auto* second = b.Call(fn_second);
+        auto* third = b.Call(fn_third);
+        auto* p2 = b.Access(ty.ptr<uniform, vec4<i32>, read>(), p1, second, third);
+        b.ir.SetName(p2, "p2");
+        b.Call(ty.vec4<i32>(), fn_a, 10_i, p2, 20_i);
+        b.Return(fn_b);
+    });
+
+    auto* fn_c = b.Function("c", ty.void_());
+    auto* fn_c_p =
+        b.FunctionParam("p", ty.ptr<uniform, array<array<array<vec4<i32>, 8>, 8>, 8>, read>());
+    fn_c->SetParams({fn_c_p});
+    b.Append(fn_c->Block(), [&] {
+        auto* p0 = b.Let("p0", fn_c_p);
+        auto* first = b.Call(fn_first);
+        auto* p1 = b.Access(ty.ptr<uniform, array<array<vec4<i32>, 8>, 8>, read>(), p0, first);
+        b.ir.SetName(p1, "p1");
+        auto* second = b.Call(fn_second);
+        auto* third = b.Call(fn_third);
+        auto* p2 = b.Access(ty.ptr<uniform, vec4<i32>, read>(), p1, second, third);
+        b.ir.SetName(p2, "p2");
+        b.Call(ty.vec4<i32>(), fn_a, 10_i, p2, 20_i);
+        b.Return(fn_c);
+    });
+
+    auto* fn_d = b.Function("d", ty.void_());
+    b.Append(fn_d->Block(), [&] {
+        b.Call(ty.void_(), fn_c, U);
+        b.Return(fn_d);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %U:ptr<uniform, array<array<array<vec4<i32>, 8>, 8>, 8>, read> = var @binding_point(0, 0)
+  %i:ptr<private, i32, read_write> = var
+}
+
+%first = func():i32 -> %b2 {
+  %b2 = block {
+    %4:i32 = load %i
+    %5:i32 = add %4, 1i
+    store %i, %5
+    %6:i32 = load %i
+    ret %6
+  }
+}
+%second = func():i32 -> %b3 {
+  %b3 = block {
+    %8:i32 = load %i
+    %9:i32 = add %8, 1i
+    store %i, %9
+    %10:i32 = load %i
+    ret %10
+  }
+}
+%third = func():i32 -> %b4 {
+  %b4 = block {
+    %12:i32 = load %i
+    %13:i32 = add %12, 1i
+    store %i, %13
+    %14:i32 = load %i
+    ret %14
+  }
+}
+%a = func(%pre:i32, %p:ptr<uniform, vec4<i32>, read>, %post:i32):vec4<i32> -> %b5 {
+  %b5 = block {
+    %19:vec4<i32> = load %p
+    ret %19
+  }
+}
+%b = func():void -> %b6 {
+  %b6 = block {
+    %p0:ptr<uniform, array<array<array<vec4<i32>, 8>, 8>, 8>, read> = let %U
+    %22:i32 = call %first
+    %p1:ptr<uniform, array<array<vec4<i32>, 8>, 8>, read> = access %p0, %22
+    %24:i32 = call %second
+    %25:i32 = call %third
+    %p2:ptr<uniform, vec4<i32>, read> = access %p1, %24, %25
+    %27:vec4<i32> = call %a, 10i, %p2, 20i
+    ret
+  }
+}
+%c = func(%p_1:ptr<uniform, array<array<array<vec4<i32>, 8>, 8>, 8>, read>):void -> %b7 {  # %p_1: 'p'
+  %b7 = block {
+    %p0_1:ptr<uniform, array<array<array<vec4<i32>, 8>, 8>, 8>, read> = let %p_1  # %p0_1: 'p0'
+    %31:i32 = call %first
+    %p1_1:ptr<uniform, array<array<vec4<i32>, 8>, 8>, read> = access %p0_1, %31  # %p1_1: 'p1'
+    %33:i32 = call %second
+    %34:i32 = call %third
+    %p2_1:ptr<uniform, vec4<i32>, read> = access %p1_1, %33, %34  # %p2_1: 'p2'
+    %36:vec4<i32> = call %a, 10i, %p2_1, 20i
+    ret
+  }
+}
+%d = func():void -> %b8 {
+  %b8 = block {
+    %38:void = call %c, %U
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %U:ptr<uniform, array<array<array<vec4<i32>, 8>, 8>, 8>, read> = var @binding_point(0, 0)
+  %i:ptr<private, i32, read_write> = var
+}
+
+%first = func():i32 -> %b2 {
+  %b2 = block {
+    %4:i32 = load %i
+    %5:i32 = add %4, 1i
+    store %i, %5
+    %6:i32 = load %i
+    ret %6
+  }
+}
+%second = func():i32 -> %b3 {
+  %b3 = block {
+    %8:i32 = load %i
+    %9:i32 = add %8, 1i
+    store %i, %9
+    %10:i32 = load %i
+    ret %10
+  }
+}
+%third = func():i32 -> %b4 {
+  %b4 = block {
+    %12:i32 = load %i
+    %13:i32 = add %12, 1i
+    store %i, %13
+    %14:i32 = load %i
+    ret %14
+  }
+}
+%a_U_X_X_X = func(%pre:i32, %p_indices:array<u32, 3>, %post:i32):vec4<i32> -> %b5 {
+  %b5 = block {
+    %19:u32 = access %p_indices, 0u
+    %20:u32 = access %p_indices, 1u
+    %21:u32 = access %p_indices, 2u
+    %22:ptr<uniform, vec4<i32>, read> = access %U, %19, %20, %21
+    %23:vec4<i32> = load %22
+    ret %23
+  }
+}
+%b = func():void -> %b6 {
+  %b6 = block {
+    %25:i32 = call %first
+    %26:i32 = call %second
+    %27:i32 = call %third
+    %28:u32 = convert %26
+    %29:u32 = convert %27
+    %30:u32 = convert %25
+    %31:array<u32, 3> = construct %30, %28, %29
+    %32:vec4<i32> = call %a_U_X_X_X, 10i, %31, 20i
+    ret
+  }
+}
+%c_U = func():void -> %b7 {
+  %b7 = block {
+    %34:i32 = call %first
+    %35:i32 = call %second
+    %36:i32 = call %third
+    %37:u32 = convert %35
+    %38:u32 = convert %36
+    %39:u32 = convert %34
+    %40:array<u32, 3> = construct %39, %37, %38
+    %41:vec4<i32> = call %a_U_X_X_X, 10i, %40, 20i
+    ret
+  }
+}
+%d = func():void -> %b8 {
+  %b8 = block {
+    %43:void = call %c_U
+    ret
+  }
+}
+)";
+
+    Run(DirectVariableAccess, DirectVariableAccessOptions{});
+
+    EXPECT_EQ(expect, str());
+}
+
+}  // namespace pointer_chains_tests
+
+////////////////////////////////////////////////////////////////////////////////
+// 'uniform' address space
+////////////////////////////////////////////////////////////////////////////////
+namespace uniform_as_tests {
+
+using IR_DirectVariableAccessTest_UniformAS = TransformTest;
+
+TEST_F(IR_DirectVariableAccessTest_UniformAS, Param_ptr_i32_read) {
+    Var* U = nullptr;
+    b.Append(b.ir.root_block,
+             [&] {  //
+                 U = b.Var<uniform, i32, read>("U");
+                 U->SetBindingPoint(0, 0);
+             });
+
+    auto* fn_a = b.Function("a", ty.i32());
+    auto* fn_a_p = b.FunctionParam("p", ty.ptr<uniform, i32, read>());
+    fn_a->SetParams({
+        b.FunctionParam("pre", ty.i32()),
+        fn_a_p,
+        b.FunctionParam("post", ty.i32()),
+    });
+    b.Append(fn_a->Block(), [&] { b.Return(fn_a, b.Load(fn_a_p)); });
+
+    auto* fn_b = b.Function("b", ty.void_());
+    b.Append(fn_b->Block(), [&] {
+        b.Call(fn_a, 10_i, U, 20_i);
+        b.Return(fn_b);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %U:ptr<uniform, i32, read> = var @binding_point(0, 0)
+}
+
+%a = func(%pre:i32, %p:ptr<uniform, i32, read>, %post:i32):i32 -> %b2 {
+  %b2 = block {
+    %6:i32 = load %p
+    ret %6
+  }
+}
+%b = func():void -> %b3 {
+  %b3 = block {
+    %8:i32 = call %a, 10i, %U, 20i
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %U:ptr<uniform, i32, read> = var @binding_point(0, 0)
+}
+
+%a_U = func(%pre:i32, %post:i32):i32 -> %b2 {
+  %b2 = block {
+    %5:ptr<uniform, i32, read> = access %U
+    %6:i32 = load %5
+    ret %6
+  }
+}
+%b = func():void -> %b3 {
+  %b3 = block {
+    %8:i32 = call %a_U, 10i, 20i
+    ret
+  }
+}
+)";
+
+    Run(DirectVariableAccess, DirectVariableAccessOptions{});
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_UniformAS, Param_ptr_vec4i32_Via_array_DynamicRead) {
+    Var* U = nullptr;
+    b.Append(b.ir.root_block,
+             [&] {  //
+                 U = b.Var<uniform, array<vec4<i32>, 8>, read>("U");
+                 U->SetBindingPoint(0, 0);
+             });
+
+    auto* fn_a = b.Function("a", ty.vec4<i32>());
+    auto* fn_a_p = b.FunctionParam("p", ty.ptr<uniform, vec4<i32>, read>());
+    fn_a->SetParams({
+        b.FunctionParam("pre", ty.i32()),
+        fn_a_p,
+        b.FunctionParam("post", ty.i32()),
+    });
+    b.Append(fn_a->Block(), [&] { b.Return(fn_a, b.Load(fn_a_p)); });
+
+    auto* fn_b = b.Function("b", ty.void_());
+    b.Append(fn_b->Block(), [&] {
+        auto* I = b.Let("I", 3_i);
+        auto* access = b.Access(ty.ptr<uniform, vec4<i32>, read>(), U, I);
+        b.Call(fn_a, 10_i, access, 20_i);
+        b.Return(fn_b);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %U:ptr<uniform, array<vec4<i32>, 8>, read> = var @binding_point(0, 0)
+}
+
+%a = func(%pre:i32, %p:ptr<uniform, vec4<i32>, read>, %post:i32):vec4<i32> -> %b2 {
+  %b2 = block {
+    %6:vec4<i32> = load %p
+    ret %6
+  }
+}
+%b = func():void -> %b3 {
+  %b3 = block {
+    %I:i32 = let 3i
+    %9:ptr<uniform, vec4<i32>, read> = access %U, %I
+    %10:vec4<i32> = call %a, 10i, %9, 20i
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %U:ptr<uniform, array<vec4<i32>, 8>, read> = var @binding_point(0, 0)
+}
+
+%a_U_X = func(%pre:i32, %p_indices:array<u32, 1>, %post:i32):vec4<i32> -> %b2 {
+  %b2 = block {
+    %6:u32 = access %p_indices, 0u
+    %7:ptr<uniform, vec4<i32>, read> = access %U, %6
+    %8:vec4<i32> = load %7
+    ret %8
+  }
+}
+%b = func():void -> %b3 {
+  %b3 = block {
+    %I:i32 = let 3i
+    %11:u32 = convert %I
+    %12:array<u32, 1> = construct %11
+    %13:vec4<i32> = call %a_U_X, 10i, %12, 20i
+    ret
+  }
+}
+)";
+
+    Run(DirectVariableAccess, DirectVariableAccessOptions{});
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_UniformAS, 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>()},
+                                            });
+    Var* U = nullptr;
+    b.Append(b.ir.root_block,
+             [&] {  //
+                 U = b.Var("U", ty.ptr<uniform, read>(Outer));
+                 U->SetBindingPoint(0, 0);
+             });
+
+    auto* fn_0 = b.Function("f0", ty.f32());
+    auto* fn_0_p = b.FunctionParam("p", ty.ptr<uniform, vec4<f32>, read>());
+    fn_0->SetParams({fn_0_p});
+    b.Append(fn_0->Block(), [&] { b.Return(fn_0, b.LoadVectorElement(fn_0_p, 0_u)); });
+
+    auto* fn_1 = b.Function("f1", ty.f32());
+    auto* fn_1_p = b.FunctionParam("p", ty.ptr<uniform, mat3x4<f32>, read>());
+    fn_1->SetParams({fn_1_p});
+    b.Append(fn_1->Block(), [&] {
+        auto* res = b.Var<function, f32>("res");
+        {
+            // res += f0(&(*p)[1]);
+            auto* call_0 = b.Call(fn_0, b.Access(ty.ptr<uniform, vec4<f32>, read>(), fn_1_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<uniform, vec4<f32>, read>(), fn_1_p, 1_i);
+            b.ir.SetName(p_vec, "p_vec");
+            auto* call_0 = b.Call(fn_0, p_vec);
+            b.Store(res, b.Add(ty.f32(), b.Load(res), call_0));
+        }
+        {
+            // res += f0(&U.arr[2].mat[1]);
+            auto* access = b.Access(ty.ptr<uniform, vec4<f32>, read>(), U, 0_u, 2_i, 0_u, 1_i);
+            auto* call_0 = b.Call(fn_0, access);
+            b.Store(res, b.Add(ty.f32(), b.Load(res), call_0));
+        }
+        {
+            // let p_vec = &U.arr[2].mat[1];
+            // res += f0(p_vec);
+            auto* p_vec = b.Access(ty.ptr<uniform, vec4<f32>, read>(), U, 0_u, 2_i, 0_u, 1_i);
+            b.ir.SetName(p_vec, "p_vec");
+            auto* call_0 = b.Call(fn_0, p_vec);
+            b.Store(res, b.Add(ty.f32(), b.Load(res), call_0));
+        }
+
+        b.Return(fn_1, b.Load(res));
+    });
+
+    auto* fn_2 = b.Function("f2", ty.f32());
+    auto* fn_2_p = b.FunctionParam("p", ty.ptr<uniform, read>(Inner));
+    fn_2->SetParams({fn_2_p});
+    b.Append(fn_2->Block(), [&] {
+        auto* p_mat = b.Access(ty.ptr<uniform, mat3x4<f32>, read>(), fn_2_p, 0_u);
+        b.ir.SetName(p_mat, "p_mat");
+        b.Return(fn_2, b.Call(fn_1, p_mat));
+    });
+
+    auto* fn_3 = b.Function("f3", ty.f32());
+    auto* fn_3_p0 = b.FunctionParam("p0", ty.ptr<uniform, read>(ty.array(Inner, 4)));
+    auto* fn_3_p1 = b.FunctionParam("p1", ty.ptr<uniform, mat3x4<f32>, read>());
+    fn_3->SetParams({fn_3_p0, fn_3_p1});
+    b.Append(fn_3->Block(), [&] {
+        auto* p0_inner = b.Access(ty.ptr<uniform, read>(Inner), fn_3_p0, 3_i);
+        b.ir.SetName(p0_inner, "p0_inner");
+        auto* call_0 = b.Call(ty.f32(), fn_2, p0_inner);
+        auto* call_1 = b.Call(ty.f32(), fn_1, fn_3_p1);
+        b.Return(fn_3, b.Add(ty.f32(), call_0, call_1));
+    });
+
+    auto* fn_4 = b.Function("f4", ty.f32());
+    auto* fn_4_p = b.FunctionParam("p", ty.ptr<uniform, read>(Outer));
+    fn_4->SetParams({fn_4_p});
+    b.Append(fn_4->Block(), [&] {
+        auto* access_0 = b.Access(ty.ptr<uniform, read>(ty.array(Inner, 4)), fn_4_p, 0_u);
+        auto* access_1 = b.Access(ty.ptr<uniform, mat3x4<f32>, read>(), U, 1_u);
+        b.Return(fn_4, b.Call(ty.f32(), fn_3, access_0, access_1));
+    });
+
+    auto* fn_b = b.Function("b", ty.void_());
+    b.Append(fn_b->Block(), [&] {
+        b.Call(ty.f32(), fn_4, U);
+        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)
+}
+
+%b1 = block {  # root
+  %U:ptr<uniform, Outer, read> = var @binding_point(0, 0)
+}
+
+%f0 = func(%p:ptr<uniform, vec4<f32>, read>):f32 -> %b2 {
+  %b2 = block {
+    %4:f32 = load_vector_element %p, 0u
+    ret %4
+  }
+}
+%f1 = func(%p_1:ptr<uniform, mat3x4<f32>, read>):f32 -> %b3 {  # %p_1: 'p'
+  %b3 = block {
+    %res:ptr<function, f32, read_write> = var
+    %8:ptr<uniform, vec4<f32>, read> = access %p_1, 1i
+    %9:f32 = call %f0, %8
+    %10:f32 = load %res
+    %11:f32 = add %10, %9
+    store %res, %11
+    %p_vec:ptr<uniform, vec4<f32>, read> = access %p_1, 1i
+    %13:f32 = call %f0, %p_vec
+    %14:f32 = load %res
+    %15:f32 = add %14, %13
+    store %res, %15
+    %16:ptr<uniform, vec4<f32>, read> = access %U, 0u, 2i, 0u, 1i
+    %17:f32 = call %f0, %16
+    %18:f32 = load %res
+    %19:f32 = add %18, %17
+    store %res, %19
+    %p_vec_1:ptr<uniform, vec4<f32>, read> = access %U, 0u, 2i, 0u, 1i  # %p_vec_1: 'p_vec'
+    %21:f32 = call %f0, %p_vec_1
+    %22:f32 = load %res
+    %23:f32 = add %22, %21
+    store %res, %23
+    %24:f32 = load %res
+    ret %24
+  }
+}
+%f2 = func(%p_2:ptr<uniform, Inner, read>):f32 -> %b4 {  # %p_2: 'p'
+  %b4 = block {
+    %p_mat:ptr<uniform, mat3x4<f32>, read> = access %p_2, 0u
+    %28:f32 = call %f1, %p_mat
+    ret %28
+  }
+}
+%f3 = func(%p0:ptr<uniform, array<Inner, 4>, read>, %p1:ptr<uniform, mat3x4<f32>, read>):f32 -> %b5 {
+  %b5 = block {
+    %p0_inner:ptr<uniform, Inner, read> = access %p0, 3i
+    %33:f32 = call %f2, %p0_inner
+    %34:f32 = call %f1, %p1
+    %35:f32 = add %33, %34
+    ret %35
+  }
+}
+%f4 = func(%p_3:ptr<uniform, Outer, read>):f32 -> %b6 {  # %p_3: 'p'
+  %b6 = block {
+    %38:ptr<uniform, array<Inner, 4>, read> = access %p_3, 0u
+    %39:ptr<uniform, mat3x4<f32>, read> = access %U, 1u
+    %40:f32 = call %f3, %38, %39
+    ret %40
+  }
+}
+%b = func():void -> %b7 {
+  %b7 = block {
+    %42:f32 = call %f4, %U
+    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)
+}
+
+%b1 = block {  # root
+  %U:ptr<uniform, Outer, read> = var @binding_point(0, 0)
+}
+
+%f0_U_mat_X = func(%p_indices:array<u32, 1>):f32 -> %b2 {
+  %b2 = block {
+    %4:u32 = access %p_indices, 0u
+    %5:ptr<uniform, vec4<f32>, read> = access %U, 1u, %4
+    %6:f32 = load_vector_element %5, 0u
+    ret %6
+  }
+}
+%f0_U_arr_X_mat_X = func(%p_indices_1:array<u32, 2>):f32 -> %b3 {  # %p_indices_1: 'p_indices'
+  %b3 = block {
+    %9:u32 = access %p_indices_1, 0u
+    %10:u32 = access %p_indices_1, 1u
+    %11:ptr<uniform, vec4<f32>, read> = access %U, 0u, %9, 0u, %10
+    %12:f32 = load_vector_element %11, 0u
+    ret %12
+  }
+}
+%f1_U_mat = func():f32 -> %b4 {
+  %b4 = block {
+    %res:ptr<function, f32, read_write> = var
+    %15:u32 = convert 1i
+    %16:array<u32, 1> = construct %15
+    %17:f32 = call %f0_U_mat_X, %16
+    %18:f32 = load %res
+    %19:f32 = add %18, %17
+    store %res, %19
+    %20:u32 = convert 1i
+    %21:array<u32, 1> = construct %20
+    %22:f32 = call %f0_U_mat_X, %21
+    %23:f32 = load %res
+    %24:f32 = add %23, %22
+    store %res, %24
+    %25:u32 = convert 2i
+    %26:u32 = convert 1i
+    %27:array<u32, 2> = construct %25, %26
+    %28:f32 = call %f0_U_arr_X_mat_X, %27
+    %29:f32 = load %res
+    %30:f32 = add %29, %28
+    store %res, %30
+    %31:u32 = convert 2i
+    %32:u32 = convert 1i
+    %33:array<u32, 2> = construct %31, %32
+    %34:f32 = call %f0_U_arr_X_mat_X, %33
+    %35:f32 = load %res
+    %36:f32 = add %35, %34
+    store %res, %36
+    %37:f32 = load %res
+    ret %37
+  }
+}
+%f1_U_arr_X_mat = func(%p_indices_2:array<u32, 1>):f32 -> %b5 {  # %p_indices_2: 'p_indices'
+  %b5 = block {
+    %40:u32 = access %p_indices_2, 0u
+    %res_1:ptr<function, f32, read_write> = var  # %res_1: 'res'
+    %42:u32 = convert 1i
+    %43:array<u32, 2> = construct %40, %42
+    %44:f32 = call %f0_U_arr_X_mat_X, %43
+    %45:f32 = load %res_1
+    %46:f32 = add %45, %44
+    store %res_1, %46
+    %47:u32 = convert 1i
+    %48:array<u32, 2> = construct %40, %47
+    %49:f32 = call %f0_U_arr_X_mat_X, %48
+    %50:f32 = load %res_1
+    %51:f32 = add %50, %49
+    store %res_1, %51
+    %52:u32 = convert 2i
+    %53:u32 = convert 1i
+    %54:array<u32, 2> = construct %52, %53
+    %55:f32 = call %f0_U_arr_X_mat_X, %54
+    %56:f32 = load %res_1
+    %57:f32 = add %56, %55
+    store %res_1, %57
+    %58:u32 = convert 2i
+    %59:u32 = convert 1i
+    %60:array<u32, 2> = construct %58, %59
+    %61:f32 = call %f0_U_arr_X_mat_X, %60
+    %62:f32 = load %res_1
+    %63:f32 = add %62, %61
+    store %res_1, %63
+    %64:f32 = load %res_1
+    ret %64
+  }
+}
+%f2_U_arr_X = func(%p_indices_3:array<u32, 1>):f32 -> %b6 {  # %p_indices_3: 'p_indices'
+  %b6 = block {
+    %67:u32 = access %p_indices_3, 0u
+    %68:array<u32, 1> = construct %67
+    %69:f32 = call %f1_U_arr_X_mat, %68
+    ret %69
+  }
+}
+%f3_U_arr_U_mat = func():f32 -> %b7 {
+  %b7 = block {
+    %71:u32 = convert 3i
+    %72:array<u32, 1> = construct %71
+    %73:f32 = call %f2_U_arr_X, %72
+    %74:f32 = call %f1_U_mat
+    %75:f32 = add %73, %74
+    ret %75
+  }
+}
+%f4_U = func():f32 -> %b8 {
+  %b8 = block {
+    %77:f32 = call %f3_U_arr_U_mat
+    ret %77
+  }
+}
+%b = func():void -> %b9 {
+  %b9 = block {
+    %79:f32 = call %f4_U
+    ret
+  }
+}
+)";
+
+    Run(DirectVariableAccess, DirectVariableAccessOptions{});
+
+    EXPECT_EQ(expect, str());
+}
+
+}  // namespace uniform_as_tests
+
+////////////////////////////////////////////////////////////////////////////////
+// 'storage' address space
+////////////////////////////////////////////////////////////////////////////////
+namespace storage_as_tests {
+
+using IR_DirectVariableAccessTest_StorageAS = TransformTest;
+
+TEST_F(IR_DirectVariableAccessTest_StorageAS, Param_ptr_i32_Via_struct_read) {
+    auto* str_ = ty.Struct(mod.symbols.New("str"), {
+                                                       {mod.symbols.Register("i"), ty.i32()},
+                                                   });
+
+    Var* S = nullptr;
+    b.Append(b.ir.root_block,
+             [&] {  //
+                 S = b.Var("S", ty.ptr<storage, read>(str_));
+                 S->SetBindingPoint(0, 0);
+             });
+
+    auto* fn_a = b.Function("a", ty.i32());
+    auto* fn_a_p = b.FunctionParam("p", ty.ptr<storage, i32, read>());
+    fn_a->SetParams({
+        b.FunctionParam("pre", ty.i32()),
+        fn_a_p,
+        b.FunctionParam("post", ty.i32()),
+    });
+    b.Append(fn_a->Block(), [&] { b.Return(fn_a, b.Load(fn_a_p)); });
+
+    auto* fn_b = b.Function("b", ty.void_());
+    b.Append(fn_b->Block(), [&] {
+        auto* access = b.Access(ty.ptr<storage, i32, read>(), S, 0_u);
+        b.Call(fn_a, 10_i, access, 20_i);
+        b.Return(fn_b);
+    });
+
+    auto* src = R"(
+str = struct @align(4) {
+  i:i32 @offset(0)
+}
+
+%b1 = block {  # root
+  %S:ptr<storage, str, read> = var @binding_point(0, 0)
+}
+
+%a = func(%pre:i32, %p:ptr<storage, i32, read>, %post:i32):i32 -> %b2 {
+  %b2 = block {
+    %6:i32 = load %p
+    ret %6
+  }
+}
+%b = func():void -> %b3 {
+  %b3 = block {
+    %8:ptr<storage, i32, read> = access %S, 0u
+    %9:i32 = call %a, 10i, %8, 20i
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+str = struct @align(4) {
+  i:i32 @offset(0)
+}
+
+%b1 = block {  # root
+  %S:ptr<storage, str, read> = var @binding_point(0, 0)
+}
+
+%a_S_i = func(%pre:i32, %post:i32):i32 -> %b2 {
+  %b2 = block {
+    %5:ptr<storage, i32, read> = access %S, 0u
+    %6:i32 = load %5
+    ret %6
+  }
+}
+%b = func():void -> %b3 {
+  %b3 = block {
+    %8:i32 = call %a_S_i, 10i, 20i
+    ret
+  }
+}
+)";
+
+    Run(DirectVariableAccess, DirectVariableAccessOptions{});
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_StorageAS, Param_ptr_arr_i32_Via_struct_write) {
+    auto* str_ =
+        ty.Struct(mod.symbols.New("str"), {
+                                              {mod.symbols.Register("arr"), ty.array<i32, 4>()},
+                                          });
+
+    Var* S = nullptr;
+    b.Append(b.ir.root_block,
+             [&] {  //
+                 S = b.Var("S", ty.ptr<storage>(str_));
+                 S->SetBindingPoint(0, 0);
+             });
+
+    auto* fn_a = b.Function("a", ty.void_());
+    auto* fn_a_p = b.FunctionParam("p", ty.ptr<storage, array<i32, 4>>());
+    fn_a->SetParams({
+        b.FunctionParam("pre", ty.i32()),
+        fn_a_p,
+        b.FunctionParam("post", ty.i32()),
+    });
+    b.Append(fn_a->Block(), [&] {
+        b.Store(fn_a_p, b.Splat(ty.array<i32, 4>(), 0_i, 4));
+        b.Return(fn_a);
+    });
+
+    auto* fn_b = b.Function("b", ty.void_());
+    b.Append(fn_b->Block(), [&] {
+        auto* access = b.Access(ty.ptr<storage, array<i32, 4>>(), S, 0_u);
+        b.Call(fn_a, 10_i, access, 20_i);
+        b.Return(fn_b);
+    });
+
+    auto* src = R"(
+str = struct @align(4) {
+  arr:array<i32, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %S:ptr<storage, str, read_write> = var @binding_point(0, 0)
+}
+
+%a = func(%pre:i32, %p:ptr<storage, array<i32, 4>, read_write>, %post:i32):void -> %b2 {
+  %b2 = block {
+    store %p, array<i32, 4>(0i)
+    ret
+  }
+}
+%b = func():void -> %b3 {
+  %b3 = block {
+    %7:ptr<storage, array<i32, 4>, read_write> = access %S, 0u
+    %8:void = call %a, 10i, %7, 20i
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+str = struct @align(4) {
+  arr:array<i32, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %S:ptr<storage, str, read_write> = var @binding_point(0, 0)
+}
+
+%a_S_arr = func(%pre:i32, %post:i32):void -> %b2 {
+  %b2 = block {
+    %5:ptr<storage, array<i32, 4>, read_write> = access %S, 0u
+    store %5, array<i32, 4>(0i)
+    ret
+  }
+}
+%b = func():void -> %b3 {
+  %b3 = block {
+    %7:void = call %a_S_arr, 10i, 20i
+    ret
+  }
+}
+)";
+
+    Run(DirectVariableAccess, DirectVariableAccessOptions{});
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_StorageAS, Param_ptr_vec4i32_Via_array_DynamicWrite) {
+    Var* S = nullptr;
+    b.Append(b.ir.root_block,
+             [&] {  //
+                 S = b.Var<storage, array<vec4<i32>, 8>>("S");
+                 S->SetBindingPoint(0, 0);
+             });
+
+    auto* fn_a = b.Function("a", ty.void_());
+    auto* fn_a_p = b.FunctionParam("p", ty.ptr<storage, vec4<i32>>());
+    fn_a->SetParams({
+        b.FunctionParam("pre", ty.i32()),
+        fn_a_p,
+        b.FunctionParam("post", ty.i32()),
+    });
+    b.Append(fn_a->Block(), [&] {
+        b.Store(fn_a_p, b.Splat(ty.vec4<i32>(), 0_i, 4));
+        b.Return(fn_a);
+    });
+
+    auto* fn_b = b.Function("b", ty.void_());
+    b.Append(fn_b->Block(), [&] {
+        auto* I = b.Let("I", 3_i);
+        auto* access = b.Access(ty.ptr<storage, vec4<i32>>(), S, I);
+        b.Call(fn_a, 10_i, access, 20_i);
+        b.Return(fn_b);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %S:ptr<storage, array<vec4<i32>, 8>, read_write> = var @binding_point(0, 0)
+}
+
+%a = func(%pre:i32, %p:ptr<storage, vec4<i32>, read_write>, %post:i32):void -> %b2 {
+  %b2 = block {
+    store %p, vec4<i32>(0i)
+    ret
+  }
+}
+%b = func():void -> %b3 {
+  %b3 = block {
+    %I:i32 = let 3i
+    %8:ptr<storage, vec4<i32>, read_write> = access %S, %I
+    %9:void = call %a, 10i, %8, 20i
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %S:ptr<storage, array<vec4<i32>, 8>, read_write> = var @binding_point(0, 0)
+}
+
+%a_S_X = func(%pre:i32, %p_indices:array<u32, 1>, %post:i32):void -> %b2 {
+  %b2 = block {
+    %6:u32 = access %p_indices, 0u
+    %7:ptr<storage, vec4<i32>, read_write> = access %S, %6
+    store %7, vec4<i32>(0i)
+    ret
+  }
+}
+%b = func():void -> %b3 {
+  %b3 = block {
+    %I:i32 = let 3i
+    %10:u32 = convert %I
+    %11:array<u32, 1> = construct %10
+    %12:void = call %a_S_X, 10i, %11, 20i
+    ret
+  }
+}
+)";
+
+    Run(DirectVariableAccess, DirectVariableAccessOptions{});
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_StorageAS, 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>()},
+                                            });
+    Var* S = nullptr;
+    b.Append(b.ir.root_block,
+             [&] {  //
+                 S = b.Var("S", ty.ptr<storage, read>(Outer));
+                 S->SetBindingPoint(0, 0);
+             });
+
+    auto* fn_0 = b.Function("f0", ty.f32());
+    auto* fn_0_p = b.FunctionParam("p", ty.ptr<storage, vec4<f32>, read>());
+    fn_0->SetParams({fn_0_p});
+    b.Append(fn_0->Block(), [&] { b.Return(fn_0, b.LoadVectorElement(fn_0_p, 0_u)); });
+
+    auto* fn_1 = b.Function("f1", ty.f32());
+    auto* fn_1_p = b.FunctionParam("p", ty.ptr<storage, mat3x4<f32>, read>());
+    fn_1->SetParams({fn_1_p});
+    b.Append(fn_1->Block(), [&] {
+        auto* res = b.Var<function, f32>("res");
+        {
+            // res += f0(&(*p)[1]);
+            auto* call_0 = b.Call(fn_0, b.Access(ty.ptr<storage, vec4<f32>, read>(), fn_1_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<storage, vec4<f32>, read>(), fn_1_p, 1_i);
+            b.ir.SetName(p_vec, "p_vec");
+            auto* call_0 = b.Call(fn_0, p_vec);
+            b.Store(res, b.Add(ty.f32(), b.Load(res), call_0));
+        }
+        {
+            // res += f0(&U.arr[2].mat[1]);
+            auto* access = b.Access(ty.ptr<storage, vec4<f32>, read>(), S, 0_u, 2_i, 0_u, 1_i);
+            auto* call_0 = b.Call(fn_0, access);
+            b.Store(res, b.Add(ty.f32(), b.Load(res), call_0));
+        }
+        {
+            // let p_vec = &U.arr[2].mat[1];
+            // res += f0(p_vec);
+            auto* p_vec = b.Access(ty.ptr<storage, vec4<f32>, read>(), S, 0_u, 2_i, 0_u, 1_i);
+            b.ir.SetName(p_vec, "p_vec");
+            auto* call_0 = b.Call(fn_0, p_vec);
+            b.Store(res, b.Add(ty.f32(), b.Load(res), call_0));
+        }
+
+        b.Return(fn_1, b.Load(res));
+    });
+
+    auto* fn_2 = b.Function("f2", ty.f32());
+    auto* fn_2_p = b.FunctionParam("p", ty.ptr<storage, read>(Inner));
+    fn_2->SetParams({fn_2_p});
+    b.Append(fn_2->Block(), [&] {
+        auto* p_mat = b.Access(ty.ptr<storage, mat3x4<f32>, read>(), fn_2_p, 0_u);
+        b.ir.SetName(p_mat, "p_mat");
+        b.Return(fn_2, b.Call(fn_1, p_mat));
+    });
+
+    auto* fn_3 = b.Function("f3", ty.f32());
+    auto* fn_3_p0 = b.FunctionParam("p0", ty.ptr<storage, read>(ty.array(Inner, 4)));
+    auto* fn_3_p1 = b.FunctionParam("p1", ty.ptr<storage, mat3x4<f32>, read>());
+    fn_3->SetParams({fn_3_p0, fn_3_p1});
+    b.Append(fn_3->Block(), [&] {
+        auto* p0_inner = b.Access(ty.ptr<storage, read>(Inner), fn_3_p0, 3_i);
+        b.ir.SetName(p0_inner, "p0_inner");
+        auto* call_0 = b.Call(ty.f32(), fn_2, p0_inner);
+        auto* call_1 = b.Call(ty.f32(), fn_1, fn_3_p1);
+        b.Return(fn_3, b.Add(ty.f32(), call_0, call_1));
+    });
+
+    auto* fn_4 = b.Function("f4", ty.f32());
+    auto* fn_4_p = b.FunctionParam("p", ty.ptr<storage, read>(Outer));
+    fn_4->SetParams({fn_4_p});
+    b.Append(fn_4->Block(), [&] {
+        auto* access_0 = b.Access(ty.ptr<storage, read>(ty.array(Inner, 4)), fn_4_p, 0_u);
+        auto* access_1 = b.Access(ty.ptr<storage, mat3x4<f32>, read>(), S, 1_u);
+        b.Return(fn_4, b.Call(ty.f32(), fn_3, access_0, access_1));
+    });
+
+    auto* fn_b = b.Function("b", ty.void_());
+    b.Append(fn_b->Block(), [&] {
+        b.Call(ty.f32(), fn_4, S);
+        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)
+}
+
+%b1 = block {  # root
+  %S:ptr<storage, Outer, read> = var @binding_point(0, 0)
+}
+
+%f0 = func(%p:ptr<storage, vec4<f32>, read>):f32 -> %b2 {
+  %b2 = block {
+    %4:f32 = load_vector_element %p, 0u
+    ret %4
+  }
+}
+%f1 = func(%p_1:ptr<storage, mat3x4<f32>, read>):f32 -> %b3 {  # %p_1: 'p'
+  %b3 = block {
+    %res:ptr<function, f32, read_write> = var
+    %8:ptr<storage, vec4<f32>, read> = access %p_1, 1i
+    %9:f32 = call %f0, %8
+    %10:f32 = load %res
+    %11:f32 = add %10, %9
+    store %res, %11
+    %p_vec:ptr<storage, vec4<f32>, read> = access %p_1, 1i
+    %13:f32 = call %f0, %p_vec
+    %14:f32 = load %res
+    %15:f32 = add %14, %13
+    store %res, %15
+    %16:ptr<storage, vec4<f32>, read> = access %S, 0u, 2i, 0u, 1i
+    %17:f32 = call %f0, %16
+    %18:f32 = load %res
+    %19:f32 = add %18, %17
+    store %res, %19
+    %p_vec_1:ptr<storage, vec4<f32>, read> = access %S, 0u, 2i, 0u, 1i  # %p_vec_1: 'p_vec'
+    %21:f32 = call %f0, %p_vec_1
+    %22:f32 = load %res
+    %23:f32 = add %22, %21
+    store %res, %23
+    %24:f32 = load %res
+    ret %24
+  }
+}
+%f2 = func(%p_2:ptr<storage, Inner, read>):f32 -> %b4 {  # %p_2: 'p'
+  %b4 = block {
+    %p_mat:ptr<storage, mat3x4<f32>, read> = access %p_2, 0u
+    %28:f32 = call %f1, %p_mat
+    ret %28
+  }
+}
+%f3 = func(%p0:ptr<storage, array<Inner, 4>, read>, %p1:ptr<storage, mat3x4<f32>, read>):f32 -> %b5 {
+  %b5 = block {
+    %p0_inner:ptr<storage, Inner, read> = access %p0, 3i
+    %33:f32 = call %f2, %p0_inner
+    %34:f32 = call %f1, %p1
+    %35:f32 = add %33, %34
+    ret %35
+  }
+}
+%f4 = func(%p_3:ptr<storage, Outer, read>):f32 -> %b6 {  # %p_3: 'p'
+  %b6 = block {
+    %38:ptr<storage, array<Inner, 4>, read> = access %p_3, 0u
+    %39:ptr<storage, mat3x4<f32>, read> = access %S, 1u
+    %40:f32 = call %f3, %38, %39
+    ret %40
+  }
+}
+%b = func():void -> %b7 {
+  %b7 = block {
+    %42:f32 = call %f4, %S
+    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)
+}
+
+%b1 = block {  # root
+  %S:ptr<storage, Outer, read> = var @binding_point(0, 0)
+}
+
+%f0_S_mat_X = func(%p_indices:array<u32, 1>):f32 -> %b2 {
+  %b2 = block {
+    %4:u32 = access %p_indices, 0u
+    %5:ptr<storage, vec4<f32>, read> = access %S, 1u, %4
+    %6:f32 = load_vector_element %5, 0u
+    ret %6
+  }
+}
+%f0_S_arr_X_mat_X = func(%p_indices_1:array<u32, 2>):f32 -> %b3 {  # %p_indices_1: 'p_indices'
+  %b3 = block {
+    %9:u32 = access %p_indices_1, 0u
+    %10:u32 = access %p_indices_1, 1u
+    %11:ptr<storage, vec4<f32>, read> = access %S, 0u, %9, 0u, %10
+    %12:f32 = load_vector_element %11, 0u
+    ret %12
+  }
+}
+%f1_S_mat = func():f32 -> %b4 {
+  %b4 = block {
+    %res:ptr<function, f32, read_write> = var
+    %15:u32 = convert 1i
+    %16:array<u32, 1> = construct %15
+    %17:f32 = call %f0_S_mat_X, %16
+    %18:f32 = load %res
+    %19:f32 = add %18, %17
+    store %res, %19
+    %20:u32 = convert 1i
+    %21:array<u32, 1> = construct %20
+    %22:f32 = call %f0_S_mat_X, %21
+    %23:f32 = load %res
+    %24:f32 = add %23, %22
+    store %res, %24
+    %25:u32 = convert 2i
+    %26:u32 = convert 1i
+    %27:array<u32, 2> = construct %25, %26
+    %28:f32 = call %f0_S_arr_X_mat_X, %27
+    %29:f32 = load %res
+    %30:f32 = add %29, %28
+    store %res, %30
+    %31:u32 = convert 2i
+    %32:u32 = convert 1i
+    %33:array<u32, 2> = construct %31, %32
+    %34:f32 = call %f0_S_arr_X_mat_X, %33
+    %35:f32 = load %res
+    %36:f32 = add %35, %34
+    store %res, %36
+    %37:f32 = load %res
+    ret %37
+  }
+}
+%f1_S_arr_X_mat = func(%p_indices_2:array<u32, 1>):f32 -> %b5 {  # %p_indices_2: 'p_indices'
+  %b5 = block {
+    %40:u32 = access %p_indices_2, 0u
+    %res_1:ptr<function, f32, read_write> = var  # %res_1: 'res'
+    %42:u32 = convert 1i
+    %43:array<u32, 2> = construct %40, %42
+    %44:f32 = call %f0_S_arr_X_mat_X, %43
+    %45:f32 = load %res_1
+    %46:f32 = add %45, %44
+    store %res_1, %46
+    %47:u32 = convert 1i
+    %48:array<u32, 2> = construct %40, %47
+    %49:f32 = call %f0_S_arr_X_mat_X, %48
+    %50:f32 = load %res_1
+    %51:f32 = add %50, %49
+    store %res_1, %51
+    %52:u32 = convert 2i
+    %53:u32 = convert 1i
+    %54:array<u32, 2> = construct %52, %53
+    %55:f32 = call %f0_S_arr_X_mat_X, %54
+    %56:f32 = load %res_1
+    %57:f32 = add %56, %55
+    store %res_1, %57
+    %58:u32 = convert 2i
+    %59:u32 = convert 1i
+    %60:array<u32, 2> = construct %58, %59
+    %61:f32 = call %f0_S_arr_X_mat_X, %60
+    %62:f32 = load %res_1
+    %63:f32 = add %62, %61
+    store %res_1, %63
+    %64:f32 = load %res_1
+    ret %64
+  }
+}
+%f2_S_arr_X = func(%p_indices_3:array<u32, 1>):f32 -> %b6 {  # %p_indices_3: 'p_indices'
+  %b6 = block {
+    %67:u32 = access %p_indices_3, 0u
+    %68:array<u32, 1> = construct %67
+    %69:f32 = call %f1_S_arr_X_mat, %68
+    ret %69
+  }
+}
+%f3_S_arr_S_mat = func():f32 -> %b7 {
+  %b7 = block {
+    %71:u32 = convert 3i
+    %72:array<u32, 1> = construct %71
+    %73:f32 = call %f2_S_arr_X, %72
+    %74:f32 = call %f1_S_mat
+    %75:f32 = add %73, %74
+    ret %75
+  }
+}
+%f4_S = func():f32 -> %b8 {
+  %b8 = block {
+    %77:f32 = call %f3_S_arr_S_mat
+    ret %77
+  }
+}
+%b = func():void -> %b9 {
+  %b9 = block {
+    %79:f32 = call %f4_S
+    ret
+  }
+}
+)";
+
+    Run(DirectVariableAccess, DirectVariableAccessOptions{});
+
+    EXPECT_EQ(expect, str());
+}
+
+}  // namespace storage_as_tests
+
+////////////////////////////////////////////////////////////////////////////////
+// 'workgroup' address space
+////////////////////////////////////////////////////////////////////////////////
+namespace workgroup_as_tests {
+
+using IR_DirectVariableAccessTest_WorkgroupAS = TransformTest;
+
+TEST_F(IR_DirectVariableAccessTest_WorkgroupAS, Param_ptr_vec4i32_Via_array_StaticRead) {
+    Var* W = nullptr;
+    b.Append(b.ir.root_block,
+             [&] {  //
+                 W = b.Var("W", ty.ptr<workgroup, array<vec4<i32>, 8>>());
+             });
+
+    auto* fn_a = b.Function("a", ty.vec4<i32>());
+    auto* fn_a_p = b.FunctionParam("p", ty.ptr<workgroup, vec4<i32>>());
+    fn_a->SetParams({
+        b.FunctionParam("pre", ty.i32()),
+        fn_a_p,
+        b.FunctionParam("post", ty.i32()),
+    });
+    b.Append(fn_a->Block(), [&] { b.Return(fn_a, b.Load(fn_a_p)); });
+
+    auto* fn_b = b.Function("b", ty.void_());
+    b.Append(fn_b->Block(), [&] {
+        auto* access = b.Access(ty.ptr<workgroup, vec4<i32>>(), W, 3_i);
+        b.Call(fn_a, 10_i, access, 20_i);
+        b.Return(fn_b);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %W:ptr<workgroup, array<vec4<i32>, 8>, read_write> = var
+}
+
+%a = func(%pre:i32, %p:ptr<workgroup, vec4<i32>, read_write>, %post:i32):vec4<i32> -> %b2 {
+  %b2 = block {
+    %6:vec4<i32> = load %p
+    ret %6
+  }
+}
+%b = func():void -> %b3 {
+  %b3 = block {
+    %8:ptr<workgroup, vec4<i32>, read_write> = access %W, 3i
+    %9:vec4<i32> = call %a, 10i, %8, 20i
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %W:ptr<workgroup, array<vec4<i32>, 8>, read_write> = var
+}
+
+%a_W_X = func(%pre:i32, %p_indices:array<u32, 1>, %post:i32):vec4<i32> -> %b2 {
+  %b2 = block {
+    %6:u32 = access %p_indices, 0u
+    %7:ptr<workgroup, vec4<i32>, read_write> = access %W, %6
+    %8:vec4<i32> = load %7
+    ret %8
+  }
+}
+%b = func():void -> %b3 {
+  %b3 = block {
+    %10:u32 = convert 3i
+    %11:array<u32, 1> = construct %10
+    %12:vec4<i32> = call %a_W_X, 10i, %11, 20i
+    ret
+  }
+}
+)";
+
+    Run(DirectVariableAccess, DirectVariableAccessOptions{});
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_WorkgroupAS, Param_ptr_vec4i32_Via_array_StaticWrite) {
+    Var* W = nullptr;
+    b.Append(b.ir.root_block,
+             [&] {  //
+                 W = b.Var<workgroup, array<vec4<i32>, 8>>("W");
+             });
+
+    auto* fn_a = b.Function("a", ty.void_());
+    auto* fn_a_p = b.FunctionParam("p", ty.ptr<workgroup, vec4<i32>>());
+    fn_a->SetParams({
+        b.FunctionParam("pre", ty.i32()),
+        fn_a_p,
+        b.FunctionParam("post", ty.i32()),
+    });
+    b.Append(fn_a->Block(), [&] {
+        b.Store(fn_a_p, b.Splat(ty.vec4<i32>(), 0_i, 4));
+        b.Return(fn_a);
+    });
+
+    auto* fn_b = b.Function("b", ty.void_());
+    b.Append(fn_b->Block(), [&] {
+        auto* access = b.Access(ty.ptr<workgroup, vec4<i32>>(), W, 3_i);
+        b.Call(fn_a, 10_i, access, 20_i);
+        b.Return(fn_b);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %W:ptr<workgroup, array<vec4<i32>, 8>, read_write> = var
+}
+
+%a = func(%pre:i32, %p:ptr<workgroup, vec4<i32>, read_write>, %post:i32):void -> %b2 {
+  %b2 = block {
+    store %p, vec4<i32>(0i)
+    ret
+  }
+}
+%b = func():void -> %b3 {
+  %b3 = block {
+    %7:ptr<workgroup, vec4<i32>, read_write> = access %W, 3i
+    %8:void = call %a, 10i, %7, 20i
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %W:ptr<workgroup, array<vec4<i32>, 8>, read_write> = var
+}
+
+%a_W_X = func(%pre:i32, %p_indices:array<u32, 1>, %post:i32):void -> %b2 {
+  %b2 = block {
+    %6:u32 = access %p_indices, 0u
+    %7:ptr<workgroup, vec4<i32>, read_write> = access %W, %6
+    store %7, vec4<i32>(0i)
+    ret
+  }
+}
+%b = func():void -> %b3 {
+  %b3 = block {
+    %9:u32 = convert 3i
+    %10:array<u32, 1> = construct %9
+    %11:void = call %a_W_X, 10i, %10, 20i
+    ret
+  }
+}
+)";
+
+    Run(DirectVariableAccess, DirectVariableAccessOptions{});
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_WorkgroupAS, 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>()},
+                                            });
+    Var* W = nullptr;
+    b.Append(b.ir.root_block,
+             [&] {  //
+                 W = b.Var("W", ty.ptr<workgroup>(Outer));
+             });
+
+    auto* fn_0 = b.Function("f0", ty.f32());
+    auto* fn_0_p = b.FunctionParam("p", ty.ptr<workgroup, vec4<f32>>());
+    fn_0->SetParams({fn_0_p});
+    b.Append(fn_0->Block(), [&] { b.Return(fn_0, b.LoadVectorElement(fn_0_p, 0_u)); });
+
+    auto* fn_1 = b.Function("f1", ty.f32());
+    auto* fn_1_p = b.FunctionParam("p", ty.ptr<workgroup, mat3x4<f32>>());
+    fn_1->SetParams({fn_1_p});
+    b.Append(fn_1->Block(), [&] {
+        auto* res = b.Var<function, f32>("res");
+        {
+            // res += f0(&(*p)[1]);
+            auto* call_0 = b.Call(fn_0, b.Access(ty.ptr<workgroup, vec4<f32>>(), fn_1_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<workgroup, vec4<f32>>(), fn_1_p, 1_i);
+            b.ir.SetName(p_vec, "p_vec");
+            auto* call_0 = b.Call(fn_0, p_vec);
+            b.Store(res, b.Add(ty.f32(), b.Load(res), call_0));
+        }
+        {
+            // res += f0(&U.arr[2].mat[1]);
+            auto* access = b.Access(ty.ptr<workgroup, vec4<f32>>(), W, 0_u, 2_i, 0_u, 1_i);
+            auto* call_0 = b.Call(fn_0, access);
+            b.Store(res, b.Add(ty.f32(), b.Load(res), call_0));
+        }
+        {
+            // let p_vec = &U.arr[2].mat[1];
+            // res += f0(p_vec);
+            auto* p_vec = b.Access(ty.ptr<workgroup, vec4<f32>>(), W, 0_u, 2_i, 0_u, 1_i);
+            b.ir.SetName(p_vec, "p_vec");
+            auto* call_0 = b.Call(fn_0, p_vec);
+            b.Store(res, b.Add(ty.f32(), b.Load(res), call_0));
+        }
+
+        b.Return(fn_1, b.Load(res));
+    });
+
+    auto* fn_2 = b.Function("f2", ty.f32());
+    auto* fn_2_p = b.FunctionParam("p", ty.ptr<workgroup>(Inner));
+    fn_2->SetParams({fn_2_p});
+    b.Append(fn_2->Block(), [&] {
+        auto* p_mat = b.Access(ty.ptr<workgroup, mat3x4<f32>>(), fn_2_p, 0_u);
+        b.ir.SetName(p_mat, "p_mat");
+        b.Return(fn_2, b.Call(fn_1, p_mat));
+    });
+
+    auto* fn_3 = b.Function("f3", ty.f32());
+    auto* fn_3_p0 = b.FunctionParam("p0", ty.ptr<workgroup>(ty.array(Inner, 4)));
+    auto* fn_3_p1 = b.FunctionParam("p1", ty.ptr<workgroup, mat3x4<f32>>());
+    fn_3->SetParams({fn_3_p0, fn_3_p1});
+    b.Append(fn_3->Block(), [&] {
+        auto* p0_inner = b.Access(ty.ptr<workgroup>(Inner), fn_3_p0, 3_i);
+        b.ir.SetName(p0_inner, "p0_inner");
+        auto* call_0 = b.Call(ty.f32(), fn_2, p0_inner);
+        auto* call_1 = b.Call(ty.f32(), fn_1, fn_3_p1);
+        b.Return(fn_3, b.Add(ty.f32(), call_0, call_1));
+    });
+
+    auto* fn_4 = b.Function("f4", ty.f32());
+    auto* fn_4_p = b.FunctionParam("p", ty.ptr<workgroup>(Outer));
+    fn_4->SetParams({fn_4_p});
+    b.Append(fn_4->Block(), [&] {
+        auto* access_0 = b.Access(ty.ptr<workgroup>(ty.array(Inner, 4)), fn_4_p, 0_u);
+        auto* access_1 = b.Access(ty.ptr<workgroup, mat3x4<f32>>(), W, 1_u);
+        b.Return(fn_4, b.Call(ty.f32(), fn_3, access_0, access_1));
+    });
+
+    auto* fn_b = b.Function("b", ty.void_());
+    b.Append(fn_b->Block(), [&] {
+        b.Call(ty.f32(), fn_4, W);
+        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)
+}
+
+%b1 = block {  # root
+  %W:ptr<workgroup, Outer, read_write> = var
+}
+
+%f0 = func(%p:ptr<workgroup, vec4<f32>, read_write>):f32 -> %b2 {
+  %b2 = block {
+    %4:f32 = load_vector_element %p, 0u
+    ret %4
+  }
+}
+%f1 = func(%p_1:ptr<workgroup, mat3x4<f32>, read_write>):f32 -> %b3 {  # %p_1: 'p'
+  %b3 = block {
+    %res:ptr<function, f32, read_write> = var
+    %8:ptr<workgroup, vec4<f32>, read_write> = access %p_1, 1i
+    %9:f32 = call %f0, %8
+    %10:f32 = load %res
+    %11:f32 = add %10, %9
+    store %res, %11
+    %p_vec:ptr<workgroup, vec4<f32>, read_write> = access %p_1, 1i
+    %13:f32 = call %f0, %p_vec
+    %14:f32 = load %res
+    %15:f32 = add %14, %13
+    store %res, %15
+    %16:ptr<workgroup, vec4<f32>, read_write> = access %W, 0u, 2i, 0u, 1i
+    %17:f32 = call %f0, %16
+    %18:f32 = load %res
+    %19:f32 = add %18, %17
+    store %res, %19
+    %p_vec_1:ptr<workgroup, vec4<f32>, read_write> = access %W, 0u, 2i, 0u, 1i  # %p_vec_1: 'p_vec'
+    %21:f32 = call %f0, %p_vec_1
+    %22:f32 = load %res
+    %23:f32 = add %22, %21
+    store %res, %23
+    %24:f32 = load %res
+    ret %24
+  }
+}
+%f2 = func(%p_2:ptr<workgroup, Inner, read_write>):f32 -> %b4 {  # %p_2: 'p'
+  %b4 = block {
+    %p_mat:ptr<workgroup, mat3x4<f32>, read_write> = access %p_2, 0u
+    %28:f32 = call %f1, %p_mat
+    ret %28
+  }
+}
+%f3 = func(%p0:ptr<workgroup, array<Inner, 4>, read_write>, %p1:ptr<workgroup, mat3x4<f32>, read_write>):f32 -> %b5 {
+  %b5 = block {
+    %p0_inner:ptr<workgroup, Inner, read_write> = access %p0, 3i
+    %33:f32 = call %f2, %p0_inner
+    %34:f32 = call %f1, %p1
+    %35:f32 = add %33, %34
+    ret %35
+  }
+}
+%f4 = func(%p_3:ptr<workgroup, Outer, read_write>):f32 -> %b6 {  # %p_3: 'p'
+  %b6 = block {
+    %38:ptr<workgroup, array<Inner, 4>, read_write> = access %p_3, 0u
+    %39:ptr<workgroup, mat3x4<f32>, read_write> = access %W, 1u
+    %40:f32 = call %f3, %38, %39
+    ret %40
+  }
+}
+%b = func():void -> %b7 {
+  %b7 = block {
+    %42:f32 = call %f4, %W
+    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)
+}
+
+%b1 = block {  # root
+  %W:ptr<workgroup, Outer, read_write> = var
+}
+
+%f0_W_mat_X = func(%p_indices:array<u32, 1>):f32 -> %b2 {
+  %b2 = block {
+    %4:u32 = access %p_indices, 0u
+    %5:ptr<workgroup, vec4<f32>, read_write> = access %W, 1u, %4
+    %6:f32 = load_vector_element %5, 0u
+    ret %6
+  }
+}
+%f0_W_arr_X_mat_X = func(%p_indices_1:array<u32, 2>):f32 -> %b3 {  # %p_indices_1: 'p_indices'
+  %b3 = block {
+    %9:u32 = access %p_indices_1, 0u
+    %10:u32 = access %p_indices_1, 1u
+    %11:ptr<workgroup, vec4<f32>, read_write> = access %W, 0u, %9, 0u, %10
+    %12:f32 = load_vector_element %11, 0u
+    ret %12
+  }
+}
+%f1_W_mat = func():f32 -> %b4 {
+  %b4 = block {
+    %res:ptr<function, f32, read_write> = var
+    %15:u32 = convert 1i
+    %16:array<u32, 1> = construct %15
+    %17:f32 = call %f0_W_mat_X, %16
+    %18:f32 = load %res
+    %19:f32 = add %18, %17
+    store %res, %19
+    %20:u32 = convert 1i
+    %21:array<u32, 1> = construct %20
+    %22:f32 = call %f0_W_mat_X, %21
+    %23:f32 = load %res
+    %24:f32 = add %23, %22
+    store %res, %24
+    %25:u32 = convert 2i
+    %26:u32 = convert 1i
+    %27:array<u32, 2> = construct %25, %26
+    %28:f32 = call %f0_W_arr_X_mat_X, %27
+    %29:f32 = load %res
+    %30:f32 = add %29, %28
+    store %res, %30
+    %31:u32 = convert 2i
+    %32:u32 = convert 1i
+    %33:array<u32, 2> = construct %31, %32
+    %34:f32 = call %f0_W_arr_X_mat_X, %33
+    %35:f32 = load %res
+    %36:f32 = add %35, %34
+    store %res, %36
+    %37:f32 = load %res
+    ret %37
+  }
+}
+%f1_W_arr_X_mat = func(%p_indices_2:array<u32, 1>):f32 -> %b5 {  # %p_indices_2: 'p_indices'
+  %b5 = block {
+    %40:u32 = access %p_indices_2, 0u
+    %res_1:ptr<function, f32, read_write> = var  # %res_1: 'res'
+    %42:u32 = convert 1i
+    %43:array<u32, 2> = construct %40, %42
+    %44:f32 = call %f0_W_arr_X_mat_X, %43
+    %45:f32 = load %res_1
+    %46:f32 = add %45, %44
+    store %res_1, %46
+    %47:u32 = convert 1i
+    %48:array<u32, 2> = construct %40, %47
+    %49:f32 = call %f0_W_arr_X_mat_X, %48
+    %50:f32 = load %res_1
+    %51:f32 = add %50, %49
+    store %res_1, %51
+    %52:u32 = convert 2i
+    %53:u32 = convert 1i
+    %54:array<u32, 2> = construct %52, %53
+    %55:f32 = call %f0_W_arr_X_mat_X, %54
+    %56:f32 = load %res_1
+    %57:f32 = add %56, %55
+    store %res_1, %57
+    %58:u32 = convert 2i
+    %59:u32 = convert 1i
+    %60:array<u32, 2> = construct %58, %59
+    %61:f32 = call %f0_W_arr_X_mat_X, %60
+    %62:f32 = load %res_1
+    %63:f32 = add %62, %61
+    store %res_1, %63
+    %64:f32 = load %res_1
+    ret %64
+  }
+}
+%f2_W_arr_X = func(%p_indices_3:array<u32, 1>):f32 -> %b6 {  # %p_indices_3: 'p_indices'
+  %b6 = block {
+    %67:u32 = access %p_indices_3, 0u
+    %68:array<u32, 1> = construct %67
+    %69:f32 = call %f1_W_arr_X_mat, %68
+    ret %69
+  }
+}
+%f3_W_arr_W_mat = func():f32 -> %b7 {
+  %b7 = block {
+    %71:u32 = convert 3i
+    %72:array<u32, 1> = construct %71
+    %73:f32 = call %f2_W_arr_X, %72
+    %74:f32 = call %f1_W_mat
+    %75:f32 = add %73, %74
+    ret %75
+  }
+}
+%f4_W = func():f32 -> %b8 {
+  %b8 = block {
+    %77:f32 = call %f3_W_arr_W_mat
+    ret %77
+  }
+}
+%b = func():void -> %b9 {
+  %b9 = block {
+    %79:f32 = call %f4_W
+    ret
+  }
+}
+)";
+
+    Run(DirectVariableAccess, DirectVariableAccessOptions{});
+
+    EXPECT_EQ(expect, str());
+}
+
+}  // namespace workgroup_as_tests
+
+////////////////////////////////////////////////////////////////////////////////
+// 'private' address space
+////////////////////////////////////////////////////////////////////////////////
+namespace private_as_tests {
+
+using IR_DirectVariableAccessTest_PrivateAS = TransformTest;
+
+TEST_F(IR_DirectVariableAccessTest_PrivateAS, Enabled_Param_ptr_i32_read) {
+    Var* P = nullptr;
+    b.Append(b.ir.root_block,
+             [&] {  //
+                 P = b.Var("P", ty.ptr<private_, i32>());
+             });
+
+    auto* fn_a = b.Function("a", ty.i32());
+    auto* fn_a_p = b.FunctionParam("p", ty.ptr<private_, i32>());
+    fn_a->SetParams({
+        b.FunctionParam("pre", ty.i32()),
+        fn_a_p,
+        b.FunctionParam("post", ty.i32()),
+    });
+    b.Append(fn_a->Block(), [&] { b.Return(fn_a, b.Load(fn_a_p)); });
+
+    auto* fn_b = b.Function("b", ty.void_());
+    b.Append(fn_b->Block(), [&] {
+        b.Call(fn_a, 10_i, P, 20_i);
+        b.Return(fn_b);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %P:ptr<private, i32, read_write> = var
+}
+
+%a = func(%pre:i32, %p:ptr<private, i32, read_write>, %post:i32):i32 -> %b2 {
+  %b2 = block {
+    %6:i32 = load %p
+    ret %6
+  }
+}
+%b = func():void -> %b3 {
+  %b3 = block {
+    %8:i32 = call %a, 10i, %P, 20i
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %P:ptr<private, i32, read_write> = var
+}
+
+%a_P = func(%pre:i32, %post:i32):i32 -> %b2 {
+  %b2 = block {
+    %5:ptr<private, i32, read_write> = access %P
+    %6:i32 = load %5
+    ret %6
+  }
+}
+%b = func():void -> %b3 {
+  %b3 = block {
+    %8:i32 = call %a_P, 10i, 20i
+    ret
+  }
+}
+)";
+
+    Run(DirectVariableAccess, kTransformPrivate);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_PrivateAS, Enabled_Param_ptr_i32_write) {
+    Var* P = nullptr;
+    b.Append(b.ir.root_block,
+             [&] {  //
+                 P = b.Var("P", ty.ptr<private_, i32>());
+             });
+
+    auto* fn_a = b.Function("a", ty.void_());
+    auto* fn_a_p = b.FunctionParam("p", ty.ptr<private_, i32>());
+    fn_a->SetParams({
+        b.FunctionParam("pre", ty.i32()),
+        fn_a_p,
+        b.FunctionParam("post", ty.i32()),
+    });
+    b.Append(fn_a->Block(), [&] {
+        b.Store(fn_a_p, 42_i);
+        b.Return(fn_a);
+    });
+
+    auto* fn_b = b.Function("b", ty.void_());
+    b.Append(fn_b->Block(), [&] {
+        b.Call(fn_a, 10_i, P, 20_i);
+        b.Return(fn_b);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %P:ptr<private, i32, read_write> = var
+}
+
+%a = func(%pre:i32, %p:ptr<private, i32, read_write>, %post:i32):void -> %b2 {
+  %b2 = block {
+    store %p, 42i
+    ret
+  }
+}
+%b = func():void -> %b3 {
+  %b3 = block {
+    %7:void = call %a, 10i, %P, 20i
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %P:ptr<private, i32, read_write> = var
+}
+
+%a_P = func(%pre:i32, %post:i32):void -> %b2 {
+  %b2 = block {
+    %5:ptr<private, i32, read_write> = access %P
+    store %5, 42i
+    ret
+  }
+}
+%b = func():void -> %b3 {
+  %b3 = block {
+    %7:void = call %a_P, 10i, 20i
+    ret
+  }
+}
+)";
+
+    Run(DirectVariableAccess, kTransformPrivate);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_PrivateAS, Enabled_Param_ptr_i32_Via_struct_read) {
+    auto* str_ = ty.Struct(mod.symbols.New("str"), {
+                                                       {mod.symbols.Register("i"), ty.i32()},
+                                                   });
+
+    Var* P = nullptr;
+    b.Append(b.ir.root_block,
+             [&] {  //
+                 P = b.Var("P", ty.ptr<private_>(str_));
+             });
+
+    auto* fn_a = b.Function("a", ty.i32());
+    auto* fn_a_p = b.FunctionParam("p", ty.ptr<private_, i32>());
+    fn_a->SetParams({
+        b.FunctionParam("pre", ty.i32()),
+        fn_a_p,
+        b.FunctionParam("post", ty.i32()),
+    });
+    b.Append(fn_a->Block(), [&] { b.Return(fn_a, b.Load(fn_a_p)); });
+
+    auto* fn_b = b.Function("b", ty.void_());
+    b.Append(fn_b->Block(), [&] {
+        auto* access = b.Access(ty.ptr<private_, i32>(), P, 0_u);
+        b.Call(fn_a, 10_i, access, 20_i);
+        b.Return(fn_b);
+    });
+
+    auto* src = R"(
+str = struct @align(4) {
+  i:i32 @offset(0)
+}
+
+%b1 = block {  # root
+  %P:ptr<private, str, read_write> = var
+}
+
+%a = func(%pre:i32, %p:ptr<private, i32, read_write>, %post:i32):i32 -> %b2 {
+  %b2 = block {
+    %6:i32 = load %p
+    ret %6
+  }
+}
+%b = func():void -> %b3 {
+  %b3 = block {
+    %8:ptr<private, i32, read_write> = access %P, 0u
+    %9:i32 = call %a, 10i, %8, 20i
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+str = struct @align(4) {
+  i:i32 @offset(0)
+}
+
+%b1 = block {  # root
+  %P:ptr<private, str, read_write> = var
+}
+
+%a_P_i = func(%pre:i32, %post:i32):i32 -> %b2 {
+  %b2 = block {
+    %5:ptr<private, i32, read_write> = access %P, 0u
+    %6:i32 = load %5
+    ret %6
+  }
+}
+%b = func():void -> %b3 {
+  %b3 = block {
+    %8:i32 = call %a_P_i, 10i, 20i
+    ret
+  }
+}
+)";
+
+    Run(DirectVariableAccess, kTransformPrivate);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_PrivateAS, Disabled_Param_ptr_i32_Via_struct_read) {
+    auto* str_ = ty.Struct(mod.symbols.New("str"), {
+                                                       {mod.symbols.Register("i"), ty.i32()},
+                                                   });
+
+    Var* P = nullptr;
+    b.Append(b.ir.root_block,
+             [&] {  //
+                 P = b.Var("P", ty.ptr<private_>(str_));
+             });
+
+    auto* fn_a = b.Function("a", ty.i32());
+    auto* fn_a_p = b.FunctionParam("p", ty.ptr<private_, i32>());
+    fn_a->SetParams({
+        b.FunctionParam("pre", ty.i32()),
+        fn_a_p,
+        b.FunctionParam("post", ty.i32()),
+    });
+    b.Append(fn_a->Block(), [&] { b.Return(fn_a, b.Load(fn_a_p)); });
+
+    auto* fn_b = b.Function("b", ty.void_());
+    b.Append(fn_b->Block(), [&] {
+        auto* access = b.Access(ty.ptr<private_, i32>(), P, 0_u);
+        b.Call(fn_a, 10_i, access, 20_i);
+        b.Return(fn_b);
+    });
+
+    auto* src = R"(
+str = struct @align(4) {
+  i:i32 @offset(0)
+}
+
+%b1 = block {  # root
+  %P:ptr<private, str, read_write> = var
+}
+
+%a = func(%pre:i32, %p:ptr<private, i32, read_write>, %post:i32):i32 -> %b2 {
+  %b2 = block {
+    %6:i32 = load %p
+    ret %6
+  }
+}
+%b = func():void -> %b3 {
+  %b3 = block {
+    %8:ptr<private, i32, read_write> = access %P, 0u
+    %9:i32 = call %a, 10i, %8, 20i
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(DirectVariableAccess, DirectVariableAccessOptions{});
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_PrivateAS, Enabled_Param_ptr_arr_i32_Via_struct_write) {
+    auto* str_ =
+        ty.Struct(mod.symbols.New("str"), {
+                                              {mod.symbols.Register("arr"), ty.array<i32, 4>()},
+                                          });
+
+    Var* P = nullptr;
+    b.Append(b.ir.root_block,
+             [&] {  //
+                 P = b.Var("P", ty.ptr<private_>(str_));
+             });
+
+    auto* fn_a = b.Function("a", ty.void_());
+    auto* fn_a_p = b.FunctionParam("p", ty.ptr<private_, array<i32, 4>>());
+    fn_a->SetParams({
+        b.FunctionParam("pre", ty.i32()),
+        fn_a_p,
+        b.FunctionParam("post", ty.i32()),
+    });
+    b.Append(fn_a->Block(), [&] {
+        b.Store(fn_a_p, b.Splat(ty.array<i32, 4>(), 0_i, 4));
+        b.Return(fn_a);
+    });
+
+    auto* fn_b = b.Function("b", ty.void_());
+    b.Append(fn_b->Block(), [&] {
+        auto* access = b.Access(ty.ptr<private_, array<i32, 4>>(), P, 0_u);
+        b.Call(fn_a, 10_i, access, 20_i);
+        b.Return(fn_b);
+    });
+
+    auto* src = R"(
+str = struct @align(4) {
+  arr:array<i32, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %P:ptr<private, str, read_write> = var
+}
+
+%a = func(%pre:i32, %p:ptr<private, array<i32, 4>, read_write>, %post:i32):void -> %b2 {
+  %b2 = block {
+    store %p, array<i32, 4>(0i)
+    ret
+  }
+}
+%b = func():void -> %b3 {
+  %b3 = block {
+    %7:ptr<private, array<i32, 4>, read_write> = access %P, 0u
+    %8:void = call %a, 10i, %7, 20i
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+str = struct @align(4) {
+  arr:array<i32, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %P:ptr<private, str, read_write> = var
+}
+
+%a_P_arr = func(%pre:i32, %post:i32):void -> %b2 {
+  %b2 = block {
+    %5:ptr<private, array<i32, 4>, read_write> = access %P, 0u
+    store %5, array<i32, 4>(0i)
+    ret
+  }
+}
+%b = func():void -> %b3 {
+  %b3 = block {
+    %7:void = call %a_P_arr, 10i, 20i
+    ret
+  }
+}
+)";
+
+    Run(DirectVariableAccess, kTransformPrivate);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_PrivateAS, Disabled_Param_ptr_arr_i32_Via_struct_write) {
+    auto* str_ =
+        ty.Struct(mod.symbols.New("str"), {
+                                              {mod.symbols.Register("arr"), ty.array<i32, 4>()},
+                                          });
+
+    Var* P = nullptr;
+    b.Append(b.ir.root_block,
+             [&] {  //
+                 P = b.Var("P", ty.ptr<private_>(str_));
+             });
+
+    auto* fn_a = b.Function("a", ty.void_());
+    auto* fn_a_p = b.FunctionParam("p", ty.ptr<private_, array<i32, 4>>());
+    fn_a->SetParams({
+        b.FunctionParam("pre", ty.i32()),
+        fn_a_p,
+        b.FunctionParam("post", ty.i32()),
+    });
+    b.Append(fn_a->Block(), [&] {
+        b.Store(fn_a_p, b.Splat(ty.array<i32, 4>(), 0_i, 4));
+        b.Return(fn_a);
+    });
+
+    auto* fn_b = b.Function("b", ty.void_());
+    b.Append(fn_b->Block(), [&] {
+        auto* access = b.Access(ty.ptr<private_, array<i32, 4>>(), P, 0_u);
+        b.Call(fn_a, 10_i, access, 20_i);
+        b.Return(fn_b);
+    });
+
+    auto* src = R"(
+str = struct @align(4) {
+  arr:array<i32, 4> @offset(0)
+}
+
+%b1 = block {  # root
+  %P:ptr<private, str, read_write> = var
+}
+
+%a = func(%pre:i32, %p:ptr<private, array<i32, 4>, read_write>, %post:i32):void -> %b2 {
+  %b2 = block {
+    store %p, array<i32, 4>(0i)
+    ret
+  }
+}
+%b = func():void -> %b3 {
+  %b3 = block {
+    %7:ptr<private, array<i32, 4>, read_write> = access %P, 0u
+    %8:void = call %a, 10i, %7, 20i
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(DirectVariableAccess, DirectVariableAccessOptions{});
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_PrivateAS, Enabled_Param_ptr_i32_mixed) {
+    auto* str_ = ty.Struct(mod.symbols.New("str"), {
+                                                       {mod.symbols.Register("i"), ty.i32()},
+                                                   });
+
+    Var* Pi = nullptr;
+    Var* Ps = nullptr;
+    Var* Pa = nullptr;
+    b.Append(b.ir.root_block,
+             [&] {  //
+                 Pi = b.Var("Pi", ty.ptr<private_, i32>());
+                 Ps = b.Var("Ps", ty.ptr<private_>(str_));
+                 Pa = b.Var("Pa", ty.ptr<private_, array<i32, 4>>());
+             });
+
+    auto* fn_a = b.Function("a", ty.i32());
+    auto* fn_a_p = b.FunctionParam("p", ty.ptr<private_, i32>());
+    fn_a->SetParams({
+        b.FunctionParam("pre", ty.i32()),
+        fn_a_p,
+        b.FunctionParam("post", ty.i32()),
+    });
+    b.Append(fn_a->Block(), [&] { b.Return(fn_a, b.Load(fn_a_p)); });
+
+    auto* fn_b = b.Function("b", ty.void_());
+    b.Append(fn_b->Block(), [&] {
+        {  // a(10, &Pi, 20);
+            b.Call(fn_a, 10_i, Pi, 20_i);
+        }
+        {  // a(30, &Ps.i, 40);
+            auto* access = b.Access(ty.ptr<private_, i32>(), Ps, 0_u);
+            b.Call(fn_a, 30_i, access, 40_i);
+        }
+        {  // a(50, &Pa[2], 60);
+            auto* access = b.Access(ty.ptr<private_, i32>(), Pa, 2_i);
+            b.Call(fn_a, 50_i, access, 60_i);
+        }
+        b.Return(fn_b);
+    });
+
+    auto* src = R"(
+str = struct @align(4) {
+  i:i32 @offset(0)
+}
+
+%b1 = block {  # root
+  %Pi:ptr<private, i32, read_write> = var
+  %Ps:ptr<private, str, read_write> = var
+  %Pa:ptr<private, array<i32, 4>, read_write> = var
+}
+
+%a = func(%pre:i32, %p:ptr<private, i32, read_write>, %post:i32):i32 -> %b2 {
+  %b2 = block {
+    %8:i32 = load %p
+    ret %8
+  }
+}
+%b = func():void -> %b3 {
+  %b3 = block {
+    %10:i32 = call %a, 10i, %Pi, 20i
+    %11:ptr<private, i32, read_write> = access %Ps, 0u
+    %12:i32 = call %a, 30i, %11, 40i
+    %13:ptr<private, i32, read_write> = access %Pa, 2i
+    %14:i32 = call %a, 50i, %13, 60i
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+str = struct @align(4) {
+  i:i32 @offset(0)
+}
+
+%b1 = block {  # root
+  %Pi:ptr<private, i32, read_write> = var
+  %Ps:ptr<private, str, read_write> = var
+  %Pa:ptr<private, array<i32, 4>, read_write> = var
+}
+
+%a_Pi = func(%pre:i32, %post:i32):i32 -> %b2 {
+  %b2 = block {
+    %7:ptr<private, i32, read_write> = access %Pi
+    %8:i32 = load %7
+    ret %8
+  }
+}
+%a_Ps_i = func(%pre_1:i32, %post_1:i32):i32 -> %b3 {  # %pre_1: 'pre', %post_1: 'post'
+  %b3 = block {
+    %12:ptr<private, i32, read_write> = access %Ps, 0u
+    %13:i32 = load %12
+    ret %13
+  }
+}
+%a_Pa_X = func(%pre_2:i32, %p_indices:array<u32, 1>, %post_2:i32):i32 -> %b4 {  # %pre_2: 'pre', %post_2: 'post'
+  %b4 = block {
+    %18:u32 = access %p_indices, 0u
+    %19:ptr<private, i32, read_write> = access %Pa, %18
+    %20:i32 = load %19
+    ret %20
+  }
+}
+%b = func():void -> %b5 {
+  %b5 = block {
+    %22:i32 = call %a_Pi, 10i, 20i
+    %23:i32 = call %a_Ps_i, 30i, 40i
+    %24:u32 = convert 2i
+    %25:array<u32, 1> = construct %24
+    %26:i32 = call %a_Pa_X, 50i, %25, 60i
+    ret
+  }
+}
+)";
+
+    Run(DirectVariableAccess, kTransformPrivate);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_PrivateAS, Disabled_Param_ptr_i32_mixed) {
+    auto* str_ = ty.Struct(mod.symbols.New("str"), {
+                                                       {mod.symbols.Register("i"), ty.i32()},
+                                                   });
+
+    Var* Pi = nullptr;
+    Var* Ps = nullptr;
+    Var* Pa = nullptr;
+    b.Append(b.ir.root_block,
+             [&] {  //
+                 Pi = b.Var("Pi", ty.ptr<private_, i32>());
+                 Ps = b.Var("Ps", ty.ptr<private_>(str_));
+                 Pa = b.Var("Pa", ty.ptr<private_, array<i32, 4>>());
+             });
+
+    auto* fn_a = b.Function("a", ty.i32());
+    auto* fn_a_p = b.FunctionParam("p", ty.ptr<private_, i32>());
+    fn_a->SetParams({
+        b.FunctionParam("pre", ty.i32()),
+        fn_a_p,
+        b.FunctionParam("post", ty.i32()),
+    });
+    b.Append(fn_a->Block(), [&] { b.Return(fn_a, b.Load(fn_a_p)); });
+
+    auto* fn_b = b.Function("b", ty.void_());
+    b.Append(fn_b->Block(), [&] {
+        {  // a(10, &Pi, 20);
+            b.Call(fn_a, 10_i, Pi, 20_i);
+        }
+        {  // a(30, &Ps.i, 40);
+            auto* access = b.Access(ty.ptr<private_, i32>(), Ps, 0_u);
+            b.Call(fn_a, 30_i, access, 40_i);
+        }
+        {  // a(50, &Pa[2], 60);
+            auto* access = b.Access(ty.ptr<private_, i32>(), Pa, 2_i);
+            b.Call(fn_a, 50_i, access, 60_i);
+        }
+        b.Return(fn_b);
+    });
+
+    auto* src = R"(
+str = struct @align(4) {
+  i:i32 @offset(0)
+}
+
+%b1 = block {  # root
+  %Pi:ptr<private, i32, read_write> = var
+  %Ps:ptr<private, str, read_write> = var
+  %Pa:ptr<private, array<i32, 4>, read_write> = var
+}
+
+%a = func(%pre:i32, %p:ptr<private, i32, read_write>, %post:i32):i32 -> %b2 {
+  %b2 = block {
+    %8:i32 = load %p
+    ret %8
+  }
+}
+%b = func():void -> %b3 {
+  %b3 = block {
+    %10:i32 = call %a, 10i, %Pi, 20i
+    %11:ptr<private, i32, read_write> = access %Ps, 0u
+    %12:i32 = call %a, 30i, %11, 40i
+    %13:ptr<private, i32, read_write> = access %Pa, 2i
+    %14:i32 = call %a, 50i, %13, 60i
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(DirectVariableAccess, DirectVariableAccessOptions{});
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_PrivateAS, 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>()},
+                                            });
+    Var* P = nullptr;
+    b.Append(b.ir.root_block,
+             [&] {  //
+                 P = b.Var("P", ty.ptr<private_>(Outer));
+             });
+
+    auto* fn_0 = b.Function("f0", ty.f32());
+    auto* fn_0_p = b.FunctionParam("p", ty.ptr<private_, vec4<f32>>());
+    fn_0->SetParams({fn_0_p});
+    b.Append(fn_0->Block(), [&] { b.Return(fn_0, b.LoadVectorElement(fn_0_p, 0_u)); });
+
+    auto* fn_1 = b.Function("f1", ty.f32());
+    auto* fn_1_p = b.FunctionParam("p", ty.ptr<private_, mat3x4<f32>>());
+    fn_1->SetParams({fn_1_p});
+    b.Append(fn_1->Block(), [&] {
+        auto* res = b.Var<function, f32>("res");
+        {
+            // res += f0(&(*p)[1]);
+            auto* call_0 = b.Call(fn_0, b.Access(ty.ptr<private_, vec4<f32>>(), fn_1_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<private_, vec4<f32>>(), fn_1_p, 1_i);
+            b.ir.SetName(p_vec, "p_vec");
+            auto* call_0 = b.Call(fn_0, p_vec);
+            b.Store(res, b.Add(ty.f32(), b.Load(res), call_0));
+        }
+        {
+            // res += f0(&U.arr[2].mat[1]);
+            auto* access = b.Access(ty.ptr<private_, vec4<f32>>(), P, 0_u, 2_i, 0_u, 1_i);
+            auto* call_0 = b.Call(fn_0, access);
+            b.Store(res, b.Add(ty.f32(), b.Load(res), call_0));
+        }
+        {
+            // let p_vec = &U.arr[2].mat[1];
+            // res += f0(p_vec);
+            auto* p_vec = b.Access(ty.ptr<private_, vec4<f32>>(), P, 0_u, 2_i, 0_u, 1_i);
+            b.ir.SetName(p_vec, "p_vec");
+            auto* call_0 = b.Call(fn_0, p_vec);
+            b.Store(res, b.Add(ty.f32(), b.Load(res), call_0));
+        }
+
+        b.Return(fn_1, b.Load(res));
+    });
+
+    auto* fn_2 = b.Function("f2", ty.f32());
+    auto* fn_2_p = b.FunctionParam("p", ty.ptr<private_>(Inner));
+    fn_2->SetParams({fn_2_p});
+    b.Append(fn_2->Block(), [&] {
+        auto* p_mat = b.Access(ty.ptr<private_, mat3x4<f32>>(), fn_2_p, 0_u);
+        b.ir.SetName(p_mat, "p_mat");
+        b.Return(fn_2, b.Call(fn_1, p_mat));
+    });
+
+    auto* fn_3 = b.Function("f3", ty.f32());
+    auto* fn_3_p0 = b.FunctionParam("p0", ty.ptr<private_>(ty.array(Inner, 4)));
+    auto* fn_3_p1 = b.FunctionParam("p1", ty.ptr<private_, mat3x4<f32>>());
+    fn_3->SetParams({fn_3_p0, fn_3_p1});
+    b.Append(fn_3->Block(), [&] {
+        auto* p0_inner = b.Access(ty.ptr<private_>(Inner), fn_3_p0, 3_i);
+        b.ir.SetName(p0_inner, "p0_inner");
+        auto* call_0 = b.Call(ty.f32(), fn_2, p0_inner);
+        auto* call_1 = b.Call(ty.f32(), fn_1, fn_3_p1);
+        b.Return(fn_3, b.Add(ty.f32(), call_0, call_1));
+    });
+
+    auto* fn_4 = b.Function("f4", ty.f32());
+    auto* fn_4_p = b.FunctionParam("p", ty.ptr<private_>(Outer));
+    fn_4->SetParams({fn_4_p});
+    b.Append(fn_4->Block(), [&] {
+        auto* access_0 = b.Access(ty.ptr<private_>(ty.array(Inner, 4)), fn_4_p, 0_u);
+        auto* access_1 = b.Access(ty.ptr<private_, mat3x4<f32>>(), P, 1_u);
+        b.Return(fn_4, b.Call(ty.f32(), fn_3, access_0, access_1));
+    });
+
+    auto* fn_b = b.Function("b", ty.void_());
+    b.Append(fn_b->Block(), [&] {
+        b.Call(ty.f32(), fn_4, P);
+        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)
+}
+
+%b1 = block {  # root
+  %P:ptr<private, Outer, read_write> = var
+}
+
+%f0 = func(%p:ptr<private, vec4<f32>, read_write>):f32 -> %b2 {
+  %b2 = block {
+    %4:f32 = load_vector_element %p, 0u
+    ret %4
+  }
+}
+%f1 = func(%p_1:ptr<private, mat3x4<f32>, read_write>):f32 -> %b3 {  # %p_1: 'p'
+  %b3 = block {
+    %res:ptr<function, f32, read_write> = var
+    %8:ptr<private, vec4<f32>, read_write> = access %p_1, 1i
+    %9:f32 = call %f0, %8
+    %10:f32 = load %res
+    %11:f32 = add %10, %9
+    store %res, %11
+    %p_vec:ptr<private, vec4<f32>, read_write> = access %p_1, 1i
+    %13:f32 = call %f0, %p_vec
+    %14:f32 = load %res
+    %15:f32 = add %14, %13
+    store %res, %15
+    %16:ptr<private, vec4<f32>, read_write> = access %P, 0u, 2i, 0u, 1i
+    %17:f32 = call %f0, %16
+    %18:f32 = load %res
+    %19:f32 = add %18, %17
+    store %res, %19
+    %p_vec_1:ptr<private, vec4<f32>, read_write> = access %P, 0u, 2i, 0u, 1i  # %p_vec_1: 'p_vec'
+    %21:f32 = call %f0, %p_vec_1
+    %22:f32 = load %res
+    %23:f32 = add %22, %21
+    store %res, %23
+    %24:f32 = load %res
+    ret %24
+  }
+}
+%f2 = func(%p_2:ptr<private, Inner, read_write>):f32 -> %b4 {  # %p_2: 'p'
+  %b4 = block {
+    %p_mat:ptr<private, mat3x4<f32>, read_write> = access %p_2, 0u
+    %28:f32 = call %f1, %p_mat
+    ret %28
+  }
+}
+%f3 = func(%p0:ptr<private, array<Inner, 4>, read_write>, %p1:ptr<private, mat3x4<f32>, read_write>):f32 -> %b5 {
+  %b5 = block {
+    %p0_inner:ptr<private, Inner, read_write> = access %p0, 3i
+    %33:f32 = call %f2, %p0_inner
+    %34:f32 = call %f1, %p1
+    %35:f32 = add %33, %34
+    ret %35
+  }
+}
+%f4 = func(%p_3:ptr<private, Outer, read_write>):f32 -> %b6 {  # %p_3: 'p'
+  %b6 = block {
+    %38:ptr<private, array<Inner, 4>, read_write> = access %p_3, 0u
+    %39:ptr<private, mat3x4<f32>, read_write> = access %P, 1u
+    %40:f32 = call %f3, %38, %39
+    ret %40
+  }
+}
+%b = func():void -> %b7 {
+  %b7 = block {
+    %42:f32 = call %f4, %P
+    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)
+}
+
+%b1 = block {  # root
+  %P:ptr<private, Outer, read_write> = var
+}
+
+%f0_P_mat_X = func(%p_indices:array<u32, 1>):f32 -> %b2 {
+  %b2 = block {
+    %4:u32 = access %p_indices, 0u
+    %5:ptr<private, vec4<f32>, read_write> = access %P, 1u, %4
+    %6:f32 = load_vector_element %5, 0u
+    ret %6
+  }
+}
+%f0_P_arr_X_mat_X = func(%p_indices_1:array<u32, 2>):f32 -> %b3 {  # %p_indices_1: 'p_indices'
+  %b3 = block {
+    %9:u32 = access %p_indices_1, 0u
+    %10:u32 = access %p_indices_1, 1u
+    %11:ptr<private, vec4<f32>, read_write> = access %P, 0u, %9, 0u, %10
+    %12:f32 = load_vector_element %11, 0u
+    ret %12
+  }
+}
+%f1_P_mat = func():f32 -> %b4 {
+  %b4 = block {
+    %res:ptr<function, f32, read_write> = var
+    %15:u32 = convert 1i
+    %16:array<u32, 1> = construct %15
+    %17:f32 = call %f0_P_mat_X, %16
+    %18:f32 = load %res
+    %19:f32 = add %18, %17
+    store %res, %19
+    %20:u32 = convert 1i
+    %21:array<u32, 1> = construct %20
+    %22:f32 = call %f0_P_mat_X, %21
+    %23:f32 = load %res
+    %24:f32 = add %23, %22
+    store %res, %24
+    %25:u32 = convert 2i
+    %26:u32 = convert 1i
+    %27:array<u32, 2> = construct %25, %26
+    %28:f32 = call %f0_P_arr_X_mat_X, %27
+    %29:f32 = load %res
+    %30:f32 = add %29, %28
+    store %res, %30
+    %31:u32 = convert 2i
+    %32:u32 = convert 1i
+    %33:array<u32, 2> = construct %31, %32
+    %34:f32 = call %f0_P_arr_X_mat_X, %33
+    %35:f32 = load %res
+    %36:f32 = add %35, %34
+    store %res, %36
+    %37:f32 = load %res
+    ret %37
+  }
+}
+%f1_P_arr_X_mat = func(%p_indices_2:array<u32, 1>):f32 -> %b5 {  # %p_indices_2: 'p_indices'
+  %b5 = block {
+    %40:u32 = access %p_indices_2, 0u
+    %res_1:ptr<function, f32, read_write> = var  # %res_1: 'res'
+    %42:u32 = convert 1i
+    %43:array<u32, 2> = construct %40, %42
+    %44:f32 = call %f0_P_arr_X_mat_X, %43
+    %45:f32 = load %res_1
+    %46:f32 = add %45, %44
+    store %res_1, %46
+    %47:u32 = convert 1i
+    %48:array<u32, 2> = construct %40, %47
+    %49:f32 = call %f0_P_arr_X_mat_X, %48
+    %50:f32 = load %res_1
+    %51:f32 = add %50, %49
+    store %res_1, %51
+    %52:u32 = convert 2i
+    %53:u32 = convert 1i
+    %54:array<u32, 2> = construct %52, %53
+    %55:f32 = call %f0_P_arr_X_mat_X, %54
+    %56:f32 = load %res_1
+    %57:f32 = add %56, %55
+    store %res_1, %57
+    %58:u32 = convert 2i
+    %59:u32 = convert 1i
+    %60:array<u32, 2> = construct %58, %59
+    %61:f32 = call %f0_P_arr_X_mat_X, %60
+    %62:f32 = load %res_1
+    %63:f32 = add %62, %61
+    store %res_1, %63
+    %64:f32 = load %res_1
+    ret %64
+  }
+}
+%f2_P_arr_X = func(%p_indices_3:array<u32, 1>):f32 -> %b6 {  # %p_indices_3: 'p_indices'
+  %b6 = block {
+    %67:u32 = access %p_indices_3, 0u
+    %68:array<u32, 1> = construct %67
+    %69:f32 = call %f1_P_arr_X_mat, %68
+    ret %69
+  }
+}
+%f3_P_arr_P_mat = func():f32 -> %b7 {
+  %b7 = block {
+    %71:u32 = convert 3i
+    %72:array<u32, 1> = construct %71
+    %73:f32 = call %f2_P_arr_X, %72
+    %74:f32 = call %f1_P_mat
+    %75:f32 = add %73, %74
+    ret %75
+  }
+}
+%f4_P = func():f32 -> %b8 {
+  %b8 = block {
+    %77:f32 = call %f3_P_arr_P_mat
+    ret %77
+  }
+}
+%b = func():void -> %b9 {
+  %b9 = block {
+    %79:f32 = call %f4_P
+    ret
+  }
+}
+)";
+
+    Run(DirectVariableAccess, kTransformPrivate);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_PrivateAS, 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>()},
+                                            });
+    Var* P = nullptr;
+    b.Append(b.ir.root_block,
+             [&] {  //
+                 P = b.Var("P", ty.ptr<private_>(Outer));
+             });
+
+    auto* fn_0 = b.Function("f0", ty.f32());
+    auto* fn_0_p = b.FunctionParam("p", ty.ptr<private_, vec4<f32>>());
+    fn_0->SetParams({fn_0_p});
+    b.Append(fn_0->Block(), [&] { b.Return(fn_0, b.LoadVectorElement(fn_0_p, 0_u)); });
+
+    auto* fn_1 = b.Function("f1", ty.f32());
+    auto* fn_1_p = b.FunctionParam("p", ty.ptr<private_, mat3x4<f32>>());
+    fn_1->SetParams({fn_1_p});
+    b.Append(fn_1->Block(), [&] {
+        auto* res = b.Var<function, f32>("res");
+        {
+            // res += f0(&(*p)[1]);
+            auto* call_0 = b.Call(fn_0, b.Access(ty.ptr<private_, vec4<f32>>(), fn_1_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<private_, vec4<f32>>(), fn_1_p, 1_i);
+            b.ir.SetName(p_vec, "p_vec");
+            auto* call_0 = b.Call(fn_0, p_vec);
+            b.Store(res, b.Add(ty.f32(), b.Load(res), call_0));
+        }
+        {
+            // res += f0(&U.arr[2].mat[1]);
+            auto* access = b.Access(ty.ptr<private_, vec4<f32>>(), P, 0_u, 2_i, 0_u, 1_i);
+            auto* call_0 = b.Call(fn_0, access);
+            b.Store(res, b.Add(ty.f32(), b.Load(res), call_0));
+        }
+        {
+            // let p_vec = &U.arr[2].mat[1];
+            // res += f0(p_vec);
+            auto* p_vec = b.Access(ty.ptr<private_, vec4<f32>>(), P, 0_u, 2_i, 0_u, 1_i);
+            b.ir.SetName(p_vec, "p_vec");
+            auto* call_0 = b.Call(fn_0, p_vec);
+            b.Store(res, b.Add(ty.f32(), b.Load(res), call_0));
+        }
+
+        b.Return(fn_1, b.Load(res));
+    });
+
+    auto* fn_2 = b.Function("f2", ty.f32());
+    auto* fn_2_p = b.FunctionParam("p", ty.ptr<private_>(Inner));
+    fn_2->SetParams({fn_2_p});
+    b.Append(fn_2->Block(), [&] {
+        auto* p_mat = b.Access(ty.ptr<private_, mat3x4<f32>>(), fn_2_p, 0_u);
+        b.ir.SetName(p_mat, "p_mat");
+        b.Return(fn_2, b.Call(fn_1, p_mat));
+    });
+
+    auto* fn_3 = b.Function("f3", ty.f32());
+    auto* fn_3_p0 = b.FunctionParam("p0", ty.ptr<private_>(ty.array(Inner, 4)));
+    auto* fn_3_p1 = b.FunctionParam("p1", ty.ptr<private_, mat3x4<f32>>());
+    fn_3->SetParams({fn_3_p0, fn_3_p1});
+    b.Append(fn_3->Block(), [&] {
+        auto* p0_inner = b.Access(ty.ptr<private_>(Inner), fn_3_p0, 3_i);
+        b.ir.SetName(p0_inner, "p0_inner");
+        auto* call_0 = b.Call(ty.f32(), fn_2, p0_inner);
+        auto* call_1 = b.Call(ty.f32(), fn_1, fn_3_p1);
+        b.Return(fn_3, b.Add(ty.f32(), call_0, call_1));
+    });
+
+    auto* fn_4 = b.Function("f4", ty.f32());
+    auto* fn_4_p = b.FunctionParam("p", ty.ptr<private_>(Outer));
+    fn_4->SetParams({fn_4_p});
+    b.Append(fn_4->Block(), [&] {
+        auto* access_0 = b.Access(ty.ptr<private_>(ty.array(Inner, 4)), fn_4_p, 0_u);
+        auto* access_1 = b.Access(ty.ptr<private_, mat3x4<f32>>(), P, 1_u);
+        b.Return(fn_4, b.Call(ty.f32(), fn_3, access_0, access_1));
+    });
+
+    auto* fn_b = b.Function("b", ty.void_());
+    b.Append(fn_b->Block(), [&] {
+        b.Call(ty.f32(), fn_4, P);
+        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)
+}
+
+%b1 = block {  # root
+  %P:ptr<private, Outer, read_write> = var
+}
+
+%f0 = func(%p:ptr<private, vec4<f32>, read_write>):f32 -> %b2 {
+  %b2 = block {
+    %4:f32 = load_vector_element %p, 0u
+    ret %4
+  }
+}
+%f1 = func(%p_1:ptr<private, mat3x4<f32>, read_write>):f32 -> %b3 {  # %p_1: 'p'
+  %b3 = block {
+    %res:ptr<function, f32, read_write> = var
+    %8:ptr<private, vec4<f32>, read_write> = access %p_1, 1i
+    %9:f32 = call %f0, %8
+    %10:f32 = load %res
+    %11:f32 = add %10, %9
+    store %res, %11
+    %p_vec:ptr<private, vec4<f32>, read_write> = access %p_1, 1i
+    %13:f32 = call %f0, %p_vec
+    %14:f32 = load %res
+    %15:f32 = add %14, %13
+    store %res, %15
+    %16:ptr<private, vec4<f32>, read_write> = access %P, 0u, 2i, 0u, 1i
+    %17:f32 = call %f0, %16
+    %18:f32 = load %res
+    %19:f32 = add %18, %17
+    store %res, %19
+    %p_vec_1:ptr<private, vec4<f32>, read_write> = access %P, 0u, 2i, 0u, 1i  # %p_vec_1: 'p_vec'
+    %21:f32 = call %f0, %p_vec_1
+    %22:f32 = load %res
+    %23:f32 = add %22, %21
+    store %res, %23
+    %24:f32 = load %res
+    ret %24
+  }
+}
+%f2 = func(%p_2:ptr<private, Inner, read_write>):f32 -> %b4 {  # %p_2: 'p'
+  %b4 = block {
+    %p_mat:ptr<private, mat3x4<f32>, read_write> = access %p_2, 0u
+    %28:f32 = call %f1, %p_mat
+    ret %28
+  }
+}
+%f3 = func(%p0:ptr<private, array<Inner, 4>, read_write>, %p1:ptr<private, mat3x4<f32>, read_write>):f32 -> %b5 {
+  %b5 = block {
+    %p0_inner:ptr<private, Inner, read_write> = access %p0, 3i
+    %33:f32 = call %f2, %p0_inner
+    %34:f32 = call %f1, %p1
+    %35:f32 = add %33, %34
+    ret %35
+  }
+}
+%f4 = func(%p_3:ptr<private, Outer, read_write>):f32 -> %b6 {  # %p_3: 'p'
+  %b6 = block {
+    %38:ptr<private, array<Inner, 4>, read_write> = access %p_3, 0u
+    %39:ptr<private, mat3x4<f32>, read_write> = access %P, 1u
+    %40:f32 = call %f3, %38, %39
+    ret %40
+  }
+}
+%b = func():void -> %b7 {
+  %b7 = block {
+    %42:f32 = call %f4, %P
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(DirectVariableAccess, DirectVariableAccessOptions{});
+
+    EXPECT_EQ(expect, str());
+}
+
+}  // namespace private_as_tests
+
+////////////////////////////////////////////////////////////////////////////////
+// 'function' address space
+////////////////////////////////////////////////////////////////////////////////
+namespace function_as_tests {
+
+using IR_DirectVariableAccessTest_FunctionAS = TransformTest;
+
+TEST_F(IR_DirectVariableAccessTest_FunctionAS, Enabled_LocalPtr) {
+    auto* fn = b.Function("f", ty.void_());
+    b.Append(fn->Block(), [&] {
+        auto* v = b.Var<function, i32>("v");
+        auto* p = b.Let("p", v);
+        b.Var<function>("x", b.Load(p));
+        b.Return(fn);
+    });
+
+    auto* src = R"(
+%f = func():void -> %b1 {
+  %b1 = block {
+    %v:ptr<function, i32, read_write> = var
+    %p:ptr<function, i32, read_write> = let %v
+    %4:i32 = load %p
+    %x:ptr<function, i32, read_write> = var, %4
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;  // Nothing changes
+
+    Run(DirectVariableAccess, DirectVariableAccessOptions{});
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_FunctionAS, Enabled_Param_ptr_i32_read) {
+    auto* fn_a = b.Function("a", ty.i32());
+    auto* fn_a_p = b.FunctionParam("p", ty.ptr<function, i32>());
+    fn_a->SetParams({
+        b.FunctionParam("pre", ty.i32()),
+        fn_a_p,
+        b.FunctionParam("post", ty.i32()),
+    });
+    b.Append(fn_a->Block(), [&] { b.Return(fn_a, b.Load(fn_a_p)); });
+
+    auto* fn_b = b.Function("b", ty.void_());
+    b.Append(fn_b->Block(), [&] {
+        auto* F = b.Var<function, i32>("F");
+        b.Call(fn_a, 10_i, F, 20_i);
+        b.Return(fn_b);
+    });
+
+    auto* src = R"(
+%a = func(%pre:i32, %p:ptr<function, i32, read_write>, %post:i32):i32 -> %b1 {
+  %b1 = block {
+    %5:i32 = load %p
+    ret %5
+  }
+}
+%b = func():void -> %b2 {
+  %b2 = block {
+    %F:ptr<function, i32, read_write> = var
+    %8:i32 = call %a, 10i, %F, 20i
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%a_P = func(%pre:i32, %p_root:ptr<function, i32, read_write>, %post:i32):i32 -> %b1 {
+  %b1 = block {
+    %5:ptr<function, i32, read_write> = access %p_root
+    %6:i32 = load %5
+    ret %6
+  }
+}
+%b = func():void -> %b2 {
+  %b2 = block {
+    %F:ptr<function, i32, read_write> = var
+    %9:i32 = call %a_P, 10i, %F, 20i
+    ret
+  }
+}
+)";
+
+    Run(DirectVariableAccess, kTransformFunction);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_FunctionAS, Enabled_Param_ptr_i32_write) {
+    auto* fn_a = b.Function("a", ty.void_());
+    auto* fn_a_p = b.FunctionParam("p", ty.ptr<function, i32>());
+    fn_a->SetParams({
+        b.FunctionParam("pre", ty.i32()),
+        fn_a_p,
+        b.FunctionParam("post", ty.i32()),
+    });
+    b.Append(fn_a->Block(), [&] {
+        b.Store(fn_a_p, 42_i);
+        b.Return(fn_a);
+    });
+
+    auto* fn_b = b.Function("b", ty.void_());
+    b.Append(fn_b->Block(), [&] {
+        auto* F = b.Var<function, i32>("F");
+        b.Call(fn_a, 10_i, F, 20_i);
+        b.Return(fn_b);
+    });
+
+    auto* src = R"(
+%a = func(%pre:i32, %p:ptr<function, i32, read_write>, %post:i32):void -> %b1 {
+  %b1 = block {
+    store %p, 42i
+    ret
+  }
+}
+%b = func():void -> %b2 {
+  %b2 = block {
+    %F:ptr<function, i32, read_write> = var
+    %7:void = call %a, 10i, %F, 20i
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%a_P = func(%pre:i32, %p_root:ptr<function, i32, read_write>, %post:i32):void -> %b1 {
+  %b1 = block {
+    %5:ptr<function, i32, read_write> = access %p_root
+    store %5, 42i
+    ret
+  }
+}
+%b = func():void -> %b2 {
+  %b2 = block {
+    %F:ptr<function, i32, read_write> = var
+    %8:void = call %a_P, 10i, %F, 20i
+    ret
+  }
+}
+)";
+
+    Run(DirectVariableAccess, kTransformFunction);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_FunctionAS, Enabled_Param_ptr_i32_Via_struct_read) {
+    auto* str_ = ty.Struct(mod.symbols.New("str"), {
+                                                       {mod.symbols.Register("i"), ty.i32()},
+                                                   });
+
+    auto* fn_a = b.Function("a", ty.i32());
+    auto* fn_a_p = b.FunctionParam("p", ty.ptr<function, i32>());
+    fn_a->SetParams({
+        b.FunctionParam("pre", ty.i32()),
+        fn_a_p,
+        b.FunctionParam("post", ty.i32()),
+    });
+    b.Append(fn_a->Block(), [&] { b.Return(fn_a, b.Load(fn_a_p)); });
+
+    auto* fn_b = b.Function("b", ty.void_());
+    b.Append(fn_b->Block(), [&] {
+        auto* F = b.Var("F", ty.ptr<function>(str_));
+        auto* access = b.Access(ty.ptr<function, i32>(), F, 0_u);
+        b.Call(fn_a, 10_i, access, 20_i);
+        b.Return(fn_b);
+    });
+
+    auto* src = R"(
+str = struct @align(4) {
+  i:i32 @offset(0)
+}
+
+%a = func(%pre:i32, %p:ptr<function, i32, read_write>, %post:i32):i32 -> %b1 {
+  %b1 = block {
+    %5:i32 = load %p
+    ret %5
+  }
+}
+%b = func():void -> %b2 {
+  %b2 = block {
+    %F:ptr<function, str, read_write> = var
+    %8:ptr<function, i32, read_write> = access %F, 0u
+    %9:i32 = call %a, 10i, %8, 20i
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+str = struct @align(4) {
+  i:i32 @offset(0)
+}
+
+%a_P_i = func(%pre:i32, %p_root:ptr<function, str, read_write>, %post:i32):i32 -> %b1 {
+  %b1 = block {
+    %5:ptr<function, i32, read_write> = access %p_root, 0u
+    %6:i32 = load %5
+    ret %6
+  }
+}
+%b = func():void -> %b2 {
+  %b2 = block {
+    %F:ptr<function, str, read_write> = var
+    %9:i32 = call %a_P_i, 10i, %F, 20i
+    ret
+  }
+}
+)";
+
+    Run(DirectVariableAccess, kTransformFunction);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_FunctionAS, Enabled_Param_ptr_arr_i32_Via_struct_write) {
+    auto* str_ =
+        ty.Struct(mod.symbols.New("str"), {
+                                              {mod.symbols.Register("arr"), ty.array<i32, 4>()},
+                                          });
+
+    auto* fn_a = b.Function("a", ty.void_());
+    auto* fn_a_p = b.FunctionParam("p", ty.ptr<function, array<i32, 4>>());
+    fn_a->SetParams({
+        b.FunctionParam("pre", ty.i32()),
+        fn_a_p,
+        b.FunctionParam("post", ty.i32()),
+    });
+    b.Append(fn_a->Block(), [&] {
+        b.Store(fn_a_p, b.Splat(ty.array<i32, 4>(), 0_i, 4));
+        b.Return(fn_a);
+    });
+
+    auto* fn_b = b.Function("b", ty.void_());
+    b.Append(fn_b->Block(), [&] {
+        auto* F = b.Var("F", ty.ptr<function>(str_));
+        auto* access = b.Access(ty.ptr<function, array<i32, 4>>(), F, 0_u);
+        b.Call(fn_a, 10_i, access, 20_i);
+        b.Return(fn_b);
+    });
+
+    auto* src = R"(
+str = struct @align(4) {
+  arr:array<i32, 4> @offset(0)
+}
+
+%a = func(%pre:i32, %p:ptr<function, array<i32, 4>, read_write>, %post:i32):void -> %b1 {
+  %b1 = block {
+    store %p, array<i32, 4>(0i)
+    ret
+  }
+}
+%b = func():void -> %b2 {
+  %b2 = block {
+    %F:ptr<function, str, read_write> = var
+    %7:ptr<function, array<i32, 4>, read_write> = access %F, 0u
+    %8:void = call %a, 10i, %7, 20i
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+str = struct @align(4) {
+  arr:array<i32, 4> @offset(0)
+}
+
+%a_P_arr = func(%pre:i32, %p_root:ptr<function, str, read_write>, %post:i32):void -> %b1 {
+  %b1 = block {
+    %5:ptr<function, array<i32, 4>, read_write> = access %p_root, 0u
+    store %5, array<i32, 4>(0i)
+    ret
+  }
+}
+%b = func():void -> %b2 {
+  %b2 = block {
+    %F:ptr<function, str, read_write> = var
+    %8:void = call %a_P_arr, 10i, %F, 20i
+    ret
+  }
+}
+)";
+
+    Run(DirectVariableAccess, kTransformFunction);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_FunctionAS, Enabled_Param_ptr_i32_mixed) {
+    auto* str_ = ty.Struct(mod.symbols.New("str"), {
+                                                       {mod.symbols.Register("i"), ty.i32()},
+                                                   });
+
+    auto* fn_a = b.Function("a", ty.i32());
+    auto* fn_a_p = b.FunctionParam("p", ty.ptr<function, i32>());
+    fn_a->SetParams({
+        b.FunctionParam("pre", ty.i32()),
+        fn_a_p,
+        b.FunctionParam("post", ty.i32()),
+    });
+    b.Append(fn_a->Block(), [&] { b.Return(fn_a, b.Load(fn_a_p)); });
+
+    auto* fn_b = b.Function("b", ty.void_());
+    b.Append(fn_b->Block(), [&] {
+        auto* Fi = b.Var("Fi", ty.ptr<function, i32>());
+        auto* Fs = b.Var("Fs", ty.ptr<function>(str_));
+        auto* Fa = b.Var("Fa", ty.ptr<function, array<i32, 4>>());
+        {  // a(10, &Fi, 20);
+            b.Call(fn_a, 10_i, Fi, 20_i);
+        }
+        {  // a(30, &Fs.i, 40);
+            auto* access = b.Access(ty.ptr<function, i32>(), Fs, 0_u);
+            b.Call(fn_a, 30_i, access, 40_i);
+        }
+        {  // a(50, &Fa[2], 60);
+            auto* access = b.Access(ty.ptr<function, i32>(), Fa, 2_i);
+            b.Call(fn_a, 50_i, access, 60_i);
+        }
+        b.Return(fn_b);
+    });
+
+    auto* src = R"(
+str = struct @align(4) {
+  i:i32 @offset(0)
+}
+
+%a = func(%pre:i32, %p:ptr<function, i32, read_write>, %post:i32):i32 -> %b1 {
+  %b1 = block {
+    %5:i32 = load %p
+    ret %5
+  }
+}
+%b = func():void -> %b2 {
+  %b2 = block {
+    %Fi:ptr<function, i32, read_write> = var
+    %Fs:ptr<function, str, read_write> = var
+    %Fa:ptr<function, array<i32, 4>, read_write> = var
+    %10:i32 = call %a, 10i, %Fi, 20i
+    %11:ptr<function, i32, read_write> = access %Fs, 0u
+    %12:i32 = call %a, 30i, %11, 40i
+    %13:ptr<function, i32, read_write> = access %Fa, 2i
+    %14:i32 = call %a, 50i, %13, 60i
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+str = struct @align(4) {
+  i:i32 @offset(0)
+}
+
+%a_P = func(%pre:i32, %p_root:ptr<function, i32, read_write>, %post:i32):i32 -> %b1 {
+  %b1 = block {
+    %5:ptr<function, i32, read_write> = access %p_root
+    %6:i32 = load %5
+    ret %6
+  }
+}
+%a_P_i = func(%pre_1:i32, %p_root_1:ptr<function, str, read_write>, %post_1:i32):i32 -> %b2 {  # %pre_1: 'pre', %p_root_1: 'p_root', %post_1: 'post'
+  %b2 = block {
+    %11:ptr<function, i32, read_write> = access %p_root_1, 0u
+    %12:i32 = load %11
+    ret %12
+  }
+}
+%a_P_X = func(%pre_2:i32, %p_root_2:ptr<function, array<i32, 4>, read_write>, %p_indices:array<u32, 1>, %post_2:i32):i32 -> %b3 {  # %pre_2: 'pre', %p_root_2: 'p_root', %post_2: 'post'
+  %b3 = block {
+    %18:u32 = access %p_indices, 0u
+    %19:ptr<function, i32, read_write> = access %p_root_2, %18
+    %20:i32 = load %19
+    ret %20
+  }
+}
+%b = func():void -> %b4 {
+  %b4 = block {
+    %Fi:ptr<function, i32, read_write> = var
+    %Fs:ptr<function, str, read_write> = var
+    %Fa:ptr<function, array<i32, 4>, read_write> = var
+    %25:i32 = call %a_P, 10i, %Fi, 20i
+    %26:i32 = call %a_P_i, 30i, %Fs, 40i
+    %27:u32 = convert 2i
+    %28:array<u32, 1> = construct %27
+    %29:i32 = call %a_P_X, 50i, %Fa, %28, 60i
+    ret
+  }
+}
+)";
+
+    Run(DirectVariableAccess, kTransformFunction);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_FunctionAS, Disabled_Param_ptr_i32_Via_struct_read) {
+    auto* str_ = ty.Struct(mod.symbols.New("str"), {
+                                                       {mod.symbols.Register("i"), ty.i32()},
+                                                   });
+
+    auto* fn_a = b.Function("a", ty.i32());
+    auto* fn_a_p = b.FunctionParam("p", ty.ptr<function, i32>());
+    fn_a->SetParams({
+        b.FunctionParam("pre", ty.i32()),
+        fn_a_p,
+        b.FunctionParam("post", ty.i32()),
+    });
+    b.Append(fn_a->Block(), [&] { b.Return(fn_a, b.Load(fn_a_p)); });
+
+    auto* fn_b = b.Function("b", ty.void_());
+    b.Append(fn_b->Block(), [&] {
+        auto* F = b.Var("F", ty.ptr<function>(str_));
+        auto* access = b.Access(ty.ptr<function, i32>(), F, 0_u);
+        b.Call(fn_a, 10_i, access, 20_i);
+        b.Return(fn_b);
+    });
+
+    auto* src = R"(
+str = struct @align(4) {
+  i:i32 @offset(0)
+}
+
+%a = func(%pre:i32, %p:ptr<function, i32, read_write>, %post:i32):i32 -> %b1 {
+  %b1 = block {
+    %5:i32 = load %p
+    ret %5
+  }
+}
+%b = func():void -> %b2 {
+  %b2 = block {
+    %F:ptr<function, str, read_write> = var
+    %8:ptr<function, i32, read_write> = access %F, 0u
+    %9:i32 = call %a, 10i, %8, 20i
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(DirectVariableAccess, DirectVariableAccessOptions{});
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_FunctionAS, Disabled_Param_ptr_arr_i32_Via_struct_write) {
+    auto* str_ =
+        ty.Struct(mod.symbols.New("str"), {
+                                              {mod.symbols.Register("arr"), ty.array<i32, 4>()},
+                                          });
+
+    auto* fn_a = b.Function("a", ty.void_());
+    auto* fn_a_p = b.FunctionParam("p", ty.ptr<function, array<i32, 4>>());
+    fn_a->SetParams({
+        b.FunctionParam("pre", ty.i32()),
+        fn_a_p,
+        b.FunctionParam("post", ty.i32()),
+    });
+    b.Append(fn_a->Block(), [&] {
+        b.Store(fn_a_p, b.Splat(ty.array<i32, 4>(), 0_i, 4));
+        b.Return(fn_a);
+    });
+
+    auto* fn_b = b.Function("b", ty.void_());
+    b.Append(fn_b->Block(), [&] {
+        auto* F = b.Var("F", ty.ptr<function>(str_));
+        auto* access = b.Access(ty.ptr<function, array<i32, 4>>(), F, 0_u);
+        b.Call(fn_a, 10_i, access, 20_i);
+        b.Return(fn_b);
+    });
+
+    auto* src = R"(
+str = struct @align(4) {
+  arr:array<i32, 4> @offset(0)
+}
+
+%a = func(%pre:i32, %p:ptr<function, array<i32, 4>, read_write>, %post:i32):void -> %b1 {
+  %b1 = block {
+    store %p, array<i32, 4>(0i)
+    ret
+  }
+}
+%b = func():void -> %b2 {
+  %b2 = block {
+    %F:ptr<function, str, read_write> = var
+    %7:ptr<function, array<i32, 4>, read_write> = access %F, 0u
+    %8:void = call %a, 10i, %7, 20i
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = src;
+
+    Run(DirectVariableAccess, DirectVariableAccessOptions{});
+
+    EXPECT_EQ(expect, str());
+}
+
+}  // namespace function_as_tests
+
+////////////////////////////////////////////////////////////////////////////////
+// builtin function calls
+////////////////////////////////////////////////////////////////////////////////
+namespace builtin_fn_calls {
+
+using IR_DirectVariableAccessTest_BuiltinFn = TransformTest;
+
+TEST_F(IR_DirectVariableAccessTest_BuiltinFn, ArrayLength) {
+    Var* S = nullptr;
+    b.Append(b.ir.root_block,
+             [&] {  //
+                 S = b.Var<storage, array<f32>>("S");
+                 S->SetBindingPoint(0, 0);
+             });
+
+    auto* fn_len = b.Function("len", ty.u32());
+    auto* fn_len_p = b.FunctionParam("p", ty.ptr<storage, array<f32>>());
+    fn_len->SetParams({fn_len_p});
+    b.Append(fn_len->Block(),
+             [&] {  //
+                 b.Return(fn_len, b.Call(ty.u32(), core::BuiltinFn::kArrayLength, fn_len_p));
+             });
+
+    auto* fn_f = b.Function("b", ty.void_());
+    b.Append(fn_f->Block(), [&] {
+        b.Call(fn_len, S);
+        b.Return(fn_f);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %S:ptr<storage, array<f32>, read_write> = var @binding_point(0, 0)
+}
+
+%len = func(%p:ptr<storage, array<f32>, read_write>):u32 -> %b2 {
+  %b2 = block {
+    %4:u32 = arrayLength %p
+    ret %4
+  }
+}
+%b = func():void -> %b3 {
+  %b3 = block {
+    %6:u32 = call %len, %S
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %S:ptr<storage, array<f32>, read_write> = var @binding_point(0, 0)
+}
+
+%len_S = func():u32 -> %b2 {
+  %b2 = block {
+    %3:ptr<storage, array<f32>, read_write> = access %S
+    %4:u32 = arrayLength %3
+    ret %4
+  }
+}
+%b = func():void -> %b3 {
+  %b3 = block {
+    %6:u32 = call %len_S
+    ret
+  }
+}
+)";
+
+    Run(DirectVariableAccess, DirectVariableAccessOptions{});
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_BuiltinFn, WorkgroupUniformLoad) {
+    Var* W = nullptr;
+    b.Append(b.ir.root_block,
+             [&] {  //
+                 W = b.Var<workgroup, f32>("W");
+             });
+
+    auto* fn_load = b.Function("load", ty.f32());
+    auto* fn_load_p = b.FunctionParam("p", ty.ptr<workgroup, f32>());
+    fn_load->SetParams({fn_load_p});
+    b.Append(fn_load->Block(),
+             [&] {  //
+                 b.Return(fn_load,
+                          b.Call(ty.f32(), core::BuiltinFn::kWorkgroupUniformLoad, fn_load_p));
+             });
+
+    auto* fn_f = b.Function("b", ty.void_());
+    b.Append(fn_f->Block(), [&] {
+        b.Call(fn_load, W);
+        b.Return(fn_f);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %W:ptr<workgroup, f32, read_write> = var
+}
+
+%load = func(%p:ptr<workgroup, f32, read_write>):f32 -> %b2 {
+  %b2 = block {
+    %4:f32 = workgroupUniformLoad %p
+    ret %4
+  }
+}
+%b = func():void -> %b3 {
+  %b3 = block {
+    %6:f32 = call %load, %W
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %W:ptr<workgroup, f32, read_write> = var
+}
+
+%load_W = func():f32 -> %b2 {
+  %b2 = block {
+    %3:ptr<workgroup, f32, read_write> = access %W
+    %4:f32 = workgroupUniformLoad %3
+    ret %4
+  }
+}
+%b = func():void -> %b3 {
+  %b3 = block {
+    %6:f32 = call %load_W
+    ret
+  }
+}
+)";
+
+    Run(DirectVariableAccess, DirectVariableAccessOptions{});
+
+    EXPECT_EQ(expect, str());
+}
+
+}  // namespace builtin_fn_calls
+
+////////////////////////////////////////////////////////////////////////////////
+// complex tests
+////////////////////////////////////////////////////////////////////////////////
+namespace complex_tests {
+
+using IR_DirectVariableAccessTest_Complex = TransformTest;
+
+TEST_F(IR_DirectVariableAccessTest_Complex, Param_ptr_mixed_vec4i32_ViaMultiple) {
+    auto* str_ = ty.Struct(mod.symbols.New("str"), {
+                                                       {mod.symbols.Register("i"), ty.vec4<i32>()},
+                                                   });
+
+    Var* U = nullptr;
+    Var* U_str = nullptr;
+    Var* U_arr = nullptr;
+    Var* U_arr_arr = nullptr;
+    Var* S = nullptr;
+    Var* S_str = nullptr;
+    Var* S_arr = nullptr;
+    Var* S_arr_arr = nullptr;
+    Var* W = nullptr;
+    Var* W_str = nullptr;
+    Var* W_arr = nullptr;
+    Var* W_arr_arr = nullptr;
+    b.Append(b.ir.root_block,
+             [&] {  //
+                 U = b.Var<uniform, vec4<i32>, read>("U");
+                 U->SetBindingPoint(0, 0);
+                 U_str = b.Var("U_str", ty.ptr<uniform, read>(str_));
+                 U_str->SetBindingPoint(0, 1);
+                 U_arr = b.Var<uniform, array<vec4<i32>, 8>, read>("U_arr");
+                 U_arr->SetBindingPoint(0, 2);
+                 U_arr_arr = b.Var<uniform, array<array<vec4<i32>, 8>, 4>, read>("U_arr_arr");
+                 U_arr_arr->SetBindingPoint(0, 3);
+
+                 S = b.Var<storage, vec4<i32>, read>("S");
+                 S->SetBindingPoint(1, 0);
+                 S_str = b.Var("S_str", ty.ptr<storage, read>(str_));
+                 S_str->SetBindingPoint(1, 1);
+                 S_arr = b.Var<storage, array<vec4<i32>, 8>, read>("S_arr");
+                 S_arr->SetBindingPoint(1, 2);
+                 S_arr_arr = b.Var<storage, array<array<vec4<i32>, 8>, 4>, read>("S_arr_arr");
+                 S_arr_arr->SetBindingPoint(1, 3);
+
+                 W = b.Var<workgroup, vec4<i32>>("W");
+                 W_str = b.Var("W_str", ty.ptr<workgroup>(str_));
+                 W_arr = b.Var<workgroup, array<vec4<i32>, 8>>("W_arr");
+                 W_arr_arr = b.Var<workgroup, array<array<vec4<i32>, 8>, 4>>("W_arr_arr");
+             });
+
+    auto* fn_u = b.Function("fn_u", ty.vec4<i32>());
+    auto* fn_u_p = b.FunctionParam("p", ty.ptr<uniform, vec4<i32>, read>());
+    fn_u->SetParams({fn_u_p});
+    b.Append(fn_u->Block(), [&] { b.Return(fn_u, b.Load(fn_u_p)); });
+
+    auto* fn_s = b.Function("fn_s", ty.vec4<i32>());
+    auto* fn_s_p = b.FunctionParam("p", ty.ptr<storage, vec4<i32>, read>());
+    fn_s->SetParams({fn_s_p});
+    b.Append(fn_s->Block(), [&] { b.Return(fn_s, b.Load(fn_s_p)); });
+
+    auto* fn_w = b.Function("fn_w", ty.vec4<i32>());
+    auto* fn_w_p = b.FunctionParam("p", ty.ptr<workgroup, vec4<i32>>());
+    fn_w->SetParams({fn_w_p});
+    b.Append(fn_w->Block(), [&] { b.Return(fn_w, b.Load(fn_w_p)); });
+
+    auto* fn_b = b.Function("b", ty.void_());
+    b.Append(fn_b->Block(), [&] {
+        auto* I = b.Let("I", 3_i);
+        auto* J = b.Let("J", 4_i);
+
+        auto* u = b.Call(fn_u, U);
+        b.ir.SetName(u, "u");
+        auto* u_str = b.Call(fn_u, b.Access(ty.ptr<uniform, vec4<i32>, read>(), U_str, 0_u));
+        b.ir.SetName(u_str, "u_str");
+        auto* u_arr0 = b.Call(fn_u, b.Access(ty.ptr<uniform, vec4<i32>, read>(), U_arr, 0_i));
+        b.ir.SetName(u_arr0, "u_arr0");
+        auto* u_arr1 = b.Call(fn_u, b.Access(ty.ptr<uniform, vec4<i32>, read>(), U_arr, 1_i));
+        b.ir.SetName(u_arr1, "u_arr1");
+        auto* u_arrI = b.Call(fn_u, b.Access(ty.ptr<uniform, vec4<i32>, read>(), U_arr, I));
+        b.ir.SetName(u_arrI, "u_arrI");
+        auto* u_arr1_arr0 =
+            b.Call(fn_u, b.Access(ty.ptr<uniform, vec4<i32>, read>(), U_arr_arr, 1_i, 0_i));
+        b.ir.SetName(u_arr1_arr0, "u_arr1_arr0");
+        auto* u_arr2_arrI =
+            b.Call(fn_u, b.Access(ty.ptr<uniform, vec4<i32>, read>(), U_arr_arr, 2_i, I));
+        b.ir.SetName(u_arr2_arrI, "u_arr2_arrI");
+        auto* u_arrI_arr2 =
+            b.Call(fn_u, b.Access(ty.ptr<uniform, vec4<i32>, read>(), U_arr_arr, I, 2_i));
+        b.ir.SetName(u_arrI_arr2, "u_arrI_arr2");
+        auto* u_arrI_arrJ =
+            b.Call(fn_u, b.Access(ty.ptr<uniform, vec4<i32>, read>(), U_arr_arr, I, J));
+        b.ir.SetName(u_arrI_arrJ, "u_arrI_arrJ");
+
+        auto* s = b.Call(fn_s, S);
+        b.ir.SetName(s, "s");
+        auto* s_str = b.Call(fn_s, b.Access(ty.ptr<storage, vec4<i32>, read>(), S_str, 0_u));
+        b.ir.SetName(s_str, "s_str");
+        auto* s_arr0 = b.Call(fn_s, b.Access(ty.ptr<storage, vec4<i32>, read>(), S_arr, 0_i));
+        b.ir.SetName(s_arr0, "s_arr0");
+        auto* s_arr1 = b.Call(fn_s, b.Access(ty.ptr<storage, vec4<i32>, read>(), S_arr, 1_i));
+        b.ir.SetName(s_arr1, "s_arr1");
+        auto* s_arrI = b.Call(fn_s, b.Access(ty.ptr<storage, vec4<i32>, read>(), S_arr, I));
+        b.ir.SetName(s_arrI, "s_arrI");
+        auto* s_arr1_arr0 =
+            b.Call(fn_s, b.Access(ty.ptr<storage, vec4<i32>, read>(), S_arr_arr, 1_i, 0_i));
+        b.ir.SetName(s_arr1_arr0, "s_arr1_arr0");
+        auto* s_arr2_arrI =
+            b.Call(fn_s, b.Access(ty.ptr<storage, vec4<i32>, read>(), S_arr_arr, 2_i, I));
+        b.ir.SetName(s_arr2_arrI, "s_arr2_arrI");
+        auto* s_arrI_arr2 =
+            b.Call(fn_s, b.Access(ty.ptr<storage, vec4<i32>, read>(), S_arr_arr, I, 2_i));
+        b.ir.SetName(s_arrI_arr2, "s_arrI_arr2");
+        auto* s_arrI_arrJ =
+            b.Call(fn_s, b.Access(ty.ptr<storage, vec4<i32>, read>(), S_arr_arr, I, J));
+        b.ir.SetName(s_arrI_arrJ, "s_arrI_arrJ");
+
+        auto* w = b.Call(fn_w, W);
+        b.ir.SetName(w, "w");
+        auto* w_str = b.Call(fn_w, b.Access(ty.ptr<workgroup, vec4<i32>>(), W_str, 0_u));
+        b.ir.SetName(w_str, "w_str");
+        auto* w_arr0 = b.Call(fn_w, b.Access(ty.ptr<workgroup, vec4<i32>>(), W_arr, 0_i));
+        b.ir.SetName(w_arr0, "w_arr0");
+        auto* w_arr1 = b.Call(fn_w, b.Access(ty.ptr<workgroup, vec4<i32>>(), W_arr, 1_i));
+        b.ir.SetName(w_arr1, "w_arr1");
+        auto* w_arrI = b.Call(fn_w, b.Access(ty.ptr<workgroup, vec4<i32>>(), W_arr, I));
+        b.ir.SetName(w_arrI, "w_arrI");
+        auto* w_arr1_arr0 =
+            b.Call(fn_w, b.Access(ty.ptr<workgroup, vec4<i32>>(), W_arr_arr, 1_i, 0_i));
+        b.ir.SetName(w_arr1_arr0, "w_arr1_arr0");
+        auto* w_arr2_arrI =
+            b.Call(fn_w, b.Access(ty.ptr<workgroup, vec4<i32>>(), W_arr_arr, 2_i, I));
+        b.ir.SetName(w_arr2_arrI, "w_arr2_arrI");
+        auto* w_arrI_arr2 =
+            b.Call(fn_w, b.Access(ty.ptr<workgroup, vec4<i32>>(), W_arr_arr, I, 2_i));
+        b.ir.SetName(w_arrI_arr2, "w_arrI_arr2");
+        auto* w_arrI_arrJ = b.Call(fn_w, b.Access(ty.ptr<workgroup, vec4<i32>>(), W_arr_arr, I, J));
+        b.ir.SetName(w_arrI_arrJ, "w_arrI_arrJ");
+
+        b.Return(fn_b);
+    });
+
+    auto* src = R"(
+str = struct @align(16) {
+  i:vec4<i32> @offset(0)
+}
+
+%b1 = block {  # root
+  %U:ptr<uniform, vec4<i32>, read> = var @binding_point(0, 0)
+  %U_str:ptr<uniform, str, read> = var @binding_point(0, 1)
+  %U_arr:ptr<uniform, array<vec4<i32>, 8>, read> = var @binding_point(0, 2)
+  %U_arr_arr:ptr<uniform, array<array<vec4<i32>, 8>, 4>, read> = var @binding_point(0, 3)
+  %S:ptr<storage, vec4<i32>, read> = var @binding_point(1, 0)
+  %S_str:ptr<storage, str, read> = var @binding_point(1, 1)
+  %S_arr:ptr<storage, array<vec4<i32>, 8>, read> = var @binding_point(1, 2)
+  %S_arr_arr:ptr<storage, array<array<vec4<i32>, 8>, 4>, read> = var @binding_point(1, 3)
+  %W:ptr<workgroup, vec4<i32>, read_write> = var
+  %W_str:ptr<workgroup, str, read_write> = var
+  %W_arr:ptr<workgroup, array<vec4<i32>, 8>, read_write> = var
+  %W_arr_arr:ptr<workgroup, array<array<vec4<i32>, 8>, 4>, read_write> = var
+}
+
+%fn_u = func(%p:ptr<uniform, vec4<i32>, read>):vec4<i32> -> %b2 {
+  %b2 = block {
+    %15:vec4<i32> = load %p
+    ret %15
+  }
+}
+%fn_s = func(%p_1:ptr<storage, vec4<i32>, read>):vec4<i32> -> %b3 {  # %p_1: 'p'
+  %b3 = block {
+    %18:vec4<i32> = load %p_1
+    ret %18
+  }
+}
+%fn_w = func(%p_2:ptr<workgroup, vec4<i32>, read_write>):vec4<i32> -> %b4 {  # %p_2: 'p'
+  %b4 = block {
+    %21:vec4<i32> = load %p_2
+    ret %21
+  }
+}
+%b = func():void -> %b5 {
+  %b5 = block {
+    %I:i32 = let 3i
+    %J:i32 = let 4i
+    %u:vec4<i32> = call %fn_u, %U
+    %26:ptr<uniform, vec4<i32>, read> = access %U_str, 0u
+    %u_str:vec4<i32> = call %fn_u, %26
+    %28:ptr<uniform, vec4<i32>, read> = access %U_arr, 0i
+    %u_arr0:vec4<i32> = call %fn_u, %28
+    %30:ptr<uniform, vec4<i32>, read> = access %U_arr, 1i
+    %u_arr1:vec4<i32> = call %fn_u, %30
+    %32:ptr<uniform, vec4<i32>, read> = access %U_arr, %I
+    %u_arrI:vec4<i32> = call %fn_u, %32
+    %34:ptr<uniform, vec4<i32>, read> = access %U_arr_arr, 1i, 0i
+    %u_arr1_arr0:vec4<i32> = call %fn_u, %34
+    %36:ptr<uniform, vec4<i32>, read> = access %U_arr_arr, 2i, %I
+    %u_arr2_arrI:vec4<i32> = call %fn_u, %36
+    %38:ptr<uniform, vec4<i32>, read> = access %U_arr_arr, %I, 2i
+    %u_arrI_arr2:vec4<i32> = call %fn_u, %38
+    %40:ptr<uniform, vec4<i32>, read> = access %U_arr_arr, %I, %J
+    %u_arrI_arrJ:vec4<i32> = call %fn_u, %40
+    %s:vec4<i32> = call %fn_s, %S
+    %43:ptr<storage, vec4<i32>, read> = access %S_str, 0u
+    %s_str:vec4<i32> = call %fn_s, %43
+    %45:ptr<storage, vec4<i32>, read> = access %S_arr, 0i
+    %s_arr0:vec4<i32> = call %fn_s, %45
+    %47:ptr<storage, vec4<i32>, read> = access %S_arr, 1i
+    %s_arr1:vec4<i32> = call %fn_s, %47
+    %49:ptr<storage, vec4<i32>, read> = access %S_arr, %I
+    %s_arrI:vec4<i32> = call %fn_s, %49
+    %51:ptr<storage, vec4<i32>, read> = access %S_arr_arr, 1i, 0i
+    %s_arr1_arr0:vec4<i32> = call %fn_s, %51
+    %53:ptr<storage, vec4<i32>, read> = access %S_arr_arr, 2i, %I
+    %s_arr2_arrI:vec4<i32> = call %fn_s, %53
+    %55:ptr<storage, vec4<i32>, read> = access %S_arr_arr, %I, 2i
+    %s_arrI_arr2:vec4<i32> = call %fn_s, %55
+    %57:ptr<storage, vec4<i32>, read> = access %S_arr_arr, %I, %J
+    %s_arrI_arrJ:vec4<i32> = call %fn_s, %57
+    %w:vec4<i32> = call %fn_w, %W
+    %60:ptr<workgroup, vec4<i32>, read_write> = access %W_str, 0u
+    %w_str:vec4<i32> = call %fn_w, %60
+    %62:ptr<workgroup, vec4<i32>, read_write> = access %W_arr, 0i
+    %w_arr0:vec4<i32> = call %fn_w, %62
+    %64:ptr<workgroup, vec4<i32>, read_write> = access %W_arr, 1i
+    %w_arr1:vec4<i32> = call %fn_w, %64
+    %66:ptr<workgroup, vec4<i32>, read_write> = access %W_arr, %I
+    %w_arrI:vec4<i32> = call %fn_w, %66
+    %68:ptr<workgroup, vec4<i32>, read_write> = access %W_arr_arr, 1i, 0i
+    %w_arr1_arr0:vec4<i32> = call %fn_w, %68
+    %70:ptr<workgroup, vec4<i32>, read_write> = access %W_arr_arr, 2i, %I
+    %w_arr2_arrI:vec4<i32> = call %fn_w, %70
+    %72:ptr<workgroup, vec4<i32>, read_write> = access %W_arr_arr, %I, 2i
+    %w_arrI_arr2:vec4<i32> = call %fn_w, %72
+    %74:ptr<workgroup, vec4<i32>, read_write> = access %W_arr_arr, %I, %J
+    %w_arrI_arrJ:vec4<i32> = call %fn_w, %74
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+str = struct @align(16) {
+  i:vec4<i32> @offset(0)
+}
+
+%b1 = block {  # root
+  %U:ptr<uniform, vec4<i32>, read> = var @binding_point(0, 0)
+  %U_str:ptr<uniform, str, read> = var @binding_point(0, 1)
+  %U_arr:ptr<uniform, array<vec4<i32>, 8>, read> = var @binding_point(0, 2)
+  %U_arr_arr:ptr<uniform, array<array<vec4<i32>, 8>, 4>, read> = var @binding_point(0, 3)
+  %S:ptr<storage, vec4<i32>, read> = var @binding_point(1, 0)
+  %S_str:ptr<storage, str, read> = var @binding_point(1, 1)
+  %S_arr:ptr<storage, array<vec4<i32>, 8>, read> = var @binding_point(1, 2)
+  %S_arr_arr:ptr<storage, array<array<vec4<i32>, 8>, 4>, read> = var @binding_point(1, 3)
+  %W:ptr<workgroup, vec4<i32>, read_write> = var
+  %W_str:ptr<workgroup, str, read_write> = var
+  %W_arr:ptr<workgroup, array<vec4<i32>, 8>, read_write> = var
+  %W_arr_arr:ptr<workgroup, array<array<vec4<i32>, 8>, 4>, read_write> = var
+}
+
+%fn_u_U = func():vec4<i32> -> %b2 {
+  %b2 = block {
+    %14:ptr<uniform, vec4<i32>, read> = access %U
+    %15:vec4<i32> = load %14
+    ret %15
+  }
+}
+%fn_u_U_str_i = func():vec4<i32> -> %b3 {
+  %b3 = block {
+    %17:ptr<uniform, vec4<i32>, read> = access %U_str, 0u
+    %18:vec4<i32> = load %17
+    ret %18
+  }
+}
+%fn_u_U_arr_X = func(%p_indices:array<u32, 1>):vec4<i32> -> %b4 {
+  %b4 = block {
+    %21:u32 = access %p_indices, 0u
+    %22:ptr<uniform, vec4<i32>, read> = access %U_arr, %21
+    %23:vec4<i32> = load %22
+    ret %23
+  }
+}
+%fn_u_U_arr_arr_X_X = func(%p_indices_1:array<u32, 2>):vec4<i32> -> %b5 {  # %p_indices_1: 'p_indices'
+  %b5 = block {
+    %26:u32 = access %p_indices_1, 0u
+    %27:u32 = access %p_indices_1, 1u
+    %28:ptr<uniform, vec4<i32>, read> = access %U_arr_arr, %26, %27
+    %29:vec4<i32> = load %28
+    ret %29
+  }
+}
+%fn_s_S = func():vec4<i32> -> %b6 {
+  %b6 = block {
+    %31:ptr<storage, vec4<i32>, read> = access %S
+    %32:vec4<i32> = load %31
+    ret %32
+  }
+}
+%fn_s_S_str_i = func():vec4<i32> -> %b7 {
+  %b7 = block {
+    %34:ptr<storage, vec4<i32>, read> = access %S_str, 0u
+    %35:vec4<i32> = load %34
+    ret %35
+  }
+}
+%fn_s_S_arr_X = func(%p_indices_2:array<u32, 1>):vec4<i32> -> %b8 {  # %p_indices_2: 'p_indices'
+  %b8 = block {
+    %38:u32 = access %p_indices_2, 0u
+    %39:ptr<storage, vec4<i32>, read> = access %S_arr, %38
+    %40:vec4<i32> = load %39
+    ret %40
+  }
+}
+%fn_s_S_arr_arr_X_X = func(%p_indices_3:array<u32, 2>):vec4<i32> -> %b9 {  # %p_indices_3: 'p_indices'
+  %b9 = block {
+    %43:u32 = access %p_indices_3, 0u
+    %44:u32 = access %p_indices_3, 1u
+    %45:ptr<storage, vec4<i32>, read> = access %S_arr_arr, %43, %44
+    %46:vec4<i32> = load %45
+    ret %46
+  }
+}
+%fn_w_W = func():vec4<i32> -> %b10 {
+  %b10 = block {
+    %48:ptr<workgroup, vec4<i32>, read_write> = access %W
+    %49:vec4<i32> = load %48
+    ret %49
+  }
+}
+%fn_w_W_str_i = func():vec4<i32> -> %b11 {
+  %b11 = block {
+    %51:ptr<workgroup, vec4<i32>, read_write> = access %W_str, 0u
+    %52:vec4<i32> = load %51
+    ret %52
+  }
+}
+%fn_w_W_arr_X = func(%p_indices_4:array<u32, 1>):vec4<i32> -> %b12 {  # %p_indices_4: 'p_indices'
+  %b12 = block {
+    %55:u32 = access %p_indices_4, 0u
+    %56:ptr<workgroup, vec4<i32>, read_write> = access %W_arr, %55
+    %57:vec4<i32> = load %56
+    ret %57
+  }
+}
+%fn_w_W_arr_arr_X_X = func(%p_indices_5:array<u32, 2>):vec4<i32> -> %b13 {  # %p_indices_5: 'p_indices'
+  %b13 = block {
+    %60:u32 = access %p_indices_5, 0u
+    %61:u32 = access %p_indices_5, 1u
+    %62:ptr<workgroup, vec4<i32>, read_write> = access %W_arr_arr, %60, %61
+    %63:vec4<i32> = load %62
+    ret %63
+  }
+}
+%b = func():void -> %b14 {
+  %b14 = block {
+    %I:i32 = let 3i
+    %J:i32 = let 4i
+    %u:vec4<i32> = call %fn_u_U
+    %u_str:vec4<i32> = call %fn_u_U_str_i
+    %69:u32 = convert 0i
+    %70:array<u32, 1> = construct %69
+    %u_arr0:vec4<i32> = call %fn_u_U_arr_X, %70
+    %72:u32 = convert 1i
+    %73:array<u32, 1> = construct %72
+    %u_arr1:vec4<i32> = call %fn_u_U_arr_X, %73
+    %75:u32 = convert %I
+    %76:array<u32, 1> = construct %75
+    %u_arrI:vec4<i32> = call %fn_u_U_arr_X, %76
+    %78:u32 = convert 1i
+    %79:u32 = convert 0i
+    %80:array<u32, 2> = construct %78, %79
+    %u_arr1_arr0:vec4<i32> = call %fn_u_U_arr_arr_X_X, %80
+    %82:u32 = convert 2i
+    %83:u32 = convert %I
+    %84:array<u32, 2> = construct %82, %83
+    %u_arr2_arrI:vec4<i32> = call %fn_u_U_arr_arr_X_X, %84
+    %86:u32 = convert %I
+    %87:u32 = convert 2i
+    %88:array<u32, 2> = construct %86, %87
+    %u_arrI_arr2:vec4<i32> = call %fn_u_U_arr_arr_X_X, %88
+    %90:u32 = convert %I
+    %91:u32 = convert %J
+    %92:array<u32, 2> = construct %90, %91
+    %u_arrI_arrJ:vec4<i32> = call %fn_u_U_arr_arr_X_X, %92
+    %s:vec4<i32> = call %fn_s_S
+    %s_str:vec4<i32> = call %fn_s_S_str_i
+    %96:u32 = convert 0i
+    %97:array<u32, 1> = construct %96
+    %s_arr0:vec4<i32> = call %fn_s_S_arr_X, %97
+    %99:u32 = convert 1i
+    %100:array<u32, 1> = construct %99
+    %s_arr1:vec4<i32> = call %fn_s_S_arr_X, %100
+    %102:u32 = convert %I
+    %103:array<u32, 1> = construct %102
+    %s_arrI:vec4<i32> = call %fn_s_S_arr_X, %103
+    %105:u32 = convert 1i
+    %106:u32 = convert 0i
+    %107:array<u32, 2> = construct %105, %106
+    %s_arr1_arr0:vec4<i32> = call %fn_s_S_arr_arr_X_X, %107
+    %109:u32 = convert 2i
+    %110:u32 = convert %I
+    %111:array<u32, 2> = construct %109, %110
+    %s_arr2_arrI:vec4<i32> = call %fn_s_S_arr_arr_X_X, %111
+    %113:u32 = convert %I
+    %114:u32 = convert 2i
+    %115:array<u32, 2> = construct %113, %114
+    %s_arrI_arr2:vec4<i32> = call %fn_s_S_arr_arr_X_X, %115
+    %117:u32 = convert %I
+    %118:u32 = convert %J
+    %119:array<u32, 2> = construct %117, %118
+    %s_arrI_arrJ:vec4<i32> = call %fn_s_S_arr_arr_X_X, %119
+    %w:vec4<i32> = call %fn_w_W
+    %w_str:vec4<i32> = call %fn_w_W_str_i
+    %123:u32 = convert 0i
+    %124:array<u32, 1> = construct %123
+    %w_arr0:vec4<i32> = call %fn_w_W_arr_X, %124
+    %126:u32 = convert 1i
+    %127:array<u32, 1> = construct %126
+    %w_arr1:vec4<i32> = call %fn_w_W_arr_X, %127
+    %129:u32 = convert %I
+    %130:array<u32, 1> = construct %129
+    %w_arrI:vec4<i32> = call %fn_w_W_arr_X, %130
+    %132:u32 = convert 1i
+    %133:u32 = convert 0i
+    %134:array<u32, 2> = construct %132, %133
+    %w_arr1_arr0:vec4<i32> = call %fn_w_W_arr_arr_X_X, %134
+    %136:u32 = convert 2i
+    %137:u32 = convert %I
+    %138:array<u32, 2> = construct %136, %137
+    %w_arr2_arrI:vec4<i32> = call %fn_w_W_arr_arr_X_X, %138
+    %140:u32 = convert %I
+    %141:u32 = convert 2i
+    %142:array<u32, 2> = construct %140, %141
+    %w_arrI_arr2:vec4<i32> = call %fn_w_W_arr_arr_X_X, %142
+    %144:u32 = convert %I
+    %145:u32 = convert %J
+    %146:array<u32, 2> = construct %144, %145
+    %w_arrI_arrJ:vec4<i32> = call %fn_w_W_arr_arr_X_X, %146
+    ret
+  }
+}
+)";
+
+    Run(DirectVariableAccess, DirectVariableAccessOptions{});
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_Complex, Indexing) {
+    Var* S = nullptr;
+    b.Append(b.ir.root_block,
+             [&] {  //
+                 S = b.Var<storage, array<array<array<array<i32, 9>, 9>, 9>, 50>, read>("S");
+                 S->SetBindingPoint(0, 0);
+             });
+
+    auto* fn_a = b.Function("a", ty.i32());
+    auto* fn_a_i = b.FunctionParam("i", ty.i32());
+    fn_a->SetParams({fn_a_i});
+    b.Append(fn_a->Block(), [&] { b.Return(fn_a, fn_a_i); });
+
+    auto* fn_b = b.Function("b", ty.i32());
+    auto* fn_b_p = b.FunctionParam("p", ty.ptr<storage, array<array<array<i32, 9>, 9>, 9>, read>());
+    fn_b->SetParams({fn_b_p});
+    b.Append(fn_b->Block(), [&] {
+        auto load_0 = b.Load(b.Access(ty.ptr<storage, i32, read>(), fn_b_p, 0_i, 1_i, 2_i));
+        auto call_0 = b.Call(fn_a, load_0);
+        auto call_1 = b.Call(fn_a, 3_i);
+        auto load_1 = b.Load(b.Access(ty.ptr<storage, i32, read>(), fn_b_p, call_1, 4_i, 5_i));
+        auto call_2 = b.Call(fn_a, load_1);
+        auto call_3 = b.Call(fn_a, 7_i);
+        auto load_2 = b.Load(b.Access(ty.ptr<storage, i32, read>(), fn_b_p, 6_i, call_3, 8_i));
+        auto call_4 = b.Call(fn_a, load_2);
+        auto load_3 =
+            b.Load(b.Access(ty.ptr<storage, i32, read>(), fn_b_p, call_0, call_2, call_4));
+
+        b.Return(fn_b, load_3);
+    });
+
+    auto* fn_c = b.Function("c", ty.void_());
+    b.Append(fn_c->Block(), [&] {
+        auto* access =
+            b.Access(ty.ptr<storage, array<array<array<i32, 9>, 9>, 9>, read>(), S, 42_i);
+        auto* v = b.Call(fn_b, access);
+        b.ir.SetName(v, "v");
+        b.Return(fn_c);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %S:ptr<storage, array<array<array<array<i32, 9>, 9>, 9>, 50>, read> = var @binding_point(0, 0)
+}
+
+%a = func(%i:i32):i32 -> %b2 {
+  %b2 = block {
+    ret %i
+  }
+}
+%b = func(%p:ptr<storage, array<array<array<i32, 9>, 9>, 9>, read>):i32 -> %b3 {
+  %b3 = block {
+    %6:ptr<storage, i32, read> = access %p, 0i, 1i, 2i
+    %7:i32 = load %6
+    %8:i32 = call %a, %7
+    %9:i32 = call %a, 3i
+    %10:ptr<storage, i32, read> = access %p, %9, 4i, 5i
+    %11:i32 = load %10
+    %12:i32 = call %a, %11
+    %13:i32 = call %a, 7i
+    %14:ptr<storage, i32, read> = access %p, 6i, %13, 8i
+    %15:i32 = load %14
+    %16:i32 = call %a, %15
+    %17:ptr<storage, i32, read> = access %p, %8, %12, %16
+    %18:i32 = load %17
+    ret %18
+  }
+}
+%c = func():void -> %b4 {
+  %b4 = block {
+    %20:ptr<storage, array<array<array<i32, 9>, 9>, 9>, read> = access %S, 42i
+    %v:i32 = call %b, %20
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %S:ptr<storage, array<array<array<array<i32, 9>, 9>, 9>, 50>, read> = var @binding_point(0, 0)
+}
+
+%a = func(%i:i32):i32 -> %b2 {
+  %b2 = block {
+    ret %i
+  }
+}
+%b_S_X = func(%p_indices:array<u32, 1>):i32 -> %b3 {
+  %b3 = block {
+    %6:u32 = access %p_indices, 0u
+    %7:ptr<storage, array<array<array<i32, 9>, 9>, 9>, read> = access %S, %6
+    %8:ptr<storage, i32, read> = access %7, 0i, 1i, 2i
+    %9:i32 = load %8
+    %10:i32 = call %a, %9
+    %11:i32 = call %a, 3i
+    %12:ptr<storage, i32, read> = access %7, %11, 4i, 5i
+    %13:i32 = load %12
+    %14:i32 = call %a, %13
+    %15:i32 = call %a, 7i
+    %16:ptr<storage, i32, read> = access %7, 6i, %15, 8i
+    %17:i32 = load %16
+    %18:i32 = call %a, %17
+    %19:ptr<storage, i32, read> = access %7, %10, %14, %18
+    %20:i32 = load %19
+    ret %20
+  }
+}
+%c = func():void -> %b4 {
+  %b4 = block {
+    %22:u32 = convert 42i
+    %23:array<u32, 1> = construct %22
+    %v:i32 = call %b_S_X, %23
+    ret
+  }
+}
+)";
+
+    Run(DirectVariableAccess, DirectVariableAccessOptions{});
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_Complex, IndexingInPtrCall) {
+    Var* S = nullptr;
+    b.Append(b.ir.root_block,
+             [&] {  //
+                 S = b.Var<storage, array<array<array<array<i32, 9>, 9>, 9>, 50>, read>("S");
+                 S->SetBindingPoint(0, 0);
+             });
+
+    auto* fn_a = b.Function("a", ty.i32());
+    auto* fn_a_i = b.FunctionParam("i", ty.ptr<storage, i32, read>());
+    fn_a->SetParams({
+        b.FunctionParam("pre", ty.i32()),
+        fn_a_i,
+        b.FunctionParam("post", ty.i32()),
+    });
+    b.Append(fn_a->Block(), [&] { b.Return(fn_a, b.Load(fn_a_i)); });
+
+    auto* fn_b = b.Function("b", ty.i32());
+    auto* fn_b_p = b.FunctionParam("p", ty.ptr<storage, array<array<array<i32, 9>, 9>, 9>, read>());
+    fn_b->SetParams({fn_b_p});
+    b.Append(fn_b->Block(), [&] {
+        auto access_0 = b.Access(ty.ptr<storage, i32, read>(), fn_b_p, 0_i, 1_i, 2_i);
+        auto call_0 = b.Call(fn_a, 20_i, access_0, 30_i);
+
+        auto access_1 = b.Access(ty.ptr<storage, i32, read>(), fn_b_p, 3_i, 4_i, 5_i);
+        auto call_1 = b.Call(fn_a, 40_i, access_1, 50_i);
+
+        auto access_2 = b.Access(ty.ptr<storage, i32, read>(), fn_b_p, 6_i, 7_i, 8_i);
+        auto call_2 = b.Call(fn_a, 60_i, access_2, 70_i);
+
+        auto access_3 = b.Access(ty.ptr<storage, i32, read>(), fn_b_p, call_0, call_1, call_2);
+        auto call_3 = b.Call(fn_a, 10_i, access_3, 80_i);
+
+        b.Return(fn_b, call_3);
+    });
+
+    auto* fn_c = b.Function("c", ty.void_());
+    b.Append(fn_c->Block(), [&] {
+        auto* access =
+            b.Access(ty.ptr<storage, array<array<array<i32, 9>, 9>, 9>, read>(), S, 42_i);
+        auto* v = b.Call(fn_b, access);
+        b.ir.SetName(v, "v");
+        b.Return(fn_c);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %S:ptr<storage, array<array<array<array<i32, 9>, 9>, 9>, 50>, read> = var @binding_point(0, 0)
+}
+
+%a = func(%pre:i32, %i:ptr<storage, i32, read>, %post:i32):i32 -> %b2 {
+  %b2 = block {
+    %6:i32 = load %i
+    ret %6
+  }
+}
+%b = func(%p:ptr<storage, array<array<array<i32, 9>, 9>, 9>, read>):i32 -> %b3 {
+  %b3 = block {
+    %9:ptr<storage, i32, read> = access %p, 0i, 1i, 2i
+    %10:i32 = call %a, 20i, %9, 30i
+    %11:ptr<storage, i32, read> = access %p, 3i, 4i, 5i
+    %12:i32 = call %a, 40i, %11, 50i
+    %13:ptr<storage, i32, read> = access %p, 6i, 7i, 8i
+    %14:i32 = call %a, 60i, %13, 70i
+    %15:ptr<storage, i32, read> = access %p, %10, %12, %14
+    %16:i32 = call %a, 10i, %15, 80i
+    ret %16
+  }
+}
+%c = func():void -> %b4 {
+  %b4 = block {
+    %18:ptr<storage, array<array<array<i32, 9>, 9>, 9>, read> = access %S, 42i
+    %v:i32 = call %b, %18
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %S:ptr<storage, array<array<array<array<i32, 9>, 9>, 9>, 50>, read> = var @binding_point(0, 0)
+}
+
+%a_S_X_X_X_X = func(%pre:i32, %i_indices:array<u32, 4>, %post:i32):i32 -> %b2 {
+  %b2 = block {
+    %6:u32 = access %i_indices, 0u
+    %7:u32 = access %i_indices, 1u
+    %8:u32 = access %i_indices, 2u
+    %9:u32 = access %i_indices, 3u
+    %10:ptr<storage, i32, read> = access %S, %6, %7, %8, %9
+    %11:i32 = load %10
+    ret %11
+  }
+}
+%b_S_X = func(%p_indices:array<u32, 1>):i32 -> %b3 {
+  %b3 = block {
+    %14:u32 = access %p_indices, 0u
+    %15:u32 = convert 0i
+    %16:u32 = convert 1i
+    %17:u32 = convert 2i
+    %18:array<u32, 4> = construct %14, %15, %16, %17
+    %19:i32 = call %a_S_X_X_X_X, 20i, %18, 30i
+    %20:u32 = convert 3i
+    %21:u32 = convert 4i
+    %22:u32 = convert 5i
+    %23:array<u32, 4> = construct %14, %20, %21, %22
+    %24:i32 = call %a_S_X_X_X_X, 40i, %23, 50i
+    %25:u32 = convert 6i
+    %26:u32 = convert 7i
+    %27:u32 = convert 8i
+    %28:array<u32, 4> = construct %14, %25, %26, %27
+    %29:i32 = call %a_S_X_X_X_X, 60i, %28, 70i
+    %30:u32 = convert %19
+    %31:u32 = convert %24
+    %32:u32 = convert %29
+    %33:array<u32, 4> = construct %14, %30, %31, %32
+    %34:i32 = call %a_S_X_X_X_X, 10i, %33, 80i
+    ret %34
+  }
+}
+%c = func():void -> %b4 {
+  %b4 = block {
+    %36:u32 = convert 42i
+    %37:array<u32, 1> = construct %36
+    %v:i32 = call %b_S_X, %37
+    ret
+  }
+}
+)";
+
+    Run(DirectVariableAccess, DirectVariableAccessOptions{});
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(IR_DirectVariableAccessTest_Complex, IndexingDualPointers) {
+    Var* S = nullptr;
+    Var* U = nullptr;
+    b.Append(b.ir.root_block,
+             [&] {  //
+                 S = b.Var<storage, array<array<array<i32, 9>, 9>, 50>, read>("S");
+                 S->SetBindingPoint(0, 0);
+                 U = b.Var<uniform, array<array<array<vec4<i32>, 9>, 9>, 50>, read>("U");
+                 U->SetBindingPoint(0, 0);
+             });
+
+    auto* fn_a = b.Function("a", ty.i32());
+    auto* fn_a_i = b.FunctionParam("i", ty.i32());
+    fn_a->SetParams({fn_a_i});
+    b.Append(fn_a->Block(), [&] { b.Return(fn_a, fn_a_i); });
+
+    auto* fn_b = b.Function("b", ty.i32());
+    auto* fn_b_s = b.FunctionParam("s", ty.ptr<storage, array<array<i32, 9>, 9>, read>());
+    auto* fn_b_u = b.FunctionParam("u", ty.ptr<uniform, array<array<vec4<i32>, 9>, 9>, read>());
+    fn_b->SetParams({fn_b_s, fn_b_u});
+    b.Append(fn_b->Block(), [&] {
+        auto access_0 = b.Access(ty.ptr<uniform, vec4<i32>, read>(), fn_b_u, 0_i, 1_i);
+        auto call_0 = b.Call(fn_a, b.LoadVectorElement(access_0, 0_u));
+        auto call_1 = b.Call(fn_a, 3_i);
+
+        auto access_1 = b.Access(ty.ptr<uniform, vec4<i32>, read>(), fn_b_u, call_1, 4_i);
+        auto call_2 = b.Call(fn_a, b.LoadVectorElement(access_1, 1_u));
+
+        auto access_2 = b.Access(ty.ptr<storage, i32, read>(), fn_b_s, call_0, call_2);
+
+        b.Return(fn_b, b.Load(access_2));
+    });
+
+    auto* fn_c = b.Function("c", ty.void_());
+    b.Append(fn_c->Block(), [&] {
+        auto* access_0 = b.Access(ty.ptr<storage, array<array<i32, 9>, 9>, read>(), S, 42_i);
+        auto* access_1 = b.Access(ty.ptr<uniform, array<array<vec4<i32>, 9>, 9>, read>(), U, 24_i);
+        auto* v = b.Call(fn_b, access_0, access_1);
+        b.ir.SetName(v, "v");
+        b.Return(fn_c);
+    });
+
+    auto* src = R"(
+%b1 = block {  # root
+  %S:ptr<storage, array<array<array<i32, 9>, 9>, 50>, read> = var @binding_point(0, 0)
+  %U:ptr<uniform, array<array<array<vec4<i32>, 9>, 9>, 50>, read> = var @binding_point(0, 0)
+}
+
+%a = func(%i:i32):i32 -> %b2 {
+  %b2 = block {
+    ret %i
+  }
+}
+%b = func(%s:ptr<storage, array<array<i32, 9>, 9>, read>, %u:ptr<uniform, array<array<vec4<i32>, 9>, 9>, read>):i32 -> %b3 {
+  %b3 = block {
+    %8:ptr<uniform, vec4<i32>, read> = access %u, 0i, 1i
+    %9:i32 = load_vector_element %8, 0u
+    %10:i32 = call %a, %9
+    %11:i32 = call %a, 3i
+    %12:ptr<uniform, vec4<i32>, read> = access %u, %11, 4i
+    %13:i32 = load_vector_element %12, 1u
+    %14:i32 = call %a, %13
+    %15:ptr<storage, i32, read> = access %s, %10, %14
+    %16:i32 = load %15
+    ret %16
+  }
+}
+%c = func():void -> %b4 {
+  %b4 = block {
+    %18:ptr<storage, array<array<i32, 9>, 9>, read> = access %S, 42i
+    %19:ptr<uniform, array<array<vec4<i32>, 9>, 9>, read> = access %U, 24i
+    %v:i32 = call %b, %18, %19
+    ret
+  }
+}
+)";
+
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+%b1 = block {  # root
+  %S:ptr<storage, array<array<array<i32, 9>, 9>, 50>, read> = var @binding_point(0, 0)
+  %U:ptr<uniform, array<array<array<vec4<i32>, 9>, 9>, 50>, read> = var @binding_point(0, 0)
+}
+
+%a = func(%i:i32):i32 -> %b2 {
+  %b2 = block {
+    ret %i
+  }
+}
+%b_S_X_U_X = func(%s_indices:array<u32, 1>, %u_indices:array<u32, 1>):i32 -> %b3 {
+  %b3 = block {
+    %8:u32 = access %s_indices, 0u
+    %9:ptr<storage, array<array<i32, 9>, 9>, read> = access %S, %8
+    %10:u32 = access %u_indices, 0u
+    %11:ptr<uniform, array<array<vec4<i32>, 9>, 9>, read> = access %U, %10
+    %12:ptr<uniform, vec4<i32>, read> = access %11, 0i, 1i
+    %13:i32 = load_vector_element %12, 0u
+    %14:i32 = call %a, %13
+    %15:i32 = call %a, 3i
+    %16:ptr<uniform, vec4<i32>, read> = access %11, %15, 4i
+    %17:i32 = load_vector_element %16, 1u
+    %18:i32 = call %a, %17
+    %19:ptr<storage, i32, read> = access %9, %14, %18
+    %20:i32 = load %19
+    ret %20
+  }
+}
+%c = func():void -> %b4 {
+  %b4 = block {
+    %22:u32 = convert 42i
+    %23:array<u32, 1> = construct %22
+    %24:u32 = convert 24i
+    %25:array<u32, 1> = construct %24
+    %v:i32 = call %b_S_X_U_X, %23, %25
+    ret
+  }
+}
+)";
+
+    Run(DirectVariableAccess, DirectVariableAccessOptions{});
+
+    EXPECT_EQ(expect, str());
+}
+
+}  // namespace complex_tests
+
+}  // namespace
+}  // namespace tint::core::ir::transform
diff --git a/src/tint/lang/core/ir/transform/direct_variable_access_wgsl_test.cc b/src/tint/lang/core/ir/transform/direct_variable_access_wgsl_test.cc
new file mode 100644
index 0000000..a764250
--- /dev/null
+++ b/src/tint/lang/core/ir/transform/direct_variable_access_wgsl_test.cc
@@ -0,0 +1,2525 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// GEN_BUILD:CONDITION(tint_build_wgsl_reader && tint_build_wgsl_writer)
+
+#include "src/tint/lang/core/ir/transform/direct_variable_access.h"
+
+#include <utility>
+
+#include "src/tint/lang/core/ir/transform/helper_test.h"
+
+#include "src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.h"
+#include "src/tint/lang/wgsl/reader/reader.h"
+#include "src/tint/lang/wgsl/writer/ir_to_program/ir_to_program.h"
+#include "src/tint/lang/wgsl/writer/writer.h"
+
+namespace tint::core::ir::transform {
+namespace {
+
+using namespace tint::core::fluent_types;     // NOLINT
+using namespace tint::core::number_suffixes;  // NOLINT
+
+namespace {
+
+static constexpr DirectVariableAccessOptions kTransformPrivate = {
+    /* transform_private */ true,
+    /* transform_function */ false,
+};
+
+static constexpr DirectVariableAccessOptions kTransformFunction = {
+    /* transform_private */ false,
+    /* transform_function */ true,
+};
+
+class DirectVariableAccessTest : public TransformTestBase<testing::Test> {
+  public:
+    std::string Run(std::string in, const DirectVariableAccessOptions& options = {}) {
+        Source::File file{"test", in};
+        auto program = wgsl::reader::Parse(&file);
+        if (!program.IsValid()) {
+            return "wgsl::reader::Parse() failed: \n" + program.Diagnostics().str();
+        }
+
+        auto module = wgsl::reader::ProgramToIR(program);
+        if (!module) {
+            return "ProgramToIR() failed:\n" + module.Failure().reason.str();
+        }
+
+        auto res = DirectVariableAccess(module.Get(), options);
+        if (!res) {
+            return "DirectVariableAccess failed:\n" + res.Failure().reason.str();
+        }
+
+        auto transformed = wgsl::writer::IRToProgram(module.Get());
+        if (!transformed.IsValid()) {
+            return "wgsl::writer::IRToProgram() failed: \n" + transformed.Diagnostics().str() +
+                   "\n\nIR:\n" + ir::Disassembler(module.Get()).Disassemble() +  //
+                   "\n\nAST:\n" + Program::printer(transformed);
+        }
+
+        auto output = wgsl::writer::Generate(transformed, wgsl::writer::Options{});
+        if (!output) {
+            return "wgsl::writer::Generate() failed: \n" + output.Failure().reason.str();
+        }
+
+        return "\n" + output->wgsl;
+    }
+};
+
+}  // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+// remove uncalled
+////////////////////////////////////////////////////////////////////////////////
+namespace remove_uncalled {
+
+using IR_DirectVariableAccessWgslTest_RemoveUncalled = DirectVariableAccessTest;
+
+TEST_F(IR_DirectVariableAccessWgslTest_RemoveUncalled, PtrUniform) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+var<private> keep_me : i32 = 42i;
+
+fn u(pre : i32, p : ptr<uniform, i32>, post : i32) -> i32 {
+  return *(p);
+}
+
+)";
+
+    auto* expect = R"(
+var<private> keep_me : i32 = 42i;
+)";
+
+    auto got = Run(src);
+
+    EXPECT_EQ(expect, got);
+}
+
+TEST_F(IR_DirectVariableAccessWgslTest_RemoveUncalled, PtrStorage) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+var<private> keep_me : i32 = 42i;
+
+fn s(pre : i32, p : ptr<storage, i32>, post : i32) -> i32 {
+  return *(p);
+}
+)";
+
+    auto* expect = R"(
+var<private> keep_me : i32 = 42i;
+)";
+
+    auto got = Run(src);
+
+    EXPECT_EQ(expect, got);
+}
+
+TEST_F(IR_DirectVariableAccessWgslTest_RemoveUncalled, PtrWorkgroup) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+var<private> keep_me : i32 = 42i;
+
+fn w(pre : i32, p : ptr<workgroup, i32>, post : i32) -> i32 {
+  return *(p);
+}
+
+)";
+
+    auto* expect = R"(
+var<private> keep_me : i32 = 42i;
+)";
+
+    auto got = Run(src);
+
+    EXPECT_EQ(expect, got);
+}
+
+TEST_F(IR_DirectVariableAccessWgslTest_RemoveUncalled, PtrPrivate_Disabled) {
+    auto* src = R"(
+var<private> keep_me : i32 = 42i;
+
+fn f(pre : i32, p : ptr<private, i32>, post : i32) -> i32 {
+  return *(p);
+}
+)";
+
+    auto* expect = src;
+
+    auto got = Run(src);
+
+    EXPECT_EQ(expect, got);
+}
+
+TEST_F(IR_DirectVariableAccessWgslTest_RemoveUncalled, PtrPrivate_Enabled) {
+    auto* src = R"(
+var<private> keep_me : i32 = 42i;
+
+fn f(pre : i32, p : ptr<private, i32>, post : i32) -> i32 {
+  return *(p);
+}
+)";
+
+    auto* expect = R"(
+var<private> keep_me : i32 = 42i;
+)";
+
+    auto got = Run(src, kTransformPrivate);
+
+    EXPECT_EQ(expect, got);
+}
+
+TEST_F(IR_DirectVariableAccessWgslTest_RemoveUncalled, PtrFunction_Disabled) {
+    auto* src = R"(
+var<private> keep_me : i32 = 42i;
+
+fn f(pre : i32, p : ptr<function, i32>, post : i32) -> i32 {
+  return *(p);
+}
+)";
+
+    auto* expect = src;
+
+    auto got = Run(src);
+
+    EXPECT_EQ(expect, got);
+}
+
+TEST_F(IR_DirectVariableAccessWgslTest_RemoveUncalled, PtrFunction_Enabled) {
+    auto* src = R"(
+var<private> keep_me : i32 = 42i;
+
+fn f(pre : i32, p : ptr<function, i32>, post : i32) -> i32 {
+  return *(p);
+}
+)";
+
+    auto* expect = R"(
+var<private> keep_me : i32 = 42i;
+)";
+
+    auto got = Run(src, kTransformFunction);
+
+    EXPECT_EQ(expect, got);
+}
+
+}  // namespace remove_uncalled
+
+////////////////////////////////////////////////////////////////////////////////
+// pointer chains
+////////////////////////////////////////////////////////////////////////////////
+namespace pointer_chains_tests {
+
+using IR_DirectVariableAccessWgslTest_PtrChains = DirectVariableAccessTest;
+
+TEST_F(IR_DirectVariableAccessWgslTest_PtrChains, ConstantIndices) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+@group(0) @binding(0) var<uniform> U : array<array<array<vec4<i32>, 8>, 8>, 8>;
+
+fn a(pre : i32, p : ptr<uniform, vec4<i32>>, post : i32) -> vec4<i32> {
+  return *p;
+}
+
+fn b() {
+  let p0 = &U;
+  let p1 = &(*p0)[1];
+  let p2 = &(*p1)[1+1];
+  let p3 = &(*p2)[2*2 - 1];
+  a(10, p3, 20);
+}
+
+fn c(p : ptr<uniform, array<array<array<vec4<i32>, 8>, 8>, 8>>) {
+  let p0 = p;
+  let p1 = &(*p0)[1];
+  let p2 = &(*p1)[1+1];
+  let p3 = &(*p2)[2*2 - 1];
+  a(10, p3, 20);
+}
+
+fn d() {
+  c(&U);
+}
+)";
+
+    auto* expect =
+        R"(
+@group(0) @binding(0) var<uniform> U : array<array<array<vec4<i32>, 8u>, 8u>, 8u>;
+
+fn a_U_X_X_X(pre : i32, p_indices : array<u32, 3u>, post : i32) -> vec4<i32> {
+  return U[p_indices[0u]][p_indices[1u]][p_indices[2u]];
+}
+
+fn b() {
+  a_U_X_X_X(10i, array<u32, 3u>(u32(1i), u32(2i), u32(3i)), 20i);
+}
+
+fn c_U() {
+  a_U_X_X_X(10i, array<u32, 3u>(u32(1i), u32(2i), u32(3i)), 20i);
+}
+
+fn d() {
+  c_U();
+}
+)";
+
+    auto got = Run(src);
+
+    EXPECT_EQ(expect, got);
+}
+
+TEST_F(IR_DirectVariableAccessWgslTest_PtrChains, DynamicIndices) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+@group(0) @binding(0) var<uniform> U : array<array<array<vec4<i32>, 8>, 8>, 8>;
+
+var<private> i : i32;
+fn first() -> i32 {
+  i++;
+  return i;
+}
+fn second() -> i32 {
+  i++;
+  return i;
+}
+fn third() -> i32 {
+  i++;
+  return i;
+}
+
+fn a(pre : i32, p : ptr<uniform, vec4<i32>>, post : i32) -> vec4<i32> {
+  return *p;
+}
+
+fn b() {
+  let p0 = &U;
+  let p1 = &(*p0)[first()];
+  let p2 = &(*p1)[second()][third()];
+  a(10, p2, 20);
+}
+
+fn c(p : ptr<uniform, array<array<array<vec4<i32>, 8>, 8>, 8>>) {
+  let p0 = p;
+  let p1 = &(*p0)[first()];
+  let p2 = &(*p1)[second()][third()];
+  a(10, p2, 20);
+}
+
+fn d() {
+  c(&U);
+}
+)";
+
+    auto* expect = R"(
+@group(0) @binding(0) var<uniform> U : array<array<array<vec4<i32>, 8u>, 8u>, 8u>;
+
+var<private> i : i32;
+
+fn first() -> i32 {
+  i = (i + 1i);
+  return i;
+}
+
+fn second() -> i32 {
+  i = (i + 1i);
+  return i;
+}
+
+fn third() -> i32 {
+  i = (i + 1i);
+  return i;
+}
+
+fn a_U_X_X_X(pre : i32, p_indices : array<u32, 3u>, post : i32) -> vec4<i32> {
+  return U[p_indices[0u]][p_indices[1u]][p_indices[2u]];
+}
+
+fn b() {
+  let v = first();
+  let v_1 = second();
+  a_U_X_X_X(10i, array<u32, 3u>(u32(v), u32(v_1), u32(third())), 20i);
+}
+
+fn c_U() {
+  let v_2 = first();
+  let v_3 = second();
+  a_U_X_X_X(10i, array<u32, 3u>(u32(v_2), u32(v_3), u32(third())), 20i);
+}
+
+fn d() {
+  c_U();
+}
+)";
+
+    auto got = Run(src);
+
+    EXPECT_EQ(expect, got);
+}
+
+TEST_F(IR_DirectVariableAccessWgslTest_PtrChains, DynamicIndicesForLoopInit) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+@group(0) @binding(0) var<uniform> U : array<array<vec4<i32>, 8>, 8>;
+
+var<private> i : i32;
+fn first() -> i32 {
+  i++;
+  return i;
+}
+fn second() -> i32 {
+  i++;
+  return i;
+}
+
+fn a(pre : i32, p : ptr<uniform, vec4<i32>>, post : i32) -> vec4<i32> {
+  return *p;
+}
+
+fn b() {
+  for (let p1 = &U[first()]; true; ) {
+    a(10, &(*p1)[second()], 20);
+  }
+}
+
+fn c(p : ptr<uniform, array<array<vec4<i32>, 8>, 8>>) {
+  for (let p1 = &(*p)[first()]; true; ) {
+    a(10, &(*p1)[second()], 20);
+  }
+}
+
+fn d() {
+  c(&U);
+}
+)";
+
+    auto* expect = R"(
+@group(0) @binding(0) var<uniform> U : array<array<vec4<i32>, 8u>, 8u>;
+
+var<private> i : i32;
+
+fn first() -> i32 {
+  i = (i + 1i);
+  return i;
+}
+
+fn second() -> i32 {
+  i = (i + 1i);
+  return i;
+}
+
+fn a_U_X_X(pre : i32, p_indices : array<u32, 2u>, post : i32) -> vec4<i32> {
+  return U[p_indices[0u]][p_indices[1u]];
+}
+
+fn b() {
+  for(let v = first(); true; ) {
+    a_U_X_X(10i, array<u32, 2u>(u32(v), u32(second())), 20i);
+  }
+}
+
+fn c_U() {
+  for(let v_1 = first(); true; ) {
+    a_U_X_X(10i, array<u32, 2u>(u32(v_1), u32(second())), 20i);
+  }
+}
+
+fn d() {
+  c_U();
+}
+)";
+
+    auto got = Run(src);
+
+    EXPECT_EQ(expect, got);
+}
+
+TEST_F(IR_DirectVariableAccessWgslTest_PtrChains, DynamicIndicesForLoopCond) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+@group(0) @binding(0) var<uniform> U : array<array<vec4<i32>, 8>, 8>;
+
+var<private> i : i32;
+fn first() -> i32 {
+  i++;
+  return i;
+}
+fn second() -> i32 {
+  i++;
+  return i;
+}
+
+fn a(pre : i32, p : ptr<uniform, vec4<i32>>, post : i32) -> vec4<i32> {
+  return *p;
+}
+
+fn b() {
+  let p = &U[first()][second()];
+  for (; a(10, p, 20).x < 4; ) {
+    let body = 1;
+  }
+}
+
+fn c(p : ptr<uniform, array<array<vec4<i32>, 8>, 8>>) {
+  let p2 = &(*p)[first()][second()];
+  for (; a(10, p2, 20).x < 4; ) {
+    let body = 1;
+  }
+}
+
+fn d() {
+  c(&U);
+}
+)";
+
+    auto* expect = R"(
+@group(0) @binding(0) var<uniform> U : array<array<vec4<i32>, 8u>, 8u>;
+
+var<private> i : i32;
+
+fn first() -> i32 {
+  i = (i + 1i);
+  return i;
+}
+
+fn second() -> i32 {
+  i = (i + 1i);
+  return i;
+}
+
+fn a_U_X_X(pre : i32, p_indices : array<u32, 2u>, post : i32) -> vec4<i32> {
+  return U[p_indices[0u]][p_indices[1u]];
+}
+
+fn b() {
+  let v = first();
+  let v_1 = second();
+  while((a_U_X_X(10i, array<u32, 2u>(u32(v), u32(v_1)), 20i).x < 4i)) {
+    let body = 1i;
+  }
+}
+
+fn c_U() {
+  let v_2 = first();
+  let v_3 = second();
+  while((a_U_X_X(10i, array<u32, 2u>(u32(v_2), u32(v_3)), 20i).x < 4i)) {
+    let body = 1i;
+  }
+}
+
+fn d() {
+  c_U();
+}
+)";
+
+    auto got = Run(src);
+
+    EXPECT_EQ(expect, got);
+}
+
+TEST_F(IR_DirectVariableAccessWgslTest_PtrChains, DynamicIndicesForLoopCont) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+@group(0) @binding(0) var<uniform> U : array<array<vec4<i32>, 8>, 8>;
+
+var<private> i : i32;
+fn first() -> i32 {
+  i++;
+  return i;
+}
+fn second() -> i32 {
+  i++;
+  return i;
+}
+
+fn a(pre : i32, p : ptr<uniform, vec4<i32>>, post : i32) -> vec4<i32> {
+  return *p;
+}
+
+fn b() {
+  let p = &U[first()][second()];
+  for (var i = 0; i < 3; a(10, p, 20)) {
+    i++;
+  }
+}
+
+fn c(p : ptr<uniform, array<array<vec4<i32>, 8>, 8>>) {
+  let p2 = &(*p)[first()][second()];
+  for (var i = 0; i < 3; a(10, p2, 20)) {
+    i++;
+  }
+}
+
+fn d() {
+  c(&U);
+}
+)";
+
+    auto* expect = R"(
+@group(0) @binding(0) var<uniform> U : array<array<vec4<i32>, 8u>, 8u>;
+
+var<private> i : i32;
+
+fn first() -> i32 {
+  i = (i + 1i);
+  return i;
+}
+
+fn second() -> i32 {
+  i = (i + 1i);
+  return i;
+}
+
+fn a_U_X_X(pre : i32, p_indices : array<u32, 2u>, post : i32) -> vec4<i32> {
+  return U[p_indices[0u]][p_indices[1u]];
+}
+
+fn b() {
+  let v = first();
+  let v_1 = second();
+  for(var i : i32 = 0i; (i < 3i); a_U_X_X(10i, array<u32, 2u>(u32(v), u32(v_1)), 20i)) {
+    i = (i + 1i);
+  }
+}
+
+fn c_U() {
+  let v_2 = first();
+  let v_3 = second();
+  for(var i : i32; (i < 3i); a_U_X_X(10i, array<u32, 2u>(u32(v_2), u32(v_3)), 20i)) {
+    i = (i + 1i);
+  }
+}
+
+fn d() {
+  c_U();
+}
+)";
+
+    auto got = Run(src);
+
+    EXPECT_EQ(expect, got);
+}
+
+TEST_F(IR_DirectVariableAccessWgslTest_PtrChains, DynamicIndicesWhileCond) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+@group(0) @binding(0) var<uniform> U : array<array<vec4<i32>, 8>, 8>;
+
+var<private> i : i32;
+fn first() -> i32 {
+  i++;
+  return i;
+}
+fn second() -> i32 {
+  i++;
+  return i;
+}
+
+fn a(pre : i32, p : ptr<uniform, vec4<i32>>, post : i32) -> vec4<i32> {
+  return *p;
+}
+
+fn b() {
+  let p = &U[first()][second()];
+  while (a(10, p, 20).x < 4) {
+    let body = 1;
+  }
+}
+
+fn c(p : ptr<uniform, array<array<vec4<i32>, 8>, 8>>) {
+  let p2 = &(*p)[first()][second()];
+  while (a(10, p2, 20).x < 4) {
+    let body = 1;
+  }
+}
+
+fn d() {
+  c(&U);
+}
+)";
+
+    auto* expect = R"(
+@group(0) @binding(0) var<uniform> U : array<array<vec4<i32>, 8u>, 8u>;
+
+var<private> i : i32;
+
+fn first() -> i32 {
+  i = (i + 1i);
+  return i;
+}
+
+fn second() -> i32 {
+  i = (i + 1i);
+  return i;
+}
+
+fn a_U_X_X(pre : i32, p_indices : array<u32, 2u>, post : i32) -> vec4<i32> {
+  return U[p_indices[0u]][p_indices[1u]];
+}
+
+fn b() {
+  let v = first();
+  let v_1 = second();
+  while((a_U_X_X(10i, array<u32, 2u>(u32(v), u32(v_1)), 20i).x < 4i)) {
+    let body = 1i;
+  }
+}
+
+fn c_U() {
+  let v_2 = first();
+  let v_3 = second();
+  while((a_U_X_X(10i, array<u32, 2u>(u32(v_2), u32(v_3)), 20i).x < 4i)) {
+    let body = 1i;
+  }
+}
+
+fn d() {
+  c_U();
+}
+)";
+
+    auto got = Run(src);
+
+    EXPECT_EQ(expect, got);
+}
+}  // namespace pointer_chains_tests
+
+////////////////////////////////////////////////////////////////////////////////
+// 'uniform' address space
+////////////////////////////////////////////////////////////////////////////////
+namespace uniform_as_tests {
+
+using IR_DirectVariableAccessWgslTest_UniformAS = DirectVariableAccessTest;
+
+TEST_F(IR_DirectVariableAccessWgslTest_UniformAS, Param_ptr_i32_read) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+@group(0) @binding(0) var<uniform> U : i32;
+
+fn a(pre : i32, p : ptr<uniform, i32>, post : i32) -> i32 {
+  return *p;
+}
+
+fn b() {
+  a(10, &U, 20);
+}
+)";
+
+    auto* expect = R"(
+@group(0) @binding(0) var<uniform> U : i32;
+
+fn a_U(pre : i32, post : i32) -> i32 {
+  return U;
+}
+
+fn b() {
+  a_U(10i, 20i);
+}
+)";
+
+    auto got = Run(src);
+
+    EXPECT_EQ(expect, got);
+}
+
+TEST_F(IR_DirectVariableAccessWgslTest_UniformAS, Param_ptr_vec4i32_Via_array_DynamicRead) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+@group(0) @binding(0) var<uniform> U : array<vec4<i32>, 8>;
+
+fn a(pre : i32, p : ptr<uniform, vec4<i32>>, post : i32) -> vec4<i32> {
+  return *p;
+}
+
+fn b() {
+  let I = 3;
+  a(10, &U[I], 20);
+}
+)";
+
+    auto* expect = R"(
+@group(0) @binding(0) var<uniform> U : array<vec4<i32>, 8u>;
+
+fn a_U_X(pre : i32, p_indices : array<u32, 1u>, post : i32) -> vec4<i32> {
+  return U[p_indices[0u]];
+}
+
+fn b() {
+  let I = 3i;
+  a_U_X(10i, array<u32, 1u>(u32(I)), 20i);
+}
+)";
+
+    auto got = Run(src);
+
+    EXPECT_EQ(expect, got);
+}
+
+TEST_F(IR_DirectVariableAccessWgslTest_UniformAS, 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>,
+};
+
+@group(0) @binding(0) var<uniform> U : Outer;
+
+fn f0(p : ptr<uniform, vec4<f32>>) -> f32 {
+  return (*p).x;
+}
+
+fn f1(p : ptr<uniform, 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);
+  }
+  {
+    // call f0() with inline usage of U
+    res += f0(&U.arr[2].mat[1]);
+  }
+  {
+    // call f0() with pointer-let usage of U
+    let p_vec = &U.arr[2].mat[1];
+    res += f0(p_vec);
+  }
+  return res;
+}
+
+fn f2(p : ptr<uniform, Inner>) -> f32 {
+  let p_mat = &(*p).mat;
+  return f1(p_mat);
+}
+
+fn f3(p0 : ptr<uniform, InnerArr>, p1 : ptr<uniform, mat3x4<f32>>) -> f32 {
+  let p0_inner = &(*p0)[3];
+  return f2(p0_inner) + f1(p1);
+}
+
+fn f4(p : ptr<uniform, Outer>) -> f32 {
+  return f3(&(*p).arr, &U.mat);
+}
+
+fn b() {
+  f4(&U);
+}
+)";
+
+    auto* expect = R"(
+struct Inner {
+  mat : mat3x4<f32>,
+}
+
+struct Outer {
+  arr : array<Inner, 4u>,
+  mat : mat3x4<f32>,
+}
+
+@group(0) @binding(0) var<uniform> U : Outer;
+
+fn f0_U_mat_X(p_indices : array<u32, 1u>) -> f32 {
+  return U.mat[p_indices[0u]].x;
+}
+
+fn f0_U_arr_X_mat_X(p_indices : array<u32, 2u>) -> f32 {
+  return U.arr[p_indices[0u]].mat[p_indices[1u]].x;
+}
+
+fn f1_U_mat() -> f32 {
+  var res : f32;
+  let v = f0_U_mat_X(array<u32, 1u>(u32(1i)));
+  res = (res + v);
+  let v_1 = f0_U_mat_X(array<u32, 1u>(u32(1i)));
+  res = (res + v_1);
+  let v_2 = f0_U_arr_X_mat_X(array<u32, 2u>(u32(2i), u32(1i)));
+  res = (res + v_2);
+  let v_3 = f0_U_arr_X_mat_X(array<u32, 2u>(u32(2i), u32(1i)));
+  res = (res + v_3);
+  return res;
+}
+
+fn f1_U_arr_X_mat(p_indices : array<u32, 1u>) -> f32 {
+  let v_4 = p_indices[0u];
+  var res : f32;
+  let v_5 = f0_U_arr_X_mat_X(array<u32, 2u>(v_4, u32(1i)));
+  res = (res + v_5);
+  let v_6 = f0_U_arr_X_mat_X(array<u32, 2u>(v_4, u32(1i)));
+  res = (res + v_6);
+  let v_7 = f0_U_arr_X_mat_X(array<u32, 2u>(u32(2i), u32(1i)));
+  res = (res + v_7);
+  let v_8 = f0_U_arr_X_mat_X(array<u32, 2u>(u32(2i), u32(1i)));
+  res = (res + v_8);
+  return res;
+}
+
+fn f2_U_arr_X(p_indices : array<u32, 1u>) -> f32 {
+  return f1_U_arr_X_mat(array<u32, 1u>(p_indices[0u]));
+}
+
+fn f3_U_arr_U_mat() -> f32 {
+  return (f2_U_arr_X(array<u32, 1u>(u32(3i))) + f1_U_mat());
+}
+
+fn f4_U() -> f32 {
+  return f3_U_arr_U_mat();
+}
+
+fn b() {
+  f4_U();
+}
+)";
+
+    auto got = Run(src);
+
+    EXPECT_EQ(expect, got);
+}
+
+}  // namespace uniform_as_tests
+
+////////////////////////////////////////////////////////////////////////////////
+// 'storage' address space
+////////////////////////////////////////////////////////////////////////////////
+namespace storage_as_tests {
+
+using IR_DirectVariableAccessWgslTest_StorageAS = DirectVariableAccessTest;
+
+TEST_F(IR_DirectVariableAccessWgslTest_StorageAS, Param_ptr_i32_Via_struct_read) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+struct str {
+  i : i32,
+};
+
+@group(0) @binding(0) var<storage> S : str;
+
+fn a(pre : i32, p : ptr<storage, i32>, post : i32) -> i32 {
+  return *p;
+}
+
+fn b() {
+  a(10, &S.i, 20);
+}
+)";
+
+    auto* expect = R"(
+struct str {
+  i : i32,
+}
+
+@group(0) @binding(0) var<storage, read> S : str;
+
+fn a_S_i(pre : i32, post : i32) -> i32 {
+  return S.i;
+}
+
+fn b() {
+  a_S_i(10i, 20i);
+}
+)";
+
+    auto got = Run(src);
+
+    EXPECT_EQ(expect, got);
+}
+
+TEST_F(IR_DirectVariableAccessWgslTest_StorageAS, Param_ptr_arr_i32_Via_struct_write) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+struct str {
+  arr : array<i32, 4>,
+};
+
+@group(0) @binding(0) var<storage, read_write> S : str;
+
+fn a(pre : i32, p : ptr<storage, array<i32, 4>, read_write>, post : i32) {
+  *p = array<i32, 4>();
+}
+
+fn b() {
+  a(10, &S.arr, 20);
+}
+)";
+
+    auto* expect = R"(
+struct str {
+  arr : array<i32, 4u>,
+}
+
+@group(0) @binding(0) var<storage, read_write> S : str;
+
+fn a_S_arr(pre : i32, post : i32) {
+  S.arr = array<i32, 4u>();
+}
+
+fn b() {
+  a_S_arr(10i, 20i);
+}
+)";
+
+    auto got = Run(src);
+
+    EXPECT_EQ(expect, got);
+}
+
+TEST_F(IR_DirectVariableAccessWgslTest_StorageAS, Param_ptr_vec4i32_Via_array_DynamicWrite) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+@group(0) @binding(0) var<storage, read_write> S : array<vec4<i32>, 8>;
+
+fn a(pre : i32, p : ptr<storage, vec4<i32>, read_write>, post : i32) {
+  *p = vec4<i32>();
+}
+
+fn b() {
+  let I = 3;
+  a(10, &S[I], 20);
+}
+)";
+
+    auto* expect = R"(
+@group(0) @binding(0) var<storage, read_write> S : array<vec4<i32>, 8u>;
+
+fn a_S_X(pre : i32, p_indices : array<u32, 1u>, post : i32) {
+  S[p_indices[0u]] = vec4<i32>();
+}
+
+fn b() {
+  let I = 3i;
+  a_S_X(10i, array<u32, 1u>(u32(I)), 20i);
+}
+)";
+
+    auto got = Run(src);
+
+    EXPECT_EQ(expect, got);
+}
+
+TEST_F(IR_DirectVariableAccessWgslTest_StorageAS, 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>,
+};
+
+@group(0) @binding(0) var<storage> S : Outer;
+
+fn f0(p : ptr<storage, vec4<f32>>) -> f32 {
+  return (*p).x;
+}
+
+fn f1(p : ptr<storage, 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);
+  }
+  {
+    // call f0() with inline usage of S
+    res += f0(&S.arr[2].mat[1]);
+  }
+  {
+    // call f0() with pointer-let usage of S
+    let p_vec = &S.arr[2].mat[1];
+    res += f0(p_vec);
+  }
+  return res;
+}
+
+fn f2(p : ptr<storage, Inner>) -> f32 {
+  let p_mat = &(*p).mat;
+  return f1(p_mat);
+}
+
+fn f3(p0 : ptr<storage, InnerArr>, p1 : ptr<storage, mat3x4<f32>>) -> f32 {
+  let p0_inner = &(*p0)[3];
+  return f2(p0_inner) + f1(p1);
+}
+
+fn f4(p : ptr<storage, Outer>) -> f32 {
+  return f3(&(*p).arr, &S.mat);
+}
+
+fn b() {
+  f4(&S);
+}
+)";
+
+    auto* expect = R"(
+struct Inner {
+  mat : mat3x4<f32>,
+}
+
+struct Outer {
+  arr : array<Inner, 4u>,
+  mat : mat3x4<f32>,
+}
+
+@group(0) @binding(0) var<storage, read> S : Outer;
+
+fn f0_S_mat_X(p_indices : array<u32, 1u>) -> f32 {
+  return S.mat[p_indices[0u]].x;
+}
+
+fn f0_S_arr_X_mat_X(p_indices : array<u32, 2u>) -> f32 {
+  return S.arr[p_indices[0u]].mat[p_indices[1u]].x;
+}
+
+fn f1_S_mat() -> f32 {
+  var res : f32;
+  let v = f0_S_mat_X(array<u32, 1u>(u32(1i)));
+  res = (res + v);
+  let v_1 = f0_S_mat_X(array<u32, 1u>(u32(1i)));
+  res = (res + v_1);
+  let v_2 = f0_S_arr_X_mat_X(array<u32, 2u>(u32(2i), u32(1i)));
+  res = (res + v_2);
+  let v_3 = f0_S_arr_X_mat_X(array<u32, 2u>(u32(2i), u32(1i)));
+  res = (res + v_3);
+  return res;
+}
+
+fn f1_S_arr_X_mat(p_indices : array<u32, 1u>) -> f32 {
+  let v_4 = p_indices[0u];
+  var res : f32;
+  let v_5 = f0_S_arr_X_mat_X(array<u32, 2u>(v_4, u32(1i)));
+  res = (res + v_5);
+  let v_6 = f0_S_arr_X_mat_X(array<u32, 2u>(v_4, u32(1i)));
+  res = (res + v_6);
+  let v_7 = f0_S_arr_X_mat_X(array<u32, 2u>(u32(2i), u32(1i)));
+  res = (res + v_7);
+  let v_8 = f0_S_arr_X_mat_X(array<u32, 2u>(u32(2i), u32(1i)));
+  res = (res + v_8);
+  return res;
+}
+
+fn f2_S_arr_X(p_indices : array<u32, 1u>) -> f32 {
+  return f1_S_arr_X_mat(array<u32, 1u>(p_indices[0u]));
+}
+
+fn f3_S_arr_S_mat() -> f32 {
+  return (f2_S_arr_X(array<u32, 1u>(u32(3i))) + f1_S_mat());
+}
+
+fn f4_S() -> f32 {
+  return f3_S_arr_S_mat();
+}
+
+fn b() {
+  f4_S();
+}
+)";
+
+    auto got = Run(src);
+
+    EXPECT_EQ(expect, got);
+}
+
+}  // namespace storage_as_tests
+
+////////////////////////////////////////////////////////////////////////////////
+// 'workgroup' address space
+////////////////////////////////////////////////////////////////////////////////
+namespace workgroup_as_tests {
+
+using IR_DirectVariableAccessWgslTest_WorkgroupAS = DirectVariableAccessTest;
+
+TEST_F(IR_DirectVariableAccessWgslTest_WorkgroupAS, Param_ptr_vec4i32_Via_array_StaticRead) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+var<workgroup> W : array<vec4<i32>, 8>;
+
+fn a(pre : i32, p : ptr<workgroup, vec4<i32>>, post : i32) -> vec4<i32> {
+  return *p;
+}
+
+fn b() {
+  a(10, &W[3], 20);
+}
+)";
+
+    auto* expect = R"(
+var<workgroup> W : array<vec4<i32>, 8u>;
+
+fn a_W_X(pre : i32, p_indices : array<u32, 1u>, post : i32) -> vec4<i32> {
+  return W[p_indices[0u]];
+}
+
+fn b() {
+  a_W_X(10i, array<u32, 1u>(u32(3i)), 20i);
+}
+)";
+
+    auto got = Run(src);
+
+    EXPECT_EQ(expect, got);
+}
+
+TEST_F(IR_DirectVariableAccessWgslTest_WorkgroupAS, Param_ptr_vec4i32_Via_array_StaticWrite) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+var<workgroup> W : array<vec4<i32>, 8>;
+
+fn a(pre : i32, p : ptr<workgroup, vec4<i32>>, post : i32) {
+  *p = vec4<i32>();
+}
+
+fn b() {
+  a(10, &W[3], 20);
+}
+)";
+
+    auto* expect = R"(
+var<workgroup> W : array<vec4<i32>, 8u>;
+
+fn a_W_X(pre : i32, p_indices : array<u32, 1u>, post : i32) {
+  W[p_indices[0u]] = vec4<i32>();
+}
+
+fn b() {
+  a_W_X(10i, array<u32, 1u>(u32(3i)), 20i);
+}
+)";
+
+    auto got = Run(src);
+
+    EXPECT_EQ(expect, got);
+}
+
+TEST_F(IR_DirectVariableAccessWgslTest_WorkgroupAS, 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>,
+};
+
+var<workgroup> W : Outer;
+
+fn f0(p : ptr<workgroup, vec4<f32>>) -> f32 {
+  return (*p).x;
+}
+
+fn f1(p : ptr<workgroup, 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);
+  }
+  {
+    // call f0() with inline usage of W
+    res += f0(&W.arr[2].mat[1]);
+  }
+  {
+    // call f0() with pointer-let usage of W
+    let p_vec = &W.arr[2].mat[1];
+    res += f0(p_vec);
+  }
+  return res;
+}
+
+fn f2(p : ptr<workgroup, Inner>) -> f32 {
+  let p_mat = &(*p).mat;
+  return f1(p_mat);
+}
+
+fn f3(p0 : ptr<workgroup, InnerArr>, p1 : ptr<workgroup, mat3x4<f32>>) -> f32 {
+  let p0_inner = &(*p0)[3];
+  return f2(p0_inner) + f1(p1);
+}
+
+fn f4(p : ptr<workgroup, Outer>) -> f32 {
+  return f3(&(*p).arr, &W.mat);
+}
+
+fn b() {
+  f4(&W);
+}
+)";
+
+    auto* expect = R"(
+struct Inner {
+  mat : mat3x4<f32>,
+}
+
+struct Outer {
+  arr : array<Inner, 4u>,
+  mat : mat3x4<f32>,
+}
+
+var<workgroup> W : Outer;
+
+fn f0_W_mat_X(p_indices : array<u32, 1u>) -> f32 {
+  return W.mat[p_indices[0u]].x;
+}
+
+fn f0_W_arr_X_mat_X(p_indices : array<u32, 2u>) -> f32 {
+  return W.arr[p_indices[0u]].mat[p_indices[1u]].x;
+}
+
+fn f1_W_mat() -> f32 {
+  var res : f32;
+  let v = f0_W_mat_X(array<u32, 1u>(u32(1i)));
+  res = (res + v);
+  let v_1 = f0_W_mat_X(array<u32, 1u>(u32(1i)));
+  res = (res + v_1);
+  let v_2 = f0_W_arr_X_mat_X(array<u32, 2u>(u32(2i), u32(1i)));
+  res = (res + v_2);
+  let v_3 = f0_W_arr_X_mat_X(array<u32, 2u>(u32(2i), u32(1i)));
+  res = (res + v_3);
+  return res;
+}
+
+fn f1_W_arr_X_mat(p_indices : array<u32, 1u>) -> f32 {
+  let v_4 = p_indices[0u];
+  var res : f32;
+  let v_5 = f0_W_arr_X_mat_X(array<u32, 2u>(v_4, u32(1i)));
+  res = (res + v_5);
+  let v_6 = f0_W_arr_X_mat_X(array<u32, 2u>(v_4, u32(1i)));
+  res = (res + v_6);
+  let v_7 = f0_W_arr_X_mat_X(array<u32, 2u>(u32(2i), u32(1i)));
+  res = (res + v_7);
+  let v_8 = f0_W_arr_X_mat_X(array<u32, 2u>(u32(2i), u32(1i)));
+  res = (res + v_8);
+  return res;
+}
+
+fn f2_W_arr_X(p_indices : array<u32, 1u>) -> f32 {
+  return f1_W_arr_X_mat(array<u32, 1u>(p_indices[0u]));
+}
+
+fn f3_W_arr_W_mat() -> f32 {
+  return (f2_W_arr_X(array<u32, 1u>(u32(3i))) + f1_W_mat());
+}
+
+fn f4_W() -> f32 {
+  return f3_W_arr_W_mat();
+}
+
+fn b() {
+  f4_W();
+}
+)";
+
+    auto got = Run(src);
+
+    EXPECT_EQ(expect, got);
+}
+
+}  // namespace workgroup_as_tests
+
+////////////////////////////////////////////////////////////////////////////////
+// 'private' address space
+////////////////////////////////////////////////////////////////////////////////
+namespace private_as_tests {
+
+using IR_DirectVariableAccessWgslTest_PrivateAS = DirectVariableAccessTest;
+
+TEST_F(IR_DirectVariableAccessWgslTest_PrivateAS, Enabled_Param_ptr_i32_read) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+fn a(pre : i32, p : ptr<private, i32>, post : i32) -> i32 {
+  return *(p);
+}
+
+var<private> P : i32;
+
+fn b() {
+  a(10, &(P), 20);
+}
+)";
+
+    auto* expect = R"(
+var<private> P : i32;
+
+fn a_P(pre : i32, post : i32) -> i32 {
+  return P;
+}
+
+fn b() {
+  a_P(10i, 20i);
+}
+)";
+
+    auto got = Run(src, kTransformPrivate);
+
+    EXPECT_EQ(expect, got);
+}
+
+TEST_F(IR_DirectVariableAccessWgslTest_PrivateAS, Enabled_Param_ptr_i32_write) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+fn a(pre : i32, p : ptr<private, i32>, post : i32) {
+  *(p) = 42;
+}
+
+var<private> P : i32;
+
+fn b() {
+  a(10, &(P), 20);
+}
+)";
+
+    auto* expect = R"(
+var<private> P : i32;
+
+fn a_P(pre : i32, post : i32) {
+  P = 42i;
+}
+
+fn b() {
+  a_P(10i, 20i);
+}
+)";
+
+    auto got = Run(src, kTransformPrivate);
+
+    EXPECT_EQ(expect, got);
+}
+
+TEST_F(IR_DirectVariableAccessWgslTest_PrivateAS, Enabled_Param_ptr_i32_Via_struct_read) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+struct str {
+  i : i32,
+};
+
+fn a(pre : i32, p : ptr<private, i32>, post : i32) -> i32 {
+  return *p;
+}
+
+var<private> P : str;
+
+fn b() {
+  a(10, &P.i, 20);
+}
+)";
+
+    auto* expect = R"(
+struct str {
+  i : i32,
+}
+
+var<private> P : str;
+
+fn a_P_i(pre : i32, post : i32) -> i32 {
+  return P.i;
+}
+
+fn b() {
+  a_P_i(10i, 20i);
+}
+)";
+
+    auto got = Run(src, kTransformPrivate);
+
+    EXPECT_EQ(expect, got);
+}
+
+TEST_F(IR_DirectVariableAccessWgslTest_PrivateAS, Disabled_Param_ptr_i32_Via_struct_read) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+struct str {
+  i : i32,
+}
+
+var<private> P : str;
+
+fn a(pre : i32, p : ptr<private, i32>, post : i32) -> i32 {
+  return *(p);
+}
+
+fn b() {
+  a(10i, &(P.i), 20i);
+}
+)";
+
+    auto* expect = src;
+
+    auto got = Run(src);
+
+    EXPECT_EQ(expect, got);
+}
+
+TEST_F(IR_DirectVariableAccessWgslTest_PrivateAS, Enabled_Param_ptr_arr_i32_Via_struct_write) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+struct str {
+  arr : array<i32, 4>,
+};
+
+fn a(pre : i32, p : ptr<private, array<i32, 4>>, post : i32) {
+  *p = array<i32, 4>();
+}
+
+var<private> P : str;
+
+fn b() {
+  a(10, &P.arr, 20);
+}
+)";
+
+    auto* expect = R"(
+struct str {
+  arr : array<i32, 4u>,
+}
+
+var<private> P : str;
+
+fn a_P_arr(pre : i32, post : i32) {
+  P.arr = array<i32, 4u>();
+}
+
+fn b() {
+  a_P_arr(10i, 20i);
+}
+)";
+
+    auto got = Run(src, kTransformPrivate);
+
+    EXPECT_EQ(expect, got);
+}
+
+TEST_F(IR_DirectVariableAccessWgslTest_PrivateAS, Disabled_Param_ptr_arr_i32_Via_struct_write) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+struct str {
+  arr : array<i32, 4u>,
+}
+
+var<private> P : str;
+
+fn a(pre : i32, p : ptr<private, array<i32, 4u>>, post : i32) {
+  *(p) = array<i32, 4u>();
+}
+
+fn b() {
+  a(10i, &(P.arr), 20i);
+}
+)";
+
+    auto* expect = src;
+
+    auto got = Run(src);
+
+    EXPECT_EQ(expect, got);
+}
+
+TEST_F(IR_DirectVariableAccessWgslTest_PrivateAS, Enabled_Param_ptr_i32_mixed) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+struct str {
+  i : i32,
+};
+
+fn a(pre : i32, p : ptr<private, i32>, post : i32) -> i32 {
+  return *p;
+}
+
+var<private> Pi : i32;
+var<private> Ps : str;
+var<private> Pa : array<i32, 4>;
+
+fn b() {
+  a(10, &Pi, 20);
+  a(30, &Ps.i, 40);
+  a(50, &Pa[2], 60);
+}
+)";
+
+    auto* expect = R"(
+var<private> Pi : i32;
+
+struct str {
+  i : i32,
+}
+
+var<private> Ps : str;
+
+var<private> Pa : array<i32, 4u>;
+
+fn a_Pi(pre : i32, post : i32) -> i32 {
+  return Pi;
+}
+
+fn a_Ps_i(pre : i32, post : i32) -> i32 {
+  return Ps.i;
+}
+
+fn a_Pa_X(pre : i32, p_indices : array<u32, 1u>, post : i32) -> i32 {
+  return Pa[p_indices[0u]];
+}
+
+fn b() {
+  a_Pi(10i, 20i);
+  a_Ps_i(30i, 40i);
+  a_Pa_X(50i, array<u32, 1u>(u32(2i)), 60i);
+}
+)";
+
+    auto got = Run(src, kTransformPrivate);
+
+    EXPECT_EQ(expect, got);
+}
+
+TEST_F(IR_DirectVariableAccessWgslTest_PrivateAS, Disabled_Param_ptr_i32_mixed) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+var<private> Pi : i32;
+
+struct str {
+  i : i32,
+}
+
+var<private> Ps : str;
+
+var<private> Pa : array<i32, 4u>;
+
+fn a(pre : i32, p : ptr<private, i32>, post : i32) -> i32 {
+  return *(p);
+}
+
+fn b() {
+  a(10i, &(Pi), 20i);
+  a(30i, &(Ps.i), 40i);
+  a(50i, &(Pa[2i]), 60i);
+}
+)";
+
+    auto* expect = src;
+
+    auto got = Run(src);
+
+    EXPECT_EQ(expect, got);
+}
+
+TEST_F(IR_DirectVariableAccessWgslTest_PrivateAS, 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>,
+};
+
+var<private> P : Outer;
+
+fn f0(p : ptr<private, vec4<f32>>) -> f32 {
+  return (*p).x;
+}
+
+fn f1(p : ptr<private, 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);
+  }
+  {
+    // call f0() with inline usage of P
+    res += f0(&P.arr[2].mat[1]);
+  }
+  {
+    // call f0() with pointer-let usage of P
+    let p_vec = &P.arr[2].mat[1];
+    res += f0(p_vec);
+  }
+  return res;
+}
+
+fn f2(p : ptr<private, Inner>) -> f32 {
+  let p_mat = &(*p).mat;
+  return f1(p_mat);
+}
+
+fn f3(p0 : ptr<private, InnerArr>, p1 : ptr<private, mat3x4<f32>>) -> f32 {
+  let p0_inner = &(*p0)[3];
+  return f2(p0_inner) + f1(p1);
+}
+
+fn f4(p : ptr<private, Outer>) -> f32 {
+  return f3(&(*p).arr, &P.mat);
+}
+
+fn b() {
+  f4(&P);
+}
+)";
+
+    auto* expect = R"(
+struct Inner {
+  mat : mat3x4<f32>,
+}
+
+struct Outer {
+  arr : array<Inner, 4u>,
+  mat : mat3x4<f32>,
+}
+
+var<private> P : Outer;
+
+fn f0_P_mat_X(p_indices : array<u32, 1u>) -> f32 {
+  return P.mat[p_indices[0u]].x;
+}
+
+fn f0_P_arr_X_mat_X(p_indices : array<u32, 2u>) -> f32 {
+  return P.arr[p_indices[0u]].mat[p_indices[1u]].x;
+}
+
+fn f1_P_mat() -> f32 {
+  var res : f32;
+  let v = f0_P_mat_X(array<u32, 1u>(u32(1i)));
+  res = (res + v);
+  let v_1 = f0_P_mat_X(array<u32, 1u>(u32(1i)));
+  res = (res + v_1);
+  let v_2 = f0_P_arr_X_mat_X(array<u32, 2u>(u32(2i), u32(1i)));
+  res = (res + v_2);
+  let v_3 = f0_P_arr_X_mat_X(array<u32, 2u>(u32(2i), u32(1i)));
+  res = (res + v_3);
+  return res;
+}
+
+fn f1_P_arr_X_mat(p_indices : array<u32, 1u>) -> f32 {
+  let v_4 = p_indices[0u];
+  var res : f32;
+  let v_5 = f0_P_arr_X_mat_X(array<u32, 2u>(v_4, u32(1i)));
+  res = (res + v_5);
+  let v_6 = f0_P_arr_X_mat_X(array<u32, 2u>(v_4, u32(1i)));
+  res = (res + v_6);
+  let v_7 = f0_P_arr_X_mat_X(array<u32, 2u>(u32(2i), u32(1i)));
+  res = (res + v_7);
+  let v_8 = f0_P_arr_X_mat_X(array<u32, 2u>(u32(2i), u32(1i)));
+  res = (res + v_8);
+  return res;
+}
+
+fn f2_P_arr_X(p_indices : array<u32, 1u>) -> f32 {
+  return f1_P_arr_X_mat(array<u32, 1u>(p_indices[0u]));
+}
+
+fn f3_P_arr_P_mat() -> f32 {
+  return (f2_P_arr_X(array<u32, 1u>(u32(3i))) + f1_P_mat());
+}
+
+fn f4_P() -> f32 {
+  return f3_P_arr_P_mat();
+}
+
+fn b() {
+  f4_P();
+}
+)";
+
+    auto got = Run(src, kTransformPrivate);
+
+    EXPECT_EQ(expect, got);
+}
+
+TEST_F(IR_DirectVariableAccessWgslTest_PrivateAS, Disabled_CallChaining) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+struct Inner {
+  mat : mat3x4<f32>,
+}
+
+struct Outer {
+  arr : array<Inner, 4u>,
+  mat : mat3x4<f32>,
+}
+
+var<private> P : Outer;
+
+fn f0(p : ptr<private, vec4<f32>>) -> f32 {
+  return (*(p)).x;
+}
+
+fn f1(p : ptr<private, mat3x4<f32>>) -> f32 {
+  var res : f32;
+  let v = f0(&((*(p))[1i]));
+  res = (res + v);
+  let p_vec = &((*(p))[1i]);
+  let v_1 = f0(p_vec);
+  res = (res + v_1);
+  let v_2 = f0(&(P.arr[2i].mat[1i]));
+  res = (res + v_2);
+  let p_vec_1 = &(P.arr[2i].mat[1i]);
+  let v_3 = f0(p_vec_1);
+  res = (res + v_3);
+  return res;
+}
+
+fn f2(p : ptr<private, Inner>) -> f32 {
+  let p_mat = &((*(p)).mat);
+  return f1(p_mat);
+}
+
+fn f3(p0 : ptr<private, array<Inner, 4u>>, p1 : ptr<private, mat3x4<f32>>) -> f32 {
+  let p0_inner = &((*(p0))[3i]);
+  return (f2(p0_inner) + f1(p1));
+}
+
+fn f4(p : ptr<private, Outer>) -> f32 {
+  return f3(&((*(p)).arr), &(P.mat));
+}
+
+fn b() {
+  f4(&(P));
+}
+)";
+
+    auto* expect = src;
+
+    auto got = Run(src);
+
+    EXPECT_EQ(expect, got);
+}
+
+}  // namespace private_as_tests
+
+////////////////////////////////////////////////////////////////////////////////
+// 'function' address space
+////////////////////////////////////////////////////////////////////////////////
+namespace function_as_tests {
+
+using IR_DirectVariableAccessWgslTest_FunctionAS = DirectVariableAccessTest;
+
+TEST_F(IR_DirectVariableAccessWgslTest_FunctionAS, Enabled_LocalPtr) {
+    auto* src = R"(
+fn f() {
+  var v : i32;
+  let p = &(v);
+  var x : i32 = *(p);
+}
+)";
+
+    auto* expect = src;  // Nothing changes
+
+    auto got = Run(src);
+
+    EXPECT_EQ(expect, got);
+}
+
+TEST_F(IR_DirectVariableAccessWgslTest_FunctionAS, Enabled_Param_ptr_i32_read) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+fn a(pre : i32, p : ptr<function, i32>, post : i32) -> i32 {
+  return *(p);
+}
+
+fn b() {
+  var F : i32;
+  a(10, &(F), 20);
+}
+)";
+
+    auto* expect = R"(
+fn a_P(pre : i32, p_root : ptr<function, i32>, post : i32) -> i32 {
+  return *(p_root);
+}
+
+fn b() {
+  var F : i32;
+  a_P(10i, &(F), 20i);
+}
+)";
+
+    auto got = Run(src, kTransformFunction);
+
+    EXPECT_EQ(expect, got);
+}
+
+TEST_F(IR_DirectVariableAccessWgslTest_FunctionAS, Enabled_Param_ptr_i32_write) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+fn a(pre : i32, p : ptr<function, i32>, post : i32) {
+  *(p) = 42;
+}
+
+fn b() {
+  var F : i32;
+  a(10, &(F), 20);
+}
+)";
+
+    auto* expect = R"(
+fn a_P(pre : i32, p_root : ptr<function, i32>, post : i32) {
+  *(p_root) = 42i;
+}
+
+fn b() {
+  var F : i32;
+  a_P(10i, &(F), 20i);
+}
+)";
+
+    auto got = Run(src, kTransformFunction);
+
+    EXPECT_EQ(expect, got);
+}
+
+TEST_F(IR_DirectVariableAccessWgslTest_FunctionAS, Enabled_Param_ptr_i32_Via_struct_read) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+struct str {
+  i : i32,
+};
+
+fn a(pre : i32, p : ptr<function, i32>, post : i32) -> i32 {
+  return *p;
+}
+
+fn b() {
+  var F : str;
+  a(10, &F.i, 20);
+}
+)";
+
+    auto* expect = R"(
+struct str {
+  i : i32,
+}
+
+fn a_P_i(pre : i32, p_root : ptr<function, str>, post : i32) -> i32 {
+  return (*(p_root)).i;
+}
+
+fn b() {
+  var F : str;
+  a_P_i(10i, &(F), 20i);
+}
+)";
+
+    auto got = Run(src, kTransformFunction);
+
+    EXPECT_EQ(expect, got);
+}
+
+TEST_F(IR_DirectVariableAccessWgslTest_FunctionAS, Enabled_Param_ptr_arr_i32_Via_struct_write) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+struct str {
+  arr : array<i32, 4>,
+};
+
+fn a(pre : i32, p : ptr<function, array<i32, 4>>, post : i32) {
+  *p = array<i32, 4>();
+}
+
+fn b() {
+  var F : str;
+  a(10, &F.arr, 20);
+}
+)";
+
+    auto* expect = R"(
+struct str {
+  arr : array<i32, 4u>,
+}
+
+fn a_P_arr(pre : i32, p_root : ptr<function, str>, post : i32) {
+  (*(p_root)).arr = array<i32, 4u>();
+}
+
+fn b() {
+  var F : str;
+  a_P_arr(10i, &(F), 20i);
+}
+)";
+
+    auto got = Run(src, kTransformFunction);
+
+    EXPECT_EQ(expect, got);
+}
+
+TEST_F(IR_DirectVariableAccessWgslTest_FunctionAS, Enabled_Param_ptr_i32_mixed) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+struct str {
+  i : i32,
+};
+
+fn a(pre : i32, p : ptr<function, i32>, post : i32) -> i32 {
+  return *p;
+}
+
+fn b() {
+  var Fi : i32;
+  var Fs : str;
+  var Fa : array<i32, 4>;
+
+  a(10, &Fi, 20);
+  a(30, &Fs.i, 40);
+  a(50, &Fa[2], 60);
+}
+)";
+
+    auto* expect = R"(
+fn a_P(pre : i32, p_root : ptr<function, i32>, post : i32) -> i32 {
+  return *(p_root);
+}
+
+struct str {
+  i : i32,
+}
+
+fn a_P_i(pre : i32, p_root : ptr<function, str>, post : i32) -> i32 {
+  return (*(p_root)).i;
+}
+
+fn a_P_X(pre : i32, p_root : ptr<function, array<i32, 4u>>, p_indices : array<u32, 1u>, post : i32) -> i32 {
+  return (*(p_root))[p_indices[0u]];
+}
+
+fn b() {
+  var Fi : i32;
+  var Fs : str;
+  var Fa : array<i32, 4u>;
+  a_P(10i, &(Fi), 20i);
+  a_P_i(30i, &(Fs), 40i);
+  a_P_X(50i, &(Fa), array<u32, 1u>(u32(2i)), 60i);
+}
+)";
+
+    auto got = Run(src, kTransformFunction);
+
+    EXPECT_EQ(expect, got);
+}
+
+TEST_F(IR_DirectVariableAccessWgslTest_FunctionAS, Disabled_Param_ptr_i32_Via_struct_read) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+fn a(pre : i32, p : ptr<function, i32>, post : i32) -> i32 {
+  return *(p);
+}
+
+struct str {
+  i : i32,
+}
+
+fn b() {
+  var F : str;
+  a(10i, &(F.i), 20i);
+}
+)";
+
+    auto* expect = src;
+
+    auto got = Run(src);
+
+    EXPECT_EQ(expect, got);
+}
+
+TEST_F(IR_DirectVariableAccessWgslTest_FunctionAS, Disabled_Param_ptr_arr_i32_Via_struct_write) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+fn a(pre : i32, p : ptr<function, array<i32, 4u>>, post : i32) {
+  *(p) = array<i32, 4u>();
+}
+
+struct str {
+  arr : array<i32, 4u>,
+}
+
+fn b() {
+  var F : str;
+  a(10i, &(F.arr), 20i);
+}
+)";
+
+    auto* expect = src;
+
+    auto got = Run(src);
+
+    EXPECT_EQ(expect, got);
+}
+
+}  // namespace function_as_tests
+
+////////////////////////////////////////////////////////////////////////////////
+// builtin function calls
+////////////////////////////////////////////////////////////////////////////////
+namespace builtin_fn_calls {
+
+using IR_DirectVariableAccessWgslTest_BuiltinFn = DirectVariableAccessTest;
+
+TEST_F(IR_DirectVariableAccessWgslTest_BuiltinFn, ArrayLength) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+@group(0) @binding(0) var<storage> S : array<f32>;
+
+fn len(p : ptr<storage, array<f32>>) -> u32 {
+  return arrayLength(p);
+}
+
+fn f() {
+  let n = len(&S);
+}
+)";
+
+    auto* expect = R"(
+@group(0) @binding(0) var<storage, read> S : array<f32>;
+
+fn len_S() -> u32 {
+  return arrayLength(&(S));
+}
+
+fn f() {
+  len_S();
+}
+)";
+
+    auto got = Run(src);
+
+    EXPECT_EQ(expect, got);
+}
+
+TEST_F(IR_DirectVariableAccessWgslTest_BuiltinFn, WorkgroupUniformLoad) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+var<workgroup> W : f32;
+
+fn load(p : ptr<workgroup, f32>) -> f32 {
+  return workgroupUniformLoad(p);
+}
+
+fn f() {
+  let v = load(&W);
+}
+)";
+
+    auto* expect = R"(
+var<workgroup> W : f32;
+
+fn load_W() -> f32 {
+  return workgroupUniformLoad(&(W));
+}
+
+fn f() {
+  load_W();
+}
+)";
+
+    auto got = Run(src);
+
+    EXPECT_EQ(expect, got);
+}
+
+}  // namespace builtin_fn_calls
+
+////////////////////////////////////////////////////////////////////////////////
+// complex tests
+////////////////////////////////////////////////////////////////////////////////
+namespace complex_tests {
+
+using IR_DirectVariableAccessWgslTest_Complex = DirectVariableAccessTest;
+
+TEST_F(IR_DirectVariableAccessWgslTest_Complex, Param_ptr_mixed_vec4i32_ViaMultiple) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+struct str {
+  i : vec4<i32>,
+};
+
+@group(0) @binding(0) var<uniform> U     : vec4<i32>;
+@group(0) @binding(1) var<uniform> U_str   : str;
+@group(0) @binding(2) var<uniform> U_arr   : array<vec4<i32>, 8>;
+@group(0) @binding(3) var<uniform> U_arr_arr : array<array<vec4<i32>, 8>, 4>;
+
+@group(1) @binding(0) var<storage> S     : vec4<i32>;
+@group(1) @binding(1) var<storage> S_str   : str;
+@group(1) @binding(2) var<storage> S_arr   : array<vec4<i32>, 8>;
+@group(1) @binding(3) var<storage> S_arr_arr : array<array<vec4<i32>, 8>, 4>;
+
+          var<workgroup> W     : vec4<i32>;
+          var<workgroup> W_str   : str;
+          var<workgroup> W_arr   : array<vec4<i32>, 8>;
+          var<workgroup> W_arr_arr : array<array<vec4<i32>, 8>, 4>;
+
+fn fn_u(p : ptr<uniform, vec4<i32>>) -> vec4<i32> {
+  return *p;
+}
+
+fn fn_s(p : ptr<storage, vec4<i32>>) -> vec4<i32> {
+  return *p;
+}
+
+fn fn_w(p : ptr<workgroup, vec4<i32>>) -> vec4<i32> {
+  return *p;
+}
+
+fn b() {
+  let I = 3;
+  let J = 4;
+
+  let u           = fn_u(&U);
+  let u_str       = fn_u(&U_str.i);
+  let u_arr0      = fn_u(&U_arr[0]);
+  let u_arr1      = fn_u(&U_arr[1]);
+  let u_arrI      = fn_u(&U_arr[I]);
+  let u_arr1_arr0 = fn_u(&U_arr_arr[1][0]);
+  let u_arr2_arrI = fn_u(&U_arr_arr[2][I]);
+  let u_arrI_arr2 = fn_u(&U_arr_arr[I][2]);
+  let u_arrI_arrJ = fn_u(&U_arr_arr[I][J]);
+
+  let s           = fn_s(&S);
+  let s_str       = fn_s(&S_str.i);
+  let s_arr0      = fn_s(&S_arr[0]);
+  let s_arr1      = fn_s(&S_arr[1]);
+  let s_arrI      = fn_s(&S_arr[I]);
+  let s_arr1_arr0 = fn_s(&S_arr_arr[1][0]);
+  let s_arr2_arrI = fn_s(&S_arr_arr[2][I]);
+  let s_arrI_arr2 = fn_s(&S_arr_arr[I][2]);
+  let s_arrI_arrJ = fn_s(&S_arr_arr[I][J]);
+
+  let w           = fn_w(&W);
+  let w_str       = fn_w(&W_str.i);
+  let w_arr0      = fn_w(&W_arr[0]);
+  let w_arr1      = fn_w(&W_arr[1]);
+  let w_arrI      = fn_w(&W_arr[I]);
+  let w_arr1_arr0 = fn_w(&W_arr_arr[1][0]);
+  let w_arr2_arrI = fn_w(&W_arr_arr[2][I]);
+  let w_arrI_arr2 = fn_w(&W_arr_arr[I][2]);
+  let w_arrI_arrJ = fn_w(&W_arr_arr[I][J]);
+}
+)";
+
+    auto* expect = R"(
+@group(0) @binding(0) var<uniform> U : vec4<i32>;
+
+struct str {
+  i : vec4<i32>,
+}
+
+@group(0) @binding(1) var<uniform> U_str : str;
+
+@group(0) @binding(2) var<uniform> U_arr : array<vec4<i32>, 8u>;
+
+@group(0) @binding(3) var<uniform> U_arr_arr : array<array<vec4<i32>, 8u>, 4u>;
+
+@group(1) @binding(0) var<storage, read> S : vec4<i32>;
+
+@group(1) @binding(1) var<storage, read> S_str : str;
+
+@group(1) @binding(2) var<storage, read> S_arr : array<vec4<i32>, 8u>;
+
+@group(1) @binding(3) var<storage, read> S_arr_arr : array<array<vec4<i32>, 8u>, 4u>;
+
+var<workgroup> W : vec4<i32>;
+
+var<workgroup> W_str : str;
+
+var<workgroup> W_arr : array<vec4<i32>, 8u>;
+
+var<workgroup> W_arr_arr : array<array<vec4<i32>, 8u>, 4u>;
+
+fn fn_u_U() -> vec4<i32> {
+  return U;
+}
+
+fn fn_u_U_str_i() -> vec4<i32> {
+  return U_str.i;
+}
+
+fn fn_u_U_arr_X(p_indices : array<u32, 1u>) -> vec4<i32> {
+  return U_arr[p_indices[0u]];
+}
+
+fn fn_u_U_arr_arr_X_X(p_indices : array<u32, 2u>) -> vec4<i32> {
+  return U_arr_arr[p_indices[0u]][p_indices[1u]];
+}
+
+fn fn_s_S() -> vec4<i32> {
+  return S;
+}
+
+fn fn_s_S_str_i() -> vec4<i32> {
+  return S_str.i;
+}
+
+fn fn_s_S_arr_X(p_indices : array<u32, 1u>) -> vec4<i32> {
+  return S_arr[p_indices[0u]];
+}
+
+fn fn_s_S_arr_arr_X_X(p_indices : array<u32, 2u>) -> vec4<i32> {
+  return S_arr_arr[p_indices[0u]][p_indices[1u]];
+}
+
+fn fn_w_W() -> vec4<i32> {
+  return W;
+}
+
+fn fn_w_W_str_i() -> vec4<i32> {
+  return W_str.i;
+}
+
+fn fn_w_W_arr_X(p_indices : array<u32, 1u>) -> vec4<i32> {
+  return W_arr[p_indices[0u]];
+}
+
+fn fn_w_W_arr_arr_X_X(p_indices : array<u32, 2u>) -> vec4<i32> {
+  return W_arr_arr[p_indices[0u]][p_indices[1u]];
+}
+
+fn b() {
+  let I = 3i;
+  let J = 4i;
+  fn_u_U();
+  fn_u_U_str_i();
+  fn_u_U_arr_X(array<u32, 1u>(u32(0i)));
+  fn_u_U_arr_X(array<u32, 1u>(u32(1i)));
+  fn_u_U_arr_X(array<u32, 1u>(u32(I)));
+  fn_u_U_arr_arr_X_X(array<u32, 2u>(u32(1i), u32(0i)));
+  fn_u_U_arr_arr_X_X(array<u32, 2u>(u32(2i), u32(I)));
+  fn_u_U_arr_arr_X_X(array<u32, 2u>(u32(I), u32(2i)));
+  fn_u_U_arr_arr_X_X(array<u32, 2u>(u32(I), u32(J)));
+  fn_s_S();
+  fn_s_S_str_i();
+  fn_s_S_arr_X(array<u32, 1u>(u32(0i)));
+  fn_s_S_arr_X(array<u32, 1u>(u32(1i)));
+  fn_s_S_arr_X(array<u32, 1u>(u32(I)));
+  fn_s_S_arr_arr_X_X(array<u32, 2u>(u32(1i), u32(0i)));
+  fn_s_S_arr_arr_X_X(array<u32, 2u>(u32(2i), u32(I)));
+  fn_s_S_arr_arr_X_X(array<u32, 2u>(u32(I), u32(2i)));
+  fn_s_S_arr_arr_X_X(array<u32, 2u>(u32(I), u32(J)));
+  fn_w_W();
+  fn_w_W_str_i();
+  fn_w_W_arr_X(array<u32, 1u>(u32(0i)));
+  fn_w_W_arr_X(array<u32, 1u>(u32(1i)));
+  fn_w_W_arr_X(array<u32, 1u>(u32(I)));
+  fn_w_W_arr_arr_X_X(array<u32, 2u>(u32(1i), u32(0i)));
+  fn_w_W_arr_arr_X_X(array<u32, 2u>(u32(2i), u32(I)));
+  fn_w_W_arr_arr_X_X(array<u32, 2u>(u32(I), u32(2i)));
+  fn_w_W_arr_arr_X_X(array<u32, 2u>(u32(I), u32(J)));
+}
+)";
+
+    auto got = Run(src);
+
+    EXPECT_EQ(expect, got);
+}
+
+TEST_F(IR_DirectVariableAccessWgslTest_Complex, Indexing) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+@group(0) @binding(0) var<storage> S : array<array<array<array<i32, 9>, 9>, 9>, 50>;
+
+fn a(i : i32) -> i32 { return i; }
+
+fn b(p : ptr<storage, array<array<array<i32, 9>, 9>, 9>>) -> i32 {
+  return (*p) [ a( (*p)[0][1][2]    )]
+              [ a( (*p)[a(3)][4][5] )]
+              [ a( (*p)[6][a(7)][8] )];
+}
+
+fn c() {
+  let v = b(&S[42]);
+}
+)";
+
+    auto* expect = R"(
+@group(0) @binding(0) var<storage, read> S : array<array<array<array<i32, 9u>, 9u>, 9u>, 50u>;
+
+fn a(i : i32) -> i32 {
+  return i;
+}
+
+fn b_S_X(p_indices : array<u32, 1u>) -> i32 {
+  let v = &(S[p_indices[0u]]);
+  return (*(v))[a((*(v))[0i][1i][2i])][a((*(v))[a(3i)][4i][5i])][a((*(v))[6i][a(7i)][8i])];
+}
+
+fn c() {
+  b_S_X(array<u32, 1u>(u32(42i)));
+}
+)";
+
+    auto got = Run(src);
+
+    EXPECT_EQ(expect, got);
+}
+
+TEST_F(IR_DirectVariableAccessWgslTest_Complex, IndexingInPtrCall) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+@group(0) @binding(0) var<storage> S : array<array<array<array<i32, 9>, 9>, 9>, 50>;
+
+fn a(pre : i32, i : ptr<storage, i32>, post : i32) -> i32 {
+  return *i;
+}
+
+fn b(p : ptr<storage, array<array<array<i32, 9>, 9>, 9>>) -> i32 {
+  return a(10, &(*p)[ a( 20, &(*p)[0][1][2], 30 )]
+                    [ a( 40, &(*p)[3][4][5], 50 )]
+                    [ a( 60, &(*p)[6][7][8], 70 )], 80);
+}
+
+fn c() {
+  let v = b(&S[42]);
+}
+)";
+
+    auto* expect = R"(
+@group(0) @binding(0) var<storage, read> S : array<array<array<array<i32, 9u>, 9u>, 9u>, 50u>;
+
+fn a_S_X_X_X_X(pre : i32, i_indices : array<u32, 4u>, post : i32) -> i32 {
+  return S[i_indices[0u]][i_indices[1u]][i_indices[2u]][i_indices[3u]];
+}
+
+fn b_S_X(p_indices : array<u32, 1u>) -> i32 {
+  let v = p_indices[0u];
+  let v_1 = a_S_X_X_X_X(20i, array<u32, 4u>(v, u32(0i), u32(1i), u32(2i)), 30i);
+  let v_2 = a_S_X_X_X_X(40i, array<u32, 4u>(v, u32(3i), u32(4i), u32(5i)), 50i);
+  return a_S_X_X_X_X(10i, array<u32, 4u>(v, u32(v_1), u32(v_2), u32(a_S_X_X_X_X(60i, array<u32, 4u>(v, u32(6i), u32(7i), u32(8i)), 70i))), 80i);
+}
+
+fn c() {
+  b_S_X(array<u32, 1u>(u32(42i)));
+}
+)";
+
+    auto got = Run(src);
+
+    EXPECT_EQ(expect, got);
+}
+
+TEST_F(IR_DirectVariableAccessWgslTest_Complex, IndexingDualPointers) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+@group(0) @binding(0) var<storage> S : array<array<array<i32, 9>, 9>, 50>;
+@group(0) @binding(0) var<uniform> U : array<array<array<vec4<i32>, 9>, 9>, 50>;
+
+fn a(i : i32) -> i32 { return i; }
+
+fn b(s : ptr<storage, array<array<i32, 9>, 9>>,
+     u : ptr<uniform, array<array<vec4<i32>, 9>, 9>>) -> i32 {
+  return (*s) [ a( (*u)[0][1].x    )]
+              [ a( (*u)[a(3)][4].y )];
+}
+
+fn c() {
+  let v = b(&S[42], &U[24]);
+}
+)";
+
+    auto* expect = R"(
+@group(0) @binding(0) var<storage, read> S : array<array<array<i32, 9u>, 9u>, 50u>;
+
+@group(0) @binding(0) var<uniform> U : array<array<array<vec4<i32>, 9u>, 9u>, 50u>;
+
+fn a(i : i32) -> i32 {
+  return i;
+}
+
+fn b_S_X_U_X(s_indices : array<u32, 1u>, u_indices : array<u32, 1u>) -> i32 {
+  let v = &(U[u_indices[0u]]);
+  return S[s_indices[0u]][a((*(v))[0i][1i].x)][a((*(v))[a(3i)][4i].y)];
+}
+
+fn c() {
+  b_S_X_U_X(array<u32, 1u>(u32(42i)), array<u32, 1u>(u32(24i)));
+}
+)";
+
+    auto got = Run(src);
+
+    EXPECT_EQ(expect, got);
+}
+
+}  // namespace complex_tests
+
+}  // namespace
+}  // namespace tint::core::ir::transform
diff --git a/src/tint/lang/core/ir/user_call.cc b/src/tint/lang/core/ir/user_call.cc
index 282a5ee..641dc20 100644
--- a/src/tint/lang/core/ir/user_call.cc
+++ b/src/tint/lang/core/ir/user_call.cc
@@ -39,4 +39,11 @@
     return ctx.ir.instructions.Create<UserCall>(new_result, target, args);
 }
 
+void UserCall::SetArgs(VectorRef<Value*> arguments) {
+    auto* fn = Target();
+    ClearOperands();
+    AddOperand(UserCall::kFunctionOperandOffset, fn);
+    AddOperands(UserCall::kArgsOperandOffset, std::move(arguments));
+}
+
 }  // namespace tint::core::ir
diff --git a/src/tint/lang/core/ir/user_call.h b/src/tint/lang/core/ir/user_call.h
index f6688e2..21d2416 100644
--- a/src/tint/lang/core/ir/user_call.h
+++ b/src/tint/lang/core/ir/user_call.h
@@ -45,12 +45,16 @@
     /// @returns the call arguments
     tint::Slice<Value*> Args() override { return operands_.Slice().Offset(kArgsOperandOffset); }
 
+    /// Replaces the call arguments to @p arguments
+    /// @param arguments the new call arguments
+    void SetArgs(VectorRef<Value*> arguments);
+
     /// @returns the called function
     Function* Target() { return operands_[kFunctionOperandOffset]->As<ir::Function>(); }
 
     /// Sets called function
     /// @param target the new target of the call
-    void SetTarget(Function* target) { operands_[kFunctionOperandOffset] = target; }
+    void SetTarget(Function* target) { SetOperand(kFunctionOperandOffset, target); }
 
     /// @returns the friendly name for the instruction
     std::string FriendlyName() override { return "call"; }
diff --git a/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.cc b/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.cc
index 7115650..8160549 100644
--- a/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.cc
+++ b/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.cc
@@ -1428,7 +1428,9 @@
     Impl b(program);
     auto r = b.Build();
     if (!r) {
-        return r.Failure();
+        diag::List err = std::move(r.Failure().reason);
+        err.add_note(diag::System::IR, "AST:\n" + Program::printer(program), Source{});
+        return Failure{err};
     }
 
     return r.Move();