hlsl: Pointer support

Add `transform::InlinePointerLets` - a Transform that moves all usage of function-scope  `let` statements of a pointer type into their places of usage.

Make the HLSL writer transform pointer parameters to `inout`.

Fixed: tint:183
Change-Id: I0a7552fa6cd31c7b7691e64feae3170a81cc6c49
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/51281
Commit-Queue: Ben Clayton <bclayton@google.com>
Reviewed-by: David Neto <dneto@google.com>
diff --git a/src/BUILD.gn b/src/BUILD.gn
index 4d415b8..36f3aad 100644
--- a/src/BUILD.gn
+++ b/src/BUILD.gn
@@ -513,6 +513,8 @@
     "transform/external_texture_transform.h",
     "transform/first_index_offset.cc",
     "transform/first_index_offset.h",
+    "transform/inline_pointer_lets.cc",
+    "transform/inline_pointer_lets.h",
     "transform/manager.cc",
     "transform/manager.h",
     "transform/renamer.cc",
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 2b4254d..f928505 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -277,6 +277,8 @@
   transform/external_texture_transform.h
   transform/first_index_offset.cc
   transform/first_index_offset.h
+  transform/inline_pointer_lets.cc
+  transform/inline_pointer_lets.h
   transform/manager.cc
   transform/manager.h
   transform/renamer.cc
@@ -804,6 +806,7 @@
       transform/decompose_storage_access_test.cc
       transform/external_texture_transform_test.cc
       transform/first_index_offset_test.cc
+      transform/inline_pointer_lets_test.cc
       transform/renamer_test.cc
       transform/single_entry_point.cc
       transform/test_helper.h
diff --git a/src/transform/hlsl.cc b/src/transform/hlsl.cc
index c712e9c..d413dcf 100644
--- a/src/transform/hlsl.cc
+++ b/src/transform/hlsl.cc
@@ -27,6 +27,7 @@
 #include "src/transform/canonicalize_entry_point_io.h"
 #include "src/transform/decompose_storage_access.h"
 #include "src/transform/external_texture_transform.h"
+#include "src/transform/inline_pointer_lets.h"
 #include "src/transform/manager.h"
 
 namespace tint {
@@ -41,6 +42,7 @@
   manager.Add<DecomposeStorageAccess>();
   manager.Add<CalculateArrayLength>();
   manager.Add<ExternalTextureTransform>();
+  manager.Add<InlinePointerLets>();
   auto out = manager.Run(in, data);
   if (!out.program.IsValid()) {
     return out;
diff --git a/src/transform/inline_pointer_lets.cc b/src/transform/inline_pointer_lets.cc
new file mode 100644
index 0000000..b82d730
--- /dev/null
+++ b/src/transform/inline_pointer_lets.cc
@@ -0,0 +1,191 @@
+// Copyright 2021 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/transform/inline_pointer_lets.h"
+
+#include <memory>
+#include <unordered_map>
+#include <utility>
+
+#include "src/program_builder.h"
+#include "src/sem/block_statement.h"
+#include "src/sem/function.h"
+#include "src/sem/statement.h"
+#include "src/sem/variable.h"
+#include "src/utils/scoped_assignment.h"
+
+namespace tint {
+namespace transform {
+namespace {
+
+/// Traverses the expression `expr` looking for non-literal array indexing
+/// expressions that would affect the computed address of a pointer expression.
+/// The function-like argument `cb` is called for each found.
+/// @param program the program that owns all the expression nodes
+/// @param expr the expression to traverse
+/// @param cb a function-like object with the signature
+/// `void(const ast::Expression*)`, which is called for each array index
+/// expression
+template <typename F>
+void CollectSavedArrayIndices(const Program* program,
+                              ast::Expression* expr,
+                              F&& cb) {
+  if (auto* a = expr->As<ast::ArrayAccessorExpression>()) {
+    CollectSavedArrayIndices(program, a->array(), cb);
+
+    if (!a->idx_expr()->Is<ast::ScalarConstructorExpression>()) {
+      cb(a->idx_expr());
+    }
+    return;
+  }
+
+  if (auto* m = expr->As<ast::MemberAccessorExpression>()) {
+    CollectSavedArrayIndices(program, m->structure(), cb);
+    return;
+  }
+
+  if (auto* u = expr->As<ast::UnaryOpExpression>()) {
+    CollectSavedArrayIndices(program, u->expr(), cb);
+    return;
+  }
+
+  // Note: Other ast::Expression types can be safely ignored as they cannot be
+  // used to generate a reference or pointer.
+  // See https://gpuweb.github.io/gpuweb/wgsl/#forming-references-and-pointers
+}
+
+// PtrLet represents a `let` declaration of a pointer type.
+struct PtrLet {
+  // A map of ptr-let initializer sub-expression to the name of generated
+  // variable that holds the saved value of this sub-expression, when resolved
+  // at the point of the ptr-let declaration.
+  std::unordered_map<const ast::Expression*, Symbol> saved_vars;
+};
+
+}  // namespace
+
+InlinePointerLets::InlinePointerLets() = default;
+
+InlinePointerLets::~InlinePointerLets() = default;
+
+Output InlinePointerLets::Run(const Program* in, const DataMap&) {
+  ProgramBuilder out;
+  CloneContext ctx(&out, in);
+
+  // If not null, current_ptr_let is the current PtrLet being operated on.
+  PtrLet* current_ptr_let = nullptr;
+  // A map of the AST `let` variable to the PtrLet
+  std::unordered_map<const ast::Variable*, std::unique_ptr<PtrLet>> ptr_lets;
+
+  // Register the ast::Expression transform handler.
+  // This performs two different transformations:
+  // * Identifiers that resolve to the pointer-typed `let` declarations are
+  // replaced with the inlined (and recursively transformed) initializer
+  // expression for the `let` declaration.
+  // * Sub-expressions inside the pointer-typed `let` initializer expression
+  // that have been hoisted to a saved variable are replaced with the saved
+  // variable identifier.
+  ctx.ReplaceAll([&](ast::Expression* expr) -> ast::Expression* {
+    if (current_ptr_let) {
+      // We're currently processing the initializer expression of a
+      // pointer-typed `let` declaration. Look to see if we need to swap this
+      // Expression with a saved variable.
+      auto it = current_ptr_let->saved_vars.find(expr);
+      if (it != current_ptr_let->saved_vars.end()) {
+        return ctx.dst->Expr(it->second);
+      }
+    }
+    if (auto* ident = expr->As<ast::IdentifierExpression>()) {
+      if (auto* vu = in->Sem().Get<sem::VariableUser>(ident)) {
+        auto* var = vu->Variable()->Declaration();
+        auto it = ptr_lets.find(var);
+        if (it != ptr_lets.end()) {
+          // We've found an identifier that resolves to a `let` declaration.
+          // We need to replace this identifier with the initializer expression
+          // of the `let` declaration. Clone the initializer expression to make
+          // a copy. Note that this will call back into this ReplaceAll()
+          // handler for sub-expressions of the initializer.
+          auto* ptr_let = it->second.get();
+          // TINT_SCOPED_ASSIGNMENT provides a stack of PtrLet*, this is
+          // required to handle the 'chaining' of inlined `let`s.
+          TINT_SCOPED_ASSIGNMENT(current_ptr_let, ptr_let);
+          return ctx.Clone(var->constructor());
+        }
+      }
+    }
+    return nullptr;
+  });
+
+  // Find all the pointer-typed `let` declarations.
+  // Note that these must be function-scoped, as module-scoped `let`s are not
+  // permitted.
+  for (auto* node : in->ASTNodes().Objects()) {
+    if (auto* let = node->As<ast::VariableDeclStatement>()) {
+      if (!let->variable()->is_const()) {
+        continue;  // Not a `let` declaration. Ignore.
+      }
+
+      auto* var = in->Sem().Get(let->variable());
+      if (!var->Type()->Is<sem::Pointer>()) {
+        continue;  // Not a pointer type. Ignore.
+      }
+
+      // We're dealing with a pointer-typed `let` declaration.
+      auto ptr_let = std::make_unique<PtrLet>();
+      TINT_SCOPED_ASSIGNMENT(current_ptr_let, ptr_let.get());
+
+      auto* block = ctx.src->Sem().Get(let)->Block()->Declaration();
+
+      // Scan the initializer expression for array index expressions that need
+      // to be hoist to temporary "saved" variables.
+      CollectSavedArrayIndices(
+          ctx.src, var->Declaration()->constructor(),
+          [&](ast::Expression* idx_expr) {
+            // We have a sub-expression that needs to be saved.
+            // Create a new variable
+            auto saved_name = ctx.dst->Symbols().New(
+                ctx.src->Symbols().NameFor(var->Declaration()->symbol()) +
+                "_save");
+            auto* saved = ctx.dst->Decl(
+                ctx.dst->Const(saved_name, nullptr, ctx.Clone(idx_expr)));
+            // Place this variable after the pointer typed let. Order here is
+            // important as order-of-operations needs to be preserved.
+            // CollectSavedArrayIndices() visits the LHS of an array accessor
+            // before the index expression.
+            // Note that repeated calls to InsertAfter() with the same `after`
+            // argument will result in nodes to inserted in the order the calls
+            // are made (last call is inserted last).
+            ctx.InsertAfter(block->statements(), let, saved);
+            // Record the substitution of `idx_expr` to the saved variable with
+            // the symbol `saved_name`. This will be used by the ReplaceAll()
+            // handler above.
+            ptr_let->saved_vars.emplace(idx_expr, saved_name);
+          });
+
+      // Record the pointer-typed `let` declaration.
+      // This will be used by the ReplaceAll() handler above.
+      ptr_lets.emplace(let->variable(), std::move(ptr_let));
+      // As the original `let` declaration will be fully inlined, there's no
+      // need for the original declaration to exist. Remove it.
+      ctx.Remove(block->statements(), let);
+    }
+  }
+
+  ctx.Clone();
+
+  return Output(Program(std::move(out)));
+}
+
+}  // namespace transform
+}  // namespace tint
diff --git a/src/transform/inline_pointer_lets.h b/src/transform/inline_pointer_lets.h
new file mode 100644
index 0000000..75ca347
--- /dev/null
+++ b/src/transform/inline_pointer_lets.h
@@ -0,0 +1,52 @@
+// Copyright 2021 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_TRANSFORM_INLINE_POINTER_LETS_H_
+#define SRC_TRANSFORM_INLINE_POINTER_LETS_H_
+
+#include <string>
+#include <unordered_map>
+
+#include "src/transform/transform.h"
+
+namespace tint {
+namespace transform {
+
+/// InlinePointerLets is a Transform that moves all usage of function-scope
+/// `let` statements of a pointer type into their places of usage.
+///
+/// Parameters of a pointer type are not adjusted.
+///
+/// Note: InlinePointerLets does not operate on module-scope `let`s, as these
+/// cannot be pointers: https://gpuweb.github.io/gpuweb/wgsl/#module-constants
+/// `A module-scope let-declared constant must be of atomic-free plain type.`
+class InlinePointerLets : public Transform {
+ public:
+  /// Constructor
+  InlinePointerLets();
+
+  /// Destructor
+  ~InlinePointerLets() override;
+
+  /// Runs the transform on `program`, returning the transformation result.
+  /// @param program the source program to transform
+  /// @param data optional extra transform-specific input data
+  /// @returns the transformation result
+  Output Run(const Program* program, const DataMap& data = {}) override;
+};
+
+}  // namespace transform
+}  // namespace tint
+
+#endif  // SRC_TRANSFORM_INLINE_POINTER_LETS_H_
diff --git a/src/transform/inline_pointer_lets_test.cc b/src/transform/inline_pointer_lets_test.cc
new file mode 100644
index 0000000..6f53e62
--- /dev/null
+++ b/src/transform/inline_pointer_lets_test.cc
@@ -0,0 +1,323 @@
+// Copyright 2021 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/transform/inline_pointer_lets.h"
+
+#include <memory>
+#include <utility>
+#include <vector>
+
+#include "src/transform/test_helper.h"
+
+namespace tint {
+namespace transform {
+namespace {
+
+using InlinePointerLetsTest = TransformTest;
+
+TEST_F(InlinePointerLetsTest, EmptyModule) {
+  auto* src = "";
+  auto* expect = "";
+
+  auto got = Run<InlinePointerLets>(src);
+
+  EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(InlinePointerLetsTest, Basic) {
+  auto* src = R"(
+fn f() {
+  var v : i32;
+  let p : ptr<function, i32> = &v;
+  let x : i32 = *p;
+}
+)";
+
+  auto* expect = R"(
+fn f() {
+  var v : i32;
+  let x : i32 = *(&(v));
+}
+)";
+
+  auto got = Run<InlinePointerLets>(src);
+
+  EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(InlinePointerLetsTest, ComplexChain) {
+  auto* src = R"(
+fn f() {
+  var m : mat4x4<f32>;
+  let mp : ptr<function, mat4x4<f32>> = &m;
+  let vp : ptr<function, vec4<f32>> = &(*mp)[2];
+  let fp : ptr<function, f32> = &(*vp)[1];
+  let f : f32 = *fp;
+}
+)";
+
+  auto* expect = R"(
+fn f() {
+  var m : mat4x4<f32>;
+  let f : f32 = *(&(*(&(*(&(m))[2]))[1]));
+}
+)";
+
+  auto got = Run<InlinePointerLets>(src);
+
+  EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(InlinePointerLetsTest, Param) {
+  auto* src = R"(
+fn x(p : ptr<function, i32>) -> i32 {
+  return *p;
+}
+
+fn f() {
+  var v : i32;
+  let p : ptr<function, i32> = &v;
+  var r : i32 = x(p);
+}
+)";
+
+  auto* expect = R"(
+fn x(p : ptr<function, i32>) -> i32 {
+  return *(p);
+}
+
+fn f() {
+  var v : i32;
+  var r : i32 = x(&(v));
+}
+)";
+
+  auto got = Run<InlinePointerLets>(src);
+
+  EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(InlinePointerLetsTest, SavedVars) {
+  auto* src = R"(
+struct S {
+  i : i32;
+};
+
+fn arr() {
+  var a : array<S, 2>;
+  var i : i32 = 0;
+  var j : i32 = 0;
+  let p : ptr<function, i32> = &a[i + j].i;
+  i = 2;
+  *p = 4;
+}
+
+fn vec() {
+  var v : vec3<f32>;
+  var i : i32 = 0;
+  var j : i32 = 0;
+  let p : ptr<function, f32> = &v[i + j];
+  i = 2;
+  *p = 4.0;
+}
+
+fn mat() {
+  var m : mat3x3<f32>;
+  var i : i32 = 0;
+  var j : i32 = 0;
+  let p : ptr<function, vec3<f32>> = &m[i + j];
+  i = 2;
+  *p = vec3<f32>(4.0, 5.0, 6.0);
+}
+)";
+
+  auto* expect = R"(
+struct S {
+  i : i32;
+};
+
+fn arr() {
+  var a : array<S, 2>;
+  var i : i32 = 0;
+  var j : i32 = 0;
+  let p_save = (i + j);
+  i = 2;
+  *(&(a[p_save].i)) = 4;
+}
+
+fn vec() {
+  var v : vec3<f32>;
+  var i : i32 = 0;
+  var j : i32 = 0;
+  let p_save_1 = (i + j);
+  i = 2;
+  *(&(v[p_save_1])) = 4.0;
+}
+
+fn mat() {
+  var m : mat3x3<f32>;
+  var i : i32 = 0;
+  var j : i32 = 0;
+  let p_save_2 = (i + j);
+  i = 2;
+  *(&(m[p_save_2])) = vec3<f32>(4.0, 5.0, 6.0);
+}
+)";
+
+  auto got = Run<InlinePointerLets>(src);
+
+  EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(InlinePointerLetsTest, DontSaveLiterals) {
+  auto* src = R"(
+fn f() {
+  var arr : array<i32, 2>;
+  let p1 : ptr<function, i32> = &arr[1];
+  *p1 = 4;
+}
+)";
+
+  auto* expect = R"(
+fn f() {
+  var arr : array<i32, 2>;
+  *(&(arr[1])) = 4;
+}
+)";
+
+  auto got = Run<InlinePointerLets>(src);
+
+  EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(InlinePointerLetsTest, SavedVarsChain) {
+  auto* src = R"(
+fn f() {
+  var arr : array<array<i32, 2>, 2>;
+  let i : i32 = 0;
+  let j : i32 = 1;
+  let p : ptr<function, array<i32, 2>> = &arr[i];
+  let q : ptr<function, i32> = &(*p)[j];
+  *q = 12;
+}
+)";
+
+  auto* expect = R"(
+fn f() {
+  var arr : array<array<i32, 2>, 2>;
+  let i : i32 = 0;
+  let j : i32 = 1;
+  let p_save = i;
+  let q_save = j;
+  *(&(*(&(arr[p_save]))[q_save])) = 12;
+}
+)";
+
+  auto got = Run<InlinePointerLets>(src);
+
+  EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(InlinePointerLetsTest, MultiSavedVarsInSinglePtrLetExpr) {
+  auto* src = R"(
+fn x() -> i32 {
+  return 1;
+}
+
+fn y() -> i32 {
+  return 1;
+}
+
+fn z() -> i32 {
+  return 1;
+}
+
+struct Inner {
+  a : array<i32, 2>;
+};
+
+struct Outer {
+  a : array<Inner, 2>;
+};
+
+fn f() {
+  var arr : array<Outer, 2>;
+  let p : ptr<function, i32> = &arr[x()].a[y()].a[z()];
+  *p = 1;
+  *p = 2;
+}
+)";
+
+  auto* expect = R"(
+fn x() -> i32 {
+  return 1;
+}
+
+fn y() -> i32 {
+  return 1;
+}
+
+fn z() -> i32 {
+  return 1;
+}
+
+struct Inner {
+  a : array<i32, 2>;
+};
+
+struct Outer {
+  a : array<Inner, 2>;
+};
+
+fn f() {
+  var arr : array<Outer, 2>;
+  let p_save = x();
+  let p_save_1 = y();
+  let p_save_2 = z();
+  *(&(arr[p_save].a[p_save_1].a[p_save_2])) = 1;
+  *(&(arr[p_save].a[p_save_1].a[p_save_2])) = 2;
+}
+)";
+
+  auto got = Run<InlinePointerLets>(src);
+
+  EXPECT_EQ(expect, str(got));
+}
+
+// TODO(crbug.com/tint/819): Enable when we support inter-scope shadowing.
+TEST_F(InlinePointerLetsTest, DISABLED_ModificationAfterInline) {
+  auto* src = R"(
+fn x(p : ptr<function, i32>) -> i32 {
+  return *p;
+}
+
+fn f() {
+  var i : i32 = 1;
+  let p : ptr<function, i32> = &i;
+  if (true) {
+    var i : i32 = 2;
+    x(p);
+  }
+}
+)";
+
+  auto* expect = R"(<TODO>)";
+
+  auto got = Run<InlinePointerLets>(src);
+
+  EXPECT_EQ(expect, str(got));
+}
+}  // namespace
+}  // namespace transform
+}  // namespace tint
diff --git a/src/writer/hlsl/generator_impl.cc b/src/writer/hlsl/generator_impl.cc
index 9db64f0..8d2deaa 100644
--- a/src/writer/hlsl/generator_impl.cc
+++ b/src/writer/hlsl/generator_impl.cc
@@ -1625,7 +1625,16 @@
     }
     first = false;
 
-    auto* type = v->Type();
+    auto const* type = v->Type();
+
+    if (auto* ptr = type->As<sem::Pointer>()) {
+      // Transform pointer parameters in to `inout` parameters.
+      // The WGSL spec is highly restrictive in what can be passed in pointer
+      // parameters, which allows for this transformation. See:
+      // https://gpuweb.github.io/gpuweb/wgsl/#function-restriction
+      out << "inout ";
+      type = ptr->StoreType();
+    }
 
     // Note: WGSL only allows for StorageClass::kNone on parameters, however the
     // sanitizer transforms generates load / store functions for storage
@@ -2458,9 +2467,9 @@
     // https://docs.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-per-component-math#matrix-ordering
     out << mat->columns() << "x" << mat->rows();
   } else if (type->Is<sem::Pointer>()) {
-    // TODO(dsinclair): What do we do with pointers in HLSL?
-    // https://bugs.chromium.org/p/tint/issues/detail?id=183
-    diagnostics_.add_error("pointers not supported in HLSL");
+    TINT_ICE(diagnostics_)
+        << "Attempting to emit pointer type. These should have been removed "
+           "with the InlinePointerLets transform";
     return false;
   } else if (auto* sampler = type->As<sem::Sampler>()) {
     out << "Sampler";
@@ -2634,7 +2643,6 @@
   switch (expr->op()) {
     case ast::UnaryOp::kIndirection:
     case ast::UnaryOp::kAddressOf:
-      // TODO(crbug.com/tint/183) - support pointers
       return EmitExpression(pre, out, expr->expr());
     case ast::UnaryOp::kNot:
       out << "!";
diff --git a/src/writer/hlsl/generator_impl_function_test.cc b/src/writer/hlsl/generator_impl_function_test.cc
index 9ee9788..54865db 100644
--- a/src/writer/hlsl/generator_impl_function_test.cc
+++ b/src/writer/hlsl/generator_impl_function_test.cc
@@ -100,6 +100,25 @@
   Validate();
 }
 
+TEST_F(HlslGeneratorImplTest_Function, PtrParameter) {
+  // fn f(foo : ptr<function, f32>) -> f32 {
+  //   return *foo;
+  // }
+  Func("f", {Param("foo", ty.pointer<f32>(ast::StorageClass::kFunction))},
+       ty.f32(), {Return(Deref("foo"))});
+
+  GeneratorImpl& gen = SanitizeAndBuild();
+
+  ASSERT_TRUE(gen.Generate(out)) << gen.error();
+  EXPECT_THAT(result(), HasSubstr(R"(float f(inout float foo) {
+  return foo;
+}
+
+)"));
+
+  Validate();
+}
+
 TEST_F(HlslGeneratorImplTest_Function,
        Emit_Decoration_EntryPoint_WithInOutVars) {
   // fn frag_main([[location(0)]] foo : f32) -> [[location(1)]] f32 {
diff --git a/src/writer/hlsl/generator_impl_sanitizer_test.cc b/src/writer/hlsl/generator_impl_sanitizer_test.cc
index 6a16f4c..0c4bae7 100644
--- a/src/writer/hlsl/generator_impl_sanitizer_test.cc
+++ b/src/writer/hlsl/generator_impl_sanitizer_test.cc
@@ -36,17 +36,17 @@
   auto* ac_ty = ty.access(ast::AccessControl::kReadOnly, sb_ty);
 
   Global("sb", ac_ty, ast::StorageClass::kStorage, nullptr,
-         ast::DecorationList{
+         {
              create<ast::BindingDecoration>(0),
              create<ast::GroupDecoration>(1),
          });
 
   Func("main", ast::VariableList{}, ty.void_(),
-       ast::StatementList{
+       {
            Decl(Var("len", ty.u32(), ast::StorageClass::kNone,
                     Call("arrayLength", MemberAccessor("sb", "arr")))),
        },
-       ast::DecorationList{
+       {
            Stage(ast::PipelineStage::kFragment),
        });
 
@@ -76,10 +76,10 @@
   auto* pos = Var("pos", ty.i32(), ast::StorageClass::kNone, array_index);
 
   Func("main", ast::VariableList{}, ty.void_(),
-       ast::StatementList{
+       {
            Decl(pos),
        },
-       ast::DecorationList{
+       {
            Stage(ast::PipelineStage::kFragment),
        });
 
@@ -110,10 +110,10 @@
       Var("pos", ty.vec3<f32>(), ast::StorageClass::kNone, struct_access);
 
   Func("main", ast::VariableList{}, ty.void_(),
-       ast::StatementList{
+       {
            Decl(pos),
        },
-       ast::DecorationList{
+       {
            Stage(ast::PipelineStage::kFragment),
        });
 
@@ -137,6 +137,134 @@
 )";
   EXPECT_EQ(expect, got);
 }
+
+TEST_F(HlslSanitizerTest, InlinePtrLetsBasic) {
+  // var v : i32;
+  // let p : ptr<function, i32> = &v;
+  // let x : i32 = *p;
+  auto* v = Var("v", ty.i32());
+  auto* p =
+      Const("p", ty.pointer<i32>(ast::StorageClass::kFunction), AddressOf(v));
+  auto* x = Var("x", ty.i32(), ast::StorageClass::kNone, Deref(p));
+
+  Func("main", ast::VariableList{}, ty.void_(),
+       {
+           Decl(v),
+           Decl(p),
+           Decl(x),
+       },
+       {
+           Stage(ast::PipelineStage::kFragment),
+       });
+
+  GeneratorImpl& gen = SanitizeAndBuild();
+
+  ASSERT_TRUE(gen.Generate(out)) << gen.error();
+
+  auto got = result();
+  auto* expect = R"(void main() {
+  int v = 0;
+  int x = v;
+  return;
+}
+
+)";
+  EXPECT_EQ(expect, got);
+}
+
+TEST_F(HlslSanitizerTest, InlinePtrLetsComplexChain) {
+  // var m : mat4x4<f32>;
+  // let mp : ptr<function, mat4x4<f32>> = &m;
+  // let vp : ptr<function, vec4<f32>> = &(*mp)[2];
+  // let fp : ptr<function, f32> = &(*vp)[1];
+  // let f : f32 = *fp;
+  auto* m = Var("m", ty.mat4x4<f32>());
+  auto* mp =
+      Const("mp", ty.pointer(ty.mat4x4<f32>(), ast::StorageClass::kFunction),
+            AddressOf(m));
+  auto* vp =
+      Const("vp", ty.pointer(ty.vec4<f32>(), ast::StorageClass::kFunction),
+            AddressOf(IndexAccessor(Deref(mp), 2)));
+  auto* fp = Const("fp", ty.pointer<f32>(ast::StorageClass::kFunction),
+                   AddressOf(IndexAccessor(Deref(vp), 1)));
+  auto* f = Var("f", ty.f32(), ast::StorageClass::kNone, Deref(fp));
+
+  Func("main", ast::VariableList{}, ty.void_(),
+       {
+           Decl(m),
+           Decl(mp),
+           Decl(vp),
+           Decl(fp),
+           Decl(f),
+       },
+       {
+           Stage(ast::PipelineStage::kFragment),
+       });
+
+  GeneratorImpl& gen = SanitizeAndBuild();
+
+  ASSERT_TRUE(gen.Generate(out)) << gen.error();
+
+  auto got = result();
+  auto* expect = R"(void main() {
+  float4x4 m = float4x4(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f);
+  float f = m[2][1];
+  return;
+}
+
+)";
+  EXPECT_EQ(expect, got);
+}
+
+TEST_F(HlslSanitizerTest, InlineParam) {
+  // fn x(p : ptr<function, i32>) -> i32 {
+  //   return *p;
+  // }
+  //
+  // [[stage(fragment)]]
+  // fn main() {
+  //   var v : i32;
+  //   let p : ptr<function, i32> = &v;
+  //   var r : i32 = x(p);
+  // }
+
+  Func("x", {Param("p", ty.pointer<i32>(ast::StorageClass::kFunction))},
+       ty.i32(), {Return(Deref("p"))});
+
+  auto* v = Var("v", ty.i32());
+  auto* p = Const("p", ty.pointer(ty.i32(), ast::StorageClass::kFunction),
+                  AddressOf(v));
+  auto* r = Var("r", ty.i32(), ast::StorageClass::kNone, Call("x", p));
+
+  Func("main", ast::VariableList{}, ty.void_(),
+       {
+           Decl(v),
+           Decl(p),
+           Decl(r),
+       },
+       {
+           Stage(ast::PipelineStage::kFragment),
+       });
+
+  GeneratorImpl& gen = SanitizeAndBuild();
+
+  ASSERT_TRUE(gen.Generate(out)) << gen.error();
+
+  auto got = result();
+  auto* expect = R"(int x(inout int p) {
+  return p;
+}
+
+void main() {
+  int v = 0;
+  int r = x(v);
+  return;
+}
+
+)";
+  EXPECT_EQ(expect, got);
+}
+
 }  // namespace
 }  // namespace hlsl
 }  // namespace writer
diff --git a/test/BUILD.gn b/test/BUILD.gn
index 88723f7..8ea99a1 100644
--- a/test/BUILD.gn
+++ b/test/BUILD.gn
@@ -301,6 +301,7 @@
     "../src/transform/decompose_storage_access_test.cc",
     "../src/transform/external_texture_transform_test.cc",
     "../src/transform/first_index_offset_test.cc",
+    "../src/transform/inline_pointer_lets_test.cc",
     "../src/transform/renamer_test.cc",
     "../src/transform/single_entry_point_test.cc",
     "../src/transform/transform_test.cc",
diff --git a/test/ptr_ref/access/matrix.spvasm.expected.hlsl b/test/ptr_ref/access/matrix.spvasm.expected.hlsl
index b3db42f..a2e1de6 100644
--- a/test/ptr_ref/access/matrix.spvasm.expected.hlsl
+++ b/test/ptr_ref/access/matrix.spvasm.expected.hlsl
@@ -1 +1,8 @@
-SKIP: Failed to generate: error: pointers not supported in HLSL
+[numthreads(1, 1, 1)]
+void main() {
+  float3x3 m = float3x3(float3(0.0f, 0.0f, 0.0f), float3(0.0f, 0.0f, 0.0f), float3(0.0f, 0.0f, 0.0f));
+  m = float3x3(float3(1.0f, 2.0f, 3.0f), float3(4.0f, 5.0f, 6.0f), float3(7.0f, 8.0f, 9.0f));
+  m[1] = float3(5.0f, 5.0f, 5.0f);
+  return;
+}
+
diff --git a/test/ptr_ref/access/matrix.wgsl.expected.hlsl b/test/ptr_ref/access/matrix.wgsl.expected.hlsl
index b3db42f..731d3d3 100644
--- a/test/ptr_ref/access/matrix.wgsl.expected.hlsl
+++ b/test/ptr_ref/access/matrix.wgsl.expected.hlsl
@@ -1 +1,7 @@
-SKIP: Failed to generate: error: pointers not supported in HLSL
+[numthreads(1, 1, 1)]
+void main() {
+  float3x3 m = float3x3(float3(1.0f, 2.0f, 3.0f), float3(4.0f, 5.0f, 6.0f), float3(7.0f, 8.0f, 9.0f));
+  m[1] = float3(5.0f, 5.0f, 5.0f);
+  return;
+}
+
diff --git a/test/ptr_ref/access/vector.spvasm.expected.hlsl b/test/ptr_ref/access/vector.spvasm.expected.hlsl
index b3db42f..018e6ec 100644
--- a/test/ptr_ref/access/vector.spvasm.expected.hlsl
+++ b/test/ptr_ref/access/vector.spvasm.expected.hlsl
@@ -1 +1,8 @@
-SKIP: Failed to generate: error: pointers not supported in HLSL
+[numthreads(1, 1, 1)]
+void main() {
+  float3 v = float3(0.0f, 0.0f, 0.0f);
+  v = float3(1.0f, 2.0f, 3.0f);
+  v.y = 5.0f;
+  return;
+}
+
diff --git a/test/ptr_ref/access/vector.wgsl.expected.hlsl b/test/ptr_ref/access/vector.wgsl.expected.hlsl
index b3db42f..a455543 100644
--- a/test/ptr_ref/access/vector.wgsl.expected.hlsl
+++ b/test/ptr_ref/access/vector.wgsl.expected.hlsl
@@ -1 +1,7 @@
-SKIP: Failed to generate: error: pointers not supported in HLSL
+[numthreads(1, 1, 1)]
+void main() {
+  float3 v = float3(1.0f, 2.0f, 3.0f);
+  v.y = 5.0f;
+  return;
+}
+
diff --git a/test/ptr_ref/copy/ptr_copy.spvasm.expected.hlsl b/test/ptr_ref/copy/ptr_copy.spvasm.expected.hlsl
index 16f9b25..fb8e9c9 100644
--- a/test/ptr_ref/copy/ptr_copy.spvasm.expected.hlsl
+++ b/test/ptr_ref/copy/ptr_copy.spvasm.expected.hlsl
@@ -1 +1,6 @@
-SKIP: error: pointers not supported in HLSL
+[numthreads(1, 1, 1)]
+void main() {
+  uint x_10 = 0u;
+  return;
+}
+
diff --git a/test/ptr_ref/load/param/ptr.spvasm.expected.hlsl b/test/ptr_ref/load/param/ptr.spvasm.expected.hlsl
index 16f9b25..3f92e89 100644
--- a/test/ptr_ref/load/param/ptr.spvasm.expected.hlsl
+++ b/test/ptr_ref/load/param/ptr.spvasm.expected.hlsl
@@ -1 +1,14 @@
-SKIP: error: pointers not supported in HLSL
+int func(int value, inout int pointer) {
+  const int x_9 = pointer;
+  return (value + x_9);
+}
+
+[numthreads(1, 1, 1)]
+void main() {
+  int i = 0;
+  i = 123;
+  const int x_19 = i;
+  const int x_18 = func(x_19, i);
+  return;
+}
+
diff --git a/test/ptr_ref/load/param/ptr.wgsl.expected.hlsl b/test/ptr_ref/load/param/ptr.wgsl.expected.hlsl
index 16f9b25..1b60949 100644
--- a/test/ptr_ref/load/param/ptr.wgsl.expected.hlsl
+++ b/test/ptr_ref/load/param/ptr.wgsl.expected.hlsl
@@ -1 +1,11 @@
-SKIP: error: pointers not supported in HLSL
+int func(int value, inout int pointer) {
+  return (value + pointer);
+}
+
+[numthreads(1, 1, 1)]
+void main() {
+  int i = 123;
+  const int r = func(i, i);
+  return;
+}
+
diff --git a/test/ptr_ref/store/local/i32.wgsl.expected.hlsl b/test/ptr_ref/store/local/i32.wgsl.expected.hlsl
index b3db42f..314f128 100644
--- a/test/ptr_ref/store/local/i32.wgsl.expected.hlsl
+++ b/test/ptr_ref/store/local/i32.wgsl.expected.hlsl
@@ -1 +1,8 @@
-SKIP: Failed to generate: error: pointers not supported in HLSL
+[numthreads(1, 1, 1)]
+void main() {
+  int i = 123;
+  i = 123;
+  i = ((100 + 20) + 3);
+  return;
+}
+
diff --git a/test/ptr_ref/store/param/ptr.spvasm.expected.hlsl b/test/ptr_ref/store/param/ptr.spvasm.expected.hlsl
index 16f9b25..5fafc80 100644
--- a/test/ptr_ref/store/param/ptr.spvasm.expected.hlsl
+++ b/test/ptr_ref/store/param/ptr.spvasm.expected.hlsl
@@ -1 +1,13 @@
-SKIP: error: pointers not supported in HLSL
+void func(int value, inout int pointer) {
+  pointer = value;
+  return;
+}
+
+[numthreads(1, 1, 1)]
+void main() {
+  int i = 0;
+  i = 123;
+  func(123, i);
+  return;
+}
+
diff --git a/test/ptr_ref/store/param/ptr.wgsl.expected.hlsl b/test/ptr_ref/store/param/ptr.wgsl.expected.hlsl
index 16f9b25..2c3427a 100644
--- a/test/ptr_ref/store/param/ptr.wgsl.expected.hlsl
+++ b/test/ptr_ref/store/param/ptr.wgsl.expected.hlsl
@@ -1 +1,11 @@
-SKIP: error: pointers not supported in HLSL
+void func(int value, inout int pointer) {
+  pointer = value;
+}
+
+[numthreads(1, 1, 1)]
+void main() {
+  int i = 123;
+  func(123, i);
+  return;
+}
+