Allow derived access chain without uses in access chain conversion
diff --git a/source/opt/local_access_chain_convert_pass.cpp b/source/opt/local_access_chain_convert_pass.cpp
index ff5f912..14bec99 100644
--- a/source/opt/local_access_chain_convert_pass.cpp
+++ b/source/opt/local_access_chain_convert_pass.cpp
@@ -139,7 +139,17 @@
 bool LocalAccessChainConvertPass::HasOnlySupportedRefs(uint32_t ptrId) {
   if (supported_ref_ptrs_.find(ptrId) != supported_ref_ptrs_.end()) return true;
   analysis::UseList* uses = get_def_use_mgr()->GetUses(ptrId);
-  assert(uses != nullptr);
+
+  if (!uses) {
+    // This is a variable (or access chain to a variable) that has no uses.
+    // We won't encounter loads or stores for this <result-id> per se, but
+    // this <result-id> may be derived from some other variable (or access
+    // chain). Return true here can unblock the access chain conversion of
+    // the root variable. This particular <result-id> won't be touched and
+    // can be handled in dead code elimination.
+    return true;
+  }
+
   for (auto u : *uses) {
     SpvOp op = u.inst->opcode();
     if (IsNonPtrAccessChain(op) || op == SpvOpCopyObject) {
diff --git a/source/opt/local_single_block_elim_pass.cpp b/source/opt/local_single_block_elim_pass.cpp
index 8fd4023..4f208af 100644
--- a/source/opt/local_single_block_elim_pass.cpp
+++ b/source/opt/local_single_block_elim_pass.cpp
@@ -30,7 +30,17 @@
 bool LocalSingleBlockLoadStoreElimPass::HasOnlySupportedRefs(uint32_t ptrId) {
   if (supported_ref_ptrs_.find(ptrId) != supported_ref_ptrs_.end()) return true;
   analysis::UseList* uses = get_def_use_mgr()->GetUses(ptrId);
-  assert(uses != nullptr);
+
+  if (!uses) {
+    // This is a variable (or access chain to a variable) that has no uses.
+    // We won't encounter loads or stores for this <result-id> per se, but
+    // this <result-id> may be derived from some other variable (or access
+    // chain). Return true here can unblock the access chain conversion of
+    // the root variable. This particular <result-id> won't be touched and
+    // can be handled in dead code elimination.
+    return true;
+  }
+
   for (auto u : *uses) {
     SpvOp op = u.inst->opcode();
     if (IsNonPtrAccessChain(op) || op == SpvOpCopyObject) {
diff --git a/source/opt/local_single_store_elim_pass.cpp b/source/opt/local_single_store_elim_pass.cpp
index e3a00ba..5eda1d2 100644
--- a/source/opt/local_single_store_elim_pass.cpp
+++ b/source/opt/local_single_store_elim_pass.cpp
@@ -32,7 +32,17 @@
 bool LocalSingleStoreElimPass::HasOnlySupportedRefs(uint32_t ptrId) {
   if (supported_ref_ptrs_.find(ptrId) != supported_ref_ptrs_.end()) return true;
   analysis::UseList* uses = get_def_use_mgr()->GetUses(ptrId);
-  assert(uses != nullptr);
+
+  if (!uses) {
+    // This is a variable (or access chain to a variable) that has no uses.
+    // We won't encounter loads or stores for this <result-id> per se, but
+    // this <result-id> may be derived from some other variable (or access
+    // chain). Return true here can unblock the access chain conversion of
+    // the root variable. This particular <result-id> won't be touched and
+    // can be handled in dead code elimination.
+    return true;
+  }
+
   for (auto u : *uses) {
     SpvOp op = u.inst->opcode();
     if (IsNonPtrAccessChain(op) || op == SpvOpCopyObject) {
diff --git a/test/opt/local_access_chain_convert_test.cpp b/test/opt/local_access_chain_convert_test.cpp
index a450e6b..23221e1 100644
--- a/test/opt/local_access_chain_convert_test.cpp
+++ b/test/opt/local_access_chain_convert_test.cpp
@@ -656,6 +656,67 @@
       assembly, assembly, false, true);
 }
 
+TEST_F(LocalAccessChainConvertTest, SomeAccessChainsHaveNoUse) {
+  // Based on HLSL source code:
+  // struct S {
+  //   float f;
+  // };
+
+  // float main(float input : A) : B {
+  //   S local = { input };
+  //   return local.f;
+  // }
+
+  const std::string predefs = R"(OpCapability Shader
+OpMemoryModel Logical GLSL450
+OpEntryPoint Vertex %main "main" %in_var_A %out_var_B
+OpName %main "main"
+OpName %in_var_A "in.var.A"
+OpName %out_var_B "out.var.B"
+OpName %S "S"
+OpName %local "local"
+%int = OpTypeInt 32 1
+%void = OpTypeVoid
+%8 = OpTypeFunction %void
+%float = OpTypeFloat 32
+%_ptr_Function_float = OpTypePointer Function %float
+%_ptr_Input_float = OpTypePointer Input %float
+%_ptr_Output_float = OpTypePointer Output %float
+%S = OpTypeStruct %float
+%_ptr_Function_S = OpTypePointer Function %S
+%int_0 = OpConstant %int 0
+%in_var_A = OpVariable %_ptr_Input_float Input
+%out_var_B = OpVariable %_ptr_Output_float Output
+%main = OpFunction %void None %8
+%15 = OpLabel
+%local = OpVariable %_ptr_Function_S Function
+%16 = OpLoad %float %in_var_A
+%17 = OpCompositeConstruct %S %16
+OpStore %local %17
+)";
+
+  const std::string before =
+      R"(%18 = OpAccessChain %_ptr_Function_float %local %int_0
+%19 = OpAccessChain %_ptr_Function_float %local %int_0
+%20 = OpLoad %float %18
+OpStore %out_var_B %20
+OpReturn
+OpFunctionEnd
+)";
+
+  const std::string after =
+      R"(%19 = OpAccessChain %_ptr_Function_float %local %int_0
+%21 = OpLoad %S %local
+%22 = OpCompositeExtract %float %21 0
+OpStore %out_var_B %22
+OpReturn
+OpFunctionEnd
+)";
+
+  SinglePassRunAndCheck<opt::LocalAccessChainConvertPass>(
+      predefs + before, predefs + after, true, true);
+}
+
 // TODO(greg-lunarg): Add tests to verify handling of these cases:
 //
 //    Assorted vector and matrix types