Handle complex cases in Inspector::GenerateSamplerTargets

This code was implicitly assuming that all resources it was looking
for would be directly referenced at the intrinsic callsite, and not
passed via function parameters.

This was causing a crash in more complex cases.

The inspector code has been updated to handle cases where the
resources are not being directly referenced.

Unneeded calls to GenerateSamplerTargets() are removed.

Utility function GetOriginatingResources() is added to handle walking up
call sites to resolve resources.

Text shader based test runner is added to the Inspector tests to make
expressing complex tests easier.

BUG=tint:967

Change-Id: I2ecb6d57c518003da59f38b261bae4d62ce7e6ac
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/59340
Kokoro: Kokoro <noreply+kokoro@google.com>
Commit-Queue: Ryan Harrison <rharrison@chromium.org>
Auto-Submit: Ryan Harrison <rharrison@chromium.org>
Reviewed-by: Ben Clayton <bclayton@google.com>
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index fbf50af..d1bcece 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -632,6 +632,8 @@
     inspector/inspector_test.cc
     inspector/test_inspector_builder.cc
     inspector/test_inspector_builder.h
+    inspector/test_inspector_runner.cc
+    inspector/test_inspector_runner.h
     intrinsic_table_test.cc
     program_test.cc
     resolver/assignment_validation_test.cc
diff --git a/src/inspector/inspector.cc b/src/inspector/inspector.cc
index d02f5bf..de1f65e 100644
--- a/src/inspector/inspector.cc
+++ b/src/inspector/inspector.cc
@@ -44,6 +44,7 @@
 #include "src/sem/vector_type.h"
 #include "src/sem/void_type.h"
 #include "src/utils/math.h"
+#include "src/utils/unique_vector.h"
 
 namespace tint {
 namespace inspector {
@@ -394,8 +395,6 @@
     return {};
   }
 
-  GenerateSamplerTargets();
-
   std::vector<ResourceBinding> result;
 
   auto* func_sem = program_->Sem().Get(func);
@@ -420,8 +419,6 @@
     return {};
   }
 
-  GenerateSamplerTargets();
-
   std::vector<ResourceBinding> result;
 
   auto* func_sem = program_->Sem().Get(func);
@@ -809,24 +806,133 @@
       continue;
     }
 
-    auto* s = c->params()[sampler_index];
-    auto* sampler = sem.Get<sem::VariableUser>(s)->Variable();
-    sem::BindingPoint sampler_binding_point = {
-        sampler->Declaration()->binding_point().group->value(),
-        sampler->Declaration()->binding_point().binding->value()};
-
     auto* t = c->params()[texture_index];
-    auto* texture = sem.Get<sem::VariableUser>(t)->Variable();
-    sem::BindingPoint texture_binding_point = {
-        texture->Declaration()->binding_point().group->value(),
-        texture->Declaration()->binding_point().binding->value()};
+    auto* s = c->params()[sampler_index];
 
-    for (auto entry_point : entry_points) {
-      const auto& ep_name = program_->Symbols().NameFor(entry_point);
-      (*sampler_targets_)[ep_name].add(
-          {sampler_binding_point, texture_binding_point});
+    GetOriginatingResources(
+        std::array<const ast::Expression*, 2>{t, s},
+        [&](std::array<const sem::GlobalVariable*, 2> globals) {
+          auto* texture = globals[0];
+          sem::BindingPoint texture_binding_point = {
+              texture->Declaration()->binding_point().group->value(),
+              texture->Declaration()->binding_point().binding->value()};
+
+          auto* sampler = globals[1];
+          sem::BindingPoint sampler_binding_point = {
+              sampler->Declaration()->binding_point().group->value(),
+              sampler->Declaration()->binding_point().binding->value()};
+
+          for (auto entry_point : entry_points) {
+            const auto& ep_name = program_->Symbols().NameFor(entry_point);
+            (*sampler_targets_)[ep_name].add(
+                {sampler_binding_point, texture_binding_point});
+          }
+        });
+  }
+}
+
+template <size_t N, typename F>
+void Inspector::GetOriginatingResources(
+    std::array<const ast::Expression*, N> exprs,
+    F&& callback) {
+  if (!program_->IsValid()) {
+    TINT_ICE(Inspector, diagnostics_)
+        << "attempting to get originating resources in invalid program";
+    return;
+  }
+
+  auto& sem = program_->Sem();
+
+  std::array<const sem::GlobalVariable*, N> globals{};
+  std::array<const sem::Parameter*, N> parameters{};
+  UniqueVector<const ast::CallExpression*> callsites;
+
+  for (size_t i = 0; i < N; i++) {
+    auto*& expr = exprs[i];
+    // Resolve each of the expressions
+    while (true) {
+      if (auto* user = sem.Get<sem::VariableUser>(expr)) {
+        auto* var = user->Variable();
+
+        if (auto* global = tint::As<sem::GlobalVariable>(var)) {
+          // Found the global resource declaration.
+          globals[i] = global;
+          break;  // Done with this expression.
+        }
+
+        if (auto* local = tint::As<sem::LocalVariable>(var)) {
+          // Chase the variable
+          expr = local->Declaration()->constructor();
+          if (!expr) {
+            TINT_ICE(Inspector, diagnostics_)
+                << "resource variable had no initializer";
+            return;
+          }
+          continue;  // Continue chasing the expression in this function
+        }
+
+        if (auto* param = tint::As<sem::Parameter>(var)) {
+          // Gather each of the callers of this function
+          auto* func = tint::As<sem::Function>(param->Owner());
+          if (func->CallSites().empty()) {
+            // One or more of the expressions is a parameter, but this function
+            // is not called. Ignore.
+            return;
+          }
+          for (auto* call_expr : func->CallSites()) {
+            callsites.add(call_expr);
+          }
+          // Need to evaluate each function call with the group of
+          // expressions, so move on to the next expression.
+          parameters[i] = param;
+          break;
+        }
+
+        TINT_ICE(Inspector, diagnostics_)
+            << "unexpected variable type " << var->TypeInfo().name;
+      }
+
+      if (auto* unary = tint::As<ast::UnaryOpExpression>(expr)) {
+        switch (unary->op()) {
+          case ast::UnaryOp::kAddressOf:
+          case ast::UnaryOp::kIndirection:
+            // `*` and `&` are the only valid unary ops for a resource type,
+            // and must be balanced in order for the program to have passed
+            // validation. Just skip past these.
+            expr = unary->expr();
+            continue;
+          default: {
+            TINT_ICE(Inspector, diagnostics_)
+                << "unexpected unary op on resource: " << unary->op();
+            return;
+          }
+        }
+      }
+
+      TINT_ICE(Inspector, diagnostics_)
+          << "cannot resolve originating resource with expression type "
+          << expr->TypeInfo().name;
+      return;
     }
   }
+
+  if (callsites.size()) {
+    for (auto* call_expr : callsites) {
+      // Make a copy of the expressions for this callsite
+      std::array<const ast::Expression*, N> call_exprs = exprs;
+      // Patch all the parameter expressions with their argument
+      for (size_t i = 0; i < N; i++) {
+        if (auto* param = parameters[i]) {
+          call_exprs[i] = call_expr->params()[param->Index()];
+        }
+      }
+      // Now call GetOriginatingResources() with from the callsite
+      GetOriginatingResources(call_exprs, callback);
+    }
+  } else {
+    // All the expressions resolved to globals
+    callback(globals);
+  }
 }
 
 }  // namespace inspector
diff --git a/src/inspector/inspector.h b/src/inspector/inspector.h
index 613bd0b..ce8ce63 100644
--- a/src/inspector/inspector.h
+++ b/src/inspector/inspector.h
@@ -210,6 +210,21 @@
 
   /// Constructes |sampler_targets_| if it hasn't already been instantiated.
   void GenerateSamplerTargets();
+
+  /// For a N-uple of expressions, resolve to the appropriate global resources
+  /// and call 'cb'.
+  /// 'cb' may be called multiple times.
+  /// Assumes that not being able to resolve the resources is an error, so will
+  /// invoke TINT_ICE when that occurs.
+  /// @tparam N number of expressions in the n-uple
+  /// @tparam F type of the callback provided.
+  /// @param exprs N-uple of expressions to resolve.
+  /// @param cb is a callback function with the signature:
+  /// `void(std::array<const sem::GlobalVariable*, N>)`, which is invoked
+  /// whenever a set of expressions are resolved to globals.
+  template <size_t N, typename F>
+  void GetOriginatingResources(std::array<const ast::Expression*, N> exprs,
+                               F&& cb);
 };
 
 }  // namespace inspector
diff --git a/src/inspector/inspector_test.cc b/src/inspector/inspector_test.cc
index 91d120a..9e43df5 100644
--- a/src/inspector/inspector_test.cc
+++ b/src/inspector/inspector_test.cc
@@ -20,6 +20,7 @@
 #include "src/ast/struct_block_decoration.h"
 #include "src/ast/workgroup_decoration.h"
 #include "src/inspector/test_inspector_builder.h"
+#include "src/inspector/test_inspector_runner.h"
 #include "src/program_builder.h"
 #include "src/sem/depth_texture_type.h"
 #include "src/sem/external_texture_type.h"
@@ -34,8 +35,15 @@
 
 // All the tests that descend from InspectorBuilder are expected to define their
 // test state via building up the AST through InspectorBuilder and then generate
-// the program with ::Build().
-// The returned Inspector from ::Build() can then be used to test expecations.
+// the program with ::Build.
+// The returned Inspector from ::Build can then be used to test expecations.
+//
+// All the tests that descend from InspectorRunner are expected to define their
+// test state via a WGSL shader, which will be parsed to generate a Program and
+// Inspector in ::Initialize.
+// The returned Inspector from ::Initialize can then be used to test
+// expecations.
+
 class InspectorGetEntryPointTest : public InspectorBuilder,
                                    public testing::Test {};
 
@@ -135,12 +143,16 @@
 class InspectorGetExternalTextureResourceBindingsTest : public InspectorBuilder,
                                                         public testing::Test {};
 
-class InspectorGetSamplerTextureUsesTest : public InspectorBuilder,
+class InspectorGetSamplerTextureUsesTest : public InspectorRunner,
                                            public testing::Test {};
 
 class InspectorGetWorkgroupStorageSizeTest : public InspectorBuilder,
                                              public testing::Test {};
 
+// This is a catch all for shaders that have demonstrated regressions/crashes in
+// the wild.
+class InspectorRegressionTest : public InspectorRunner, public testing::Test {};
+
 TEST_F(InspectorGetEntryPointTest, NoFunctions) {
   Inspector& inspector = Build();
 
@@ -2353,34 +2365,31 @@
 }
 
 TEST_F(InspectorGetSamplerTextureUsesTest, None) {
-  MakeEmptyBodyFunction("ep_func", ast::DecorationList{
-                                       Stage(ast::PipelineStage::kFragment),
-                                   });
+  std::string shader = R"(
+[[stage(fragment)]]
+fn main() {
+})";
 
-  Inspector& inspector = Build();
-
-  auto result = inspector.GetSamplerTextureUses("ep_func");
+  Inspector& inspector = Initialize(shader);
+  auto result = inspector.GetSamplerTextureUses("main");
   ASSERT_FALSE(inspector.has_error()) << inspector.error();
 
   ASSERT_EQ(0u, result.size());
 }
 
 TEST_F(InspectorGetSamplerTextureUsesTest, Simple) {
-  auto* sampled_texture_type =
-      ty.sampled_texture(ast::TextureDimension::k1d, ty.f32());
-  AddResource("foo_texture", sampled_texture_type, 0, 10);
-  AddSampler("foo_sampler", 0, 1);
-  AddGlobalVariable("foo_coords", ty.f32());
+  std::string shader = R"(
+[[group(0), binding(1)]] var mySampler: sampler;
+[[group(0), binding(2)]] var myTexture: texture_2d<f32>;
 
-  MakeSamplerReferenceBodyFunction("ep_func", "foo_texture", "foo_sampler",
-                                   "foo_coords", ty.f32(),
-                                   ast::DecorationList{
-                                       Stage(ast::PipelineStage::kFragment),
-                                   });
+[[stage(fragment)]]
+fn main([[location(0)]] fragUV: vec2<f32>,
+        [[location(1)]] fragPosition: vec4<f32>) -> [[location(0)]] vec4<f32> {
+  return textureSample(myTexture, mySampler, fragUV) * fragPosition;
+})";
 
-  Inspector& inspector = Build();
-
-  auto result = inspector.GetSamplerTextureUses("ep_func");
+  Inspector& inspector = Initialize(shader);
+  auto result = inspector.GetSamplerTextureUses("main");
   ASSERT_FALSE(inspector.has_error()) << inspector.error();
 
   ASSERT_EQ(1u, result.size());
@@ -2388,58 +2397,233 @@
   EXPECT_EQ(0u, result[0].sampler_binding_point.group);
   EXPECT_EQ(1u, result[0].sampler_binding_point.binding);
   EXPECT_EQ(0u, result[0].texture_binding_point.group);
-  EXPECT_EQ(10u, result[0].texture_binding_point.binding);
+  EXPECT_EQ(2u, result[0].texture_binding_point.binding);
+}
+
+TEST_F(InspectorGetSamplerTextureUsesTest, UnknownEntryPoint) {
+  std::string shader = R"(
+[[group(0), binding(1)]] var mySampler: sampler;
+[[group(0), binding(2)]] var myTexture: texture_2d<f32>;
+
+[[stage(fragment)]]
+fn main([[location(0)]] fragUV: vec2<f32>,
+        [[location(1)]] fragPosition: vec4<f32>) -> [[location(0)]] vec4<f32> {
+  return textureSample(myTexture, mySampler, fragUV) * fragPosition;
+})";
+
+  Inspector& inspector = Initialize(shader);
+  auto result = inspector.GetSamplerTextureUses("foo");
+  ASSERT_TRUE(inspector.has_error()) << inspector.error();
 }
 
 TEST_F(InspectorGetSamplerTextureUsesTest, MultipleCalls) {
-  auto* sampled_texture_type =
-      ty.sampled_texture(ast::TextureDimension::k1d, ty.f32());
-  AddResource("foo_texture", sampled_texture_type, 0, 10);
-  AddSampler("foo_sampler", 0, 1);
-  AddGlobalVariable("foo_coords", ty.f32());
+  std::string shader = R"(
+[[group(0), binding(1)]] var mySampler: sampler;
+[[group(0), binding(2)]] var myTexture: texture_2d<f32>;
 
-  MakeSamplerReferenceBodyFunction("ep_func", "foo_texture", "foo_sampler",
-                                   "foo_coords", ty.f32(),
-                                   ast::DecorationList{
-                                       Stage(ast::PipelineStage::kFragment),
-                                   });
+[[stage(fragment)]]
+fn main([[location(0)]] fragUV: vec2<f32>,
+        [[location(1)]] fragPosition: vec4<f32>) -> [[location(0)]] vec4<f32> {
+  return textureSample(myTexture, mySampler, fragUV) * fragPosition;
+})";
 
-  Inspector& inspector = Build();
-
-  auto result_0 = inspector.GetSamplerTextureUses("ep_func");
+  Inspector& inspector = Initialize(shader);
+  auto result_0 = inspector.GetSamplerTextureUses("main");
   ASSERT_FALSE(inspector.has_error()) << inspector.error();
 
-  auto result_1 = inspector.GetSamplerTextureUses("ep_func");
+  auto result_1 = inspector.GetSamplerTextureUses("main");
   ASSERT_FALSE(inspector.has_error()) << inspector.error();
 
   EXPECT_EQ(result_0, result_1);
 }
 
-TEST_F(InspectorGetSamplerTextureUsesTest, InFunction) {
-  auto* sampled_texture_type =
-      ty.sampled_texture(ast::TextureDimension::k1d, ty.f32());
-  AddResource("foo_texture", sampled_texture_type, 0, 0);
-  AddSampler("foo_sampler", 0, 1);
-  AddGlobalVariable("foo_coords", ty.f32());
+TEST_F(InspectorGetSamplerTextureUsesTest, BothIndirect) {
+  std::string shader = R"(
+[[group(0), binding(1)]] var mySampler: sampler;
+[[group(0), binding(2)]] var myTexture: texture_2d<f32>;
 
-  MakeSamplerReferenceBodyFunction("foo_func", "foo_texture", "foo_sampler",
-                                   "foo_coords", ty.f32(), {});
+fn doSample(t: texture_2d<f32>, s: sampler, uv: vec2<f32>) -> vec4<f32> {
+  return textureSample(t, s, uv);
+}
 
-  MakeCallerBodyFunction("ep_func", {"foo_func"},
-                         ast::DecorationList{
-                             Stage(ast::PipelineStage::kFragment),
-                         });
+[[stage(fragment)]]
+fn main([[location(0)]] fragUV: vec2<f32>,
+        [[location(1)]] fragPosition: vec4<f32>) -> [[location(0)]] vec4<f32> {
+  return doSample(myTexture, mySampler, fragUV) * fragPosition;
+})";
 
-  Inspector& inspector = Build();
-
-  auto result = inspector.GetSamplerTextureUses("ep_func");
+  Inspector& inspector = Initialize(shader);
+  auto result = inspector.GetSamplerTextureUses("main");
   ASSERT_FALSE(inspector.has_error()) << inspector.error();
 
   ASSERT_EQ(1u, result.size());
+
   EXPECT_EQ(0u, result[0].sampler_binding_point.group);
   EXPECT_EQ(1u, result[0].sampler_binding_point.binding);
   EXPECT_EQ(0u, result[0].texture_binding_point.group);
-  EXPECT_EQ(0u, result[0].texture_binding_point.binding);
+  EXPECT_EQ(2u, result[0].texture_binding_point.binding);
+}
+
+TEST_F(InspectorGetSamplerTextureUsesTest, SamplerIndirect) {
+  std::string shader = R"(
+[[group(0), binding(1)]] var mySampler: sampler;
+[[group(0), binding(2)]] var myTexture: texture_2d<f32>;
+
+fn doSample(s: sampler, uv: vec2<f32>) -> vec4<f32> {
+  return textureSample(myTexture, s, uv);
+}
+
+[[stage(fragment)]]
+fn main([[location(0)]] fragUV: vec2<f32>,
+        [[location(1)]] fragPosition: vec4<f32>) -> [[location(0)]] vec4<f32> {
+  return doSample(mySampler, fragUV) * fragPosition;
+})";
+
+  Inspector& inspector = Initialize(shader);
+  auto result = inspector.GetSamplerTextureUses("main");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+  ASSERT_EQ(1u, result.size());
+
+  EXPECT_EQ(0u, result[0].sampler_binding_point.group);
+  EXPECT_EQ(1u, result[0].sampler_binding_point.binding);
+  EXPECT_EQ(0u, result[0].texture_binding_point.group);
+  EXPECT_EQ(2u, result[0].texture_binding_point.binding);
+}
+
+TEST_F(InspectorGetSamplerTextureUsesTest, TextureIndirect) {
+  std::string shader = R"(
+[[group(0), binding(1)]] var mySampler: sampler;
+[[group(0), binding(2)]] var myTexture: texture_2d<f32>;
+
+fn doSample(t: texture_2d<f32>, uv: vec2<f32>) -> vec4<f32> {
+  return textureSample(t, mySampler, uv);
+}
+
+[[stage(fragment)]]
+fn main([[location(0)]] fragUV: vec2<f32>,
+        [[location(1)]] fragPosition: vec4<f32>) -> [[location(0)]] vec4<f32> {
+  return doSample(myTexture, fragUV) * fragPosition;
+})";
+
+  Inspector& inspector = Initialize(shader);
+  auto result = inspector.GetSamplerTextureUses("main");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+  ASSERT_EQ(1u, result.size());
+
+  EXPECT_EQ(0u, result[0].sampler_binding_point.group);
+  EXPECT_EQ(1u, result[0].sampler_binding_point.binding);
+  EXPECT_EQ(0u, result[0].texture_binding_point.group);
+  EXPECT_EQ(2u, result[0].texture_binding_point.binding);
+}
+
+TEST_F(InspectorGetSamplerTextureUsesTest, NeitherIndirect) {
+  std::string shader = R"(
+[[group(0), binding(1)]] var mySampler: sampler;
+[[group(0), binding(2)]] var myTexture: texture_2d<f32>;
+
+fn doSample(uv: vec2<f32>) -> vec4<f32> {
+  return textureSample(myTexture, mySampler, uv);
+}
+
+[[stage(fragment)]]
+fn main([[location(0)]] fragUV: vec2<f32>,
+        [[location(1)]] fragPosition: vec4<f32>) -> [[location(0)]] vec4<f32> {
+  return doSample(fragUV) * fragPosition;
+})";
+
+  Inspector& inspector = Initialize(shader);
+  auto result = inspector.GetSamplerTextureUses("main");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+  ASSERT_EQ(1u, result.size());
+
+  EXPECT_EQ(0u, result[0].sampler_binding_point.group);
+  EXPECT_EQ(1u, result[0].sampler_binding_point.binding);
+  EXPECT_EQ(0u, result[0].texture_binding_point.group);
+  EXPECT_EQ(2u, result[0].texture_binding_point.binding);
+}
+
+TEST_F(InspectorGetSamplerTextureUsesTest, Complex) {
+  std::string shader = R"(
+[[group(0), binding(1)]] var mySampler: sampler;
+[[group(0), binding(2)]] var myTexture: texture_2d<f32>;
+
+
+fn doSample(t: texture_2d<f32>, s: sampler, uv: vec2<f32>) -> vec4<f32> {
+  return textureSample(t, s, uv);
+}
+
+fn X(t: texture_2d<f32>, s: sampler, uv: vec2<f32>) -> vec4<f32> {
+  return doSample(t, s, uv);
+}
+
+fn Y(t: texture_2d<f32>, s: sampler, uv: vec2<f32>) -> vec4<f32> {
+  return doSample(t, s, uv);
+}
+
+fn Z(t: texture_2d<f32>, s: sampler, uv: vec2<f32>) -> vec4<f32> {
+  return X(t, s, uv) + Y(t, s, uv);
+}
+
+[[stage(fragment)]]
+fn via_call([[location(0)]] fragUV: vec2<f32>,
+        [[location(1)]] fragPosition: vec4<f32>) -> [[location(0)]] vec4<f32> {
+  return Z(myTexture, mySampler, fragUV) * fragPosition;
+}
+
+[[stage(fragment)]]
+fn via_ptr([[location(0)]] fragUV: vec2<f32>,
+        [[location(1)]] fragPosition: vec4<f32>) -> [[location(0)]] vec4<f32> {
+  let t = &myTexture;
+  let s = &mySampler;
+  return textureSample(*t, *s, fragUV) + fragPosition;
+}
+
+[[stage(fragment)]]
+fn direct([[location(0)]] fragUV: vec2<f32>,
+        [[location(1)]] fragPosition: vec4<f32>) -> [[location(0)]] vec4<f32> {
+  return textureSample(myTexture, mySampler, fragUV) + fragPosition;
+})";
+
+  Inspector& inspector = Initialize(shader);
+
+  {
+    auto result = inspector.GetSamplerTextureUses("via_call");
+    ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+    ASSERT_EQ(1u, result.size());
+
+    EXPECT_EQ(0u, result[0].sampler_binding_point.group);
+    EXPECT_EQ(1u, result[0].sampler_binding_point.binding);
+    EXPECT_EQ(0u, result[0].texture_binding_point.group);
+    EXPECT_EQ(2u, result[0].texture_binding_point.binding);
+  }
+
+  {
+    auto result = inspector.GetSamplerTextureUses("via_ptr");
+    ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+    ASSERT_EQ(1u, result.size());
+
+    EXPECT_EQ(0u, result[0].sampler_binding_point.group);
+    EXPECT_EQ(1u, result[0].sampler_binding_point.binding);
+    EXPECT_EQ(0u, result[0].texture_binding_point.group);
+    EXPECT_EQ(2u, result[0].texture_binding_point.binding);
+  }
+
+  {
+    auto result = inspector.GetSamplerTextureUses("direct");
+    ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+    ASSERT_EQ(1u, result.size());
+
+    EXPECT_EQ(0u, result[0].sampler_binding_point.group);
+    EXPECT_EQ(1u, result[0].sampler_binding_point.binding);
+    EXPECT_EQ(0u, result[0].texture_binding_point.group);
+    EXPECT_EQ(2u, result[0].texture_binding_point.binding);
+  }
 }
 
 TEST_F(InspectorGetWorkgroupStorageSizeTest, Empty) {
@@ -2529,6 +2713,27 @@
   EXPECT_EQ(1024u, inspector.GetWorkgroupStorageSize("ep_func"));
 }
 
+// Crash was occuring in ::GenerateSamplerTargets, when
+// ::GetSamplerTextureUses was called.
+TEST_F(InspectorRegressionTest, tint967) {
+  std::string shader = R"(
+[[group(0), binding(1)]] var mySampler: sampler;
+[[group(0), binding(2)]] var myTexture: texture_2d<f32>;
+
+fn doSample(t: texture_2d<f32>, s: sampler, uv: vec2<f32>) -> vec4<f32> {
+  return textureSample(t, s, uv);
+}
+
+[[stage(fragment)]]
+fn main([[location(0)]] fragUV: vec2<f32>,
+        [[location(1)]] fragPosition: vec4<f32>) -> [[location(0)]] vec4<f32> {
+  return doSample(myTexture, mySampler, fragUV) * fragPosition;
+})";
+
+  Inspector& inspector = Initialize(shader);
+  auto result = inspector.GetSamplerTextureUses("main");
+}
+
 }  // namespace
 }  // namespace inspector
 }  // namespace tint
diff --git a/src/inspector/test_inspector_runner.cc b/src/inspector/test_inspector_runner.cc
new file mode 100644
index 0000000..b7c6c7a
--- /dev/null
+++ b/src/inspector/test_inspector_runner.cc
@@ -0,0 +1,39 @@
+// 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/inspector/test_inspector_runner.h"
+
+namespace tint {
+namespace inspector {
+
+InspectorRunner::InspectorRunner() = default;
+InspectorRunner::~InspectorRunner() = default;
+
+Inspector& InspectorRunner::Initialize(std::string shader) {
+  if (inspector_) {
+    return *inspector_;
+  }
+
+  file_ = std::make_unique<Source::File>("test", shader);
+  program_ = std::make_unique<Program>(reader::wgsl::Parse(file_.get()));
+  [&]() {
+    ASSERT_TRUE(program_->IsValid())
+        << diag::Formatter().format(program_->Diagnostics());
+  }();
+  inspector_ = std::make_unique<Inspector>(program_.get());
+  return *inspector_;
+}
+
+}  // namespace inspector
+}  // namespace tint
diff --git a/src/inspector/test_inspector_runner.h b/src/inspector/test_inspector_runner.h
new file mode 100644
index 0000000..d405442
--- /dev/null
+++ b/src/inspector/test_inspector_runner.h
@@ -0,0 +1,51 @@
+// 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_INSPECTOR_TEST_INSPECTOR_RUNNER_H_
+#define SRC_INSPECTOR_TEST_INSPECTOR_RUNNER_H_
+
+#include <memory>
+#include <string>
+
+#include "gtest/gtest.h"
+#include "tint/tint.h"
+
+namespace tint {
+namespace inspector {
+
+/// Utility class for running shaders in inspector tests
+class InspectorRunner {
+ public:
+  InspectorRunner();
+  virtual ~InspectorRunner();
+
+  /// Create a Program with Inspector from the provided WGSL shader.
+  /// Should only be called once per test.
+  /// @param shader a WGSL shader
+  /// @returns a reference to the Inspector for the built Program.
+  Inspector& Initialize(std::string shader);
+
+ protected:
+  /// File created from input shader and used to create Program.
+  std::unique_ptr<Source::File> file_;
+  /// Program created by this runner.
+  std::unique_ptr<Program> program_;
+  /// Inspector for |program_|
+  std::unique_ptr<Inspector> inspector_;
+};
+
+}  // namespace inspector
+}  // namespace tint
+
+#endif  // SRC_INSPECTOR_TEST_INSPECTOR_RUNNER_H_
diff --git a/test/BUILD.gn b/test/BUILD.gn
index 67d2d39..7c8738c 100644
--- a/test/BUILD.gn
+++ b/test/BUILD.gn
@@ -224,6 +224,8 @@
     "../src/inspector/inspector_test.cc",
     "../src/inspector/test_inspector_builder.cc",
     "../src/inspector/test_inspector_builder.h",
+    "../src/inspector/test_inspector_runner.cc",
+    "../src/inspector/test_inspector_runner.h",
     "../src/intrinsic_table_test.cc",
     "../src/program_builder_test.cc",
     "../src/program_test.cc",