spirv-fuzz: Avoid void struct member when outlining functions (#3936)

Fixes #3935.
diff --git a/source/fuzz/transformation_outline_function.cpp b/source/fuzz/transformation_outline_function.cpp
index 0eec50e..26f9e0b 100644
--- a/source/fuzz/transformation_outline_function.cpp
+++ b/source/fuzz/transformation_outline_function.cpp
@@ -367,10 +367,10 @@
 
   // Fill out the body of the outlined function according to the region that is
   // being outlined.
-  PopulateOutlinedFunction(*original_region_entry_block,
-                           *original_region_exit_block, region_blocks,
-                           region_output_ids, output_id_to_fresh_id_map,
-                           ir_context, outlined_function.get());
+  PopulateOutlinedFunction(
+      *original_region_entry_block, *original_region_exit_block, region_blocks,
+      region_output_ids, output_id_to_type_id, output_id_to_fresh_id_map,
+      ir_context, outlined_function.get());
 
   // Collapse the region that has been outlined into a function down to a single
   // block that calls said function.
@@ -595,6 +595,12 @@
       for (uint32_t output_id : region_output_ids) {
         auto output_id_type =
             ir_context->get_def_use_mgr()->GetDef(output_id)->type_id();
+        if (ir_context->get_def_use_mgr()->GetDef(output_id_type)->opcode() ==
+            SpvOpTypeVoid) {
+          // We cannot add a void field to a struct.  We instead use OpUndef to
+          // handle void output ids.
+          continue;
+        }
         struct_member_types.push_back({SPV_OPERAND_TYPE_ID, {output_id_type}});
       }
       // Add a new struct type to the module.
@@ -758,6 +764,7 @@
     const opt::BasicBlock& original_region_exit_block,
     const std::set<opt::BasicBlock*>& region_blocks,
     const std::vector<uint32_t>& region_output_ids,
+    const std::map<uint32_t, uint32_t>& output_id_to_type_id,
     const std::map<uint32_t, uint32_t>& output_id_to_fresh_id_map,
     opt::IRContext* ir_context, opt::Function* outlined_function) const {
   // When we create the exit block for the outlined region, we use this pointer
@@ -857,12 +864,16 @@
         ir_context, SpvOpReturn, 0, 0, opt::Instruction::OperandList()));
   } else {
     // In the case where there are output ids, we add an OpCompositeConstruct
-    // instruction to pack all the output values into a struct, and then an
-    // OpReturnValue instruction to return this struct.
+    // instruction to pack all the non-void output values into a struct, and
+    // then an OpReturnValue instruction to return this struct.
     opt::Instruction::OperandList struct_member_operands;
     for (uint32_t id : region_output_ids) {
-      struct_member_operands.push_back(
-          {SPV_OPERAND_TYPE_ID, {output_id_to_fresh_id_map.at(id)}});
+      if (ir_context->get_def_use_mgr()
+              ->GetDef(output_id_to_type_id.at(id))
+              ->opcode() != SpvOpTypeVoid) {
+        struct_member_operands.push_back(
+            {SPV_OPERAND_TYPE_ID, {output_id_to_fresh_id_map.at(id)}});
+      }
     }
     outlined_region_exit_block->AddInstruction(MakeUnique<opt::Instruction>(
         ir_context, SpvOpCompositeConstruct,
@@ -948,15 +959,27 @@
 
   // If there are output ids, the function call will return a struct.  For each
   // output id, we add an extract operation to pull the appropriate struct
-  // member out into an output id.
-  for (uint32_t index = 0; index < region_output_ids.size(); ++index) {
-    uint32_t output_id = region_output_ids[index];
-    original_region_entry_block->AddInstruction(MakeUnique<opt::Instruction>(
-        ir_context, SpvOpCompositeExtract, output_id_to_type_id.at(output_id),
-        output_id,
-        opt::Instruction::OperandList(
-            {{SPV_OPERAND_TYPE_ID, {message_.new_caller_result_id()}},
-             {SPV_OPERAND_TYPE_LITERAL_INTEGER, {index}}})));
+  // member out into an output id.  The exception is for output ids with void
+  // type.  There are no struct entries for these, so we use an OpUndef of void
+  // type instead.
+  uint32_t struct_member_index = 0;
+  for (uint32_t output_id : region_output_ids) {
+    uint32_t output_type_id = output_id_to_type_id.at(output_id);
+    if (ir_context->get_def_use_mgr()->GetDef(output_type_id)->opcode() ==
+        SpvOpTypeVoid) {
+      original_region_entry_block->AddInstruction(MakeUnique<opt::Instruction>(
+          ir_context, SpvOpUndef, output_type_id, output_id,
+          opt::Instruction::OperandList()));
+      // struct_member_index is not incremented since there was no struct member
+      // associated with this void-typed output id.
+    } else {
+      original_region_entry_block->AddInstruction(MakeUnique<opt::Instruction>(
+          ir_context, SpvOpCompositeExtract, output_type_id, output_id,
+          opt::Instruction::OperandList(
+              {{SPV_OPERAND_TYPE_ID, {message_.new_caller_result_id()}},
+               {SPV_OPERAND_TYPE_LITERAL_INTEGER, {struct_member_index}}})));
+      struct_member_index++;
+    }
   }
 
   // Finally, we terminate the block with the merge instruction (if any) that
diff --git a/source/fuzz/transformation_outline_function.h b/source/fuzz/transformation_outline_function.h
index be56695..36c0daf 100644
--- a/source/fuzz/transformation_outline_function.h
+++ b/source/fuzz/transformation_outline_function.h
@@ -178,7 +178,8 @@
   // of |original_region_exit_block| so that it returns something appropriate,
   // and patching up branches to |original_region_entry_block| to refer to its
   // clone.  Parameters |region_output_ids| and |output_id_to_fresh_id_map| are
-  // used to determine what the function should return.
+  // used to determine what the function should return.  Parameter
+  // |output_id_to_type_id| provides the type of each output id.
   //
   // The |transformation_context| argument allow facts about blocks being
   // outlined, e.g. whether they are dead blocks, to be asserted about blocks
@@ -188,6 +189,7 @@
       const opt::BasicBlock& original_region_exit_block,
       const std::set<opt::BasicBlock*>& region_blocks,
       const std::vector<uint32_t>& region_output_ids,
+      const std::map<uint32_t, uint32_t>& output_id_to_type_id,
       const std::map<uint32_t, uint32_t>& output_id_to_fresh_id_map,
       opt::IRContext* ir_context, opt::Function* outlined_function) const;
 
diff --git a/test/fuzz/transformation_outline_function_test.cpp b/test/fuzz/transformation_outline_function_test.cpp
index c567680..6c0bff4 100644
--- a/test/fuzz/transformation_outline_function_test.cpp
+++ b/test/fuzz/transformation_outline_function_test.cpp
@@ -2531,6 +2531,98 @@
   ApplyAndCheckFreshIds(transformation, context.get(), &transformation_context);
 }
 
+TEST(TransformationOutlineFunctionTest, SkipVoidOutputId) {
+  std::string shader = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %6 "main"
+               OpExecutionMode %6 OriginUpperLeft
+               OpSource ESSL 310
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+         %21 = OpTypeBool
+         %81 = OpConstantTrue %21
+          %6 = OpFunction %2 None %3
+          %7 = OpLabel
+               OpBranch %80
+         %80 = OpLabel
+         %84 = OpFunctionCall %2 %87
+               OpBranch %90
+         %90 = OpLabel
+         %86 = OpPhi %2 %84 %80
+               OpReturn
+               OpFunctionEnd
+         %87 = OpFunction %2 None %3
+         %88 = OpLabel
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  const auto env = SPV_ENV_UNIVERSAL_1_5;
+  const auto consumer = nullptr;
+  const auto context = BuildModule(env, consumer, shader, kFuzzAssembleOption);
+  spvtools::ValidatorOptions validator_options;
+  ASSERT_TRUE(fuzzerutil::IsValidAndWellFormed(context.get(), validator_options,
+                                               kConsoleMessageConsumer));
+  TransformationContext transformation_context(
+      MakeUnique<FactManager>(context.get()), validator_options);
+  TransformationOutlineFunction transformation(
+      /*entry_block*/ 80,
+      /*exit_block*/ 80,
+      /*new_function_struct_return_type_id*/ 300,
+      /*new_function_type_id*/ 301,
+      /*new_function_id*/ 302,
+      /*new_function_region_entry_block*/ 304,
+      /*new_caller_result_id*/ 305,
+      /*new_callee_result_id*/ 306,
+      /*input_id_to_fresh_id*/ {},
+      /*output_id_to_fresh_id*/ {{84, 307}});
+
+  ASSERT_TRUE(
+      transformation.IsApplicable(context.get(), transformation_context));
+  ApplyAndCheckFreshIds(transformation, context.get(), &transformation_context);
+  ASSERT_TRUE(fuzzerutil::IsValidAndWellFormed(context.get(), validator_options,
+                                               kConsoleMessageConsumer));
+
+  std::string after_transformation = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %6 "main"
+               OpExecutionMode %6 OriginUpperLeft
+               OpSource ESSL 310
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+         %21 = OpTypeBool
+         %81 = OpConstantTrue %21
+        %300 = OpTypeStruct
+        %301 = OpTypeFunction %300
+          %6 = OpFunction %2 None %3
+          %7 = OpLabel
+               OpBranch %80
+         %80 = OpLabel
+        %305 = OpFunctionCall %300 %302
+         %84 = OpUndef %2
+               OpBranch %90
+         %90 = OpLabel
+         %86 = OpPhi %2 %84 %80
+               OpReturn
+               OpFunctionEnd
+         %87 = OpFunction %2 None %3
+         %88 = OpLabel
+               OpReturn
+               OpFunctionEnd
+        %302 = OpFunction %300 None %301
+        %304 = OpLabel
+        %307 = OpFunctionCall %2 %87
+        %306 = OpCompositeConstruct %300
+               OpReturnValue %306
+               OpFunctionEnd
+  )";
+  ASSERT_TRUE(IsEqual(env, after_transformation, context.get()));
+}
+
 TEST(TransformationOutlineFunctionTest, Miscellaneous1) {
   // This tests outlining of some non-trivial code, and also tests the way
   // overflow ids are used by the transformation.