spirv-reader: flatten output matrix, array, struct

Bug: tint:912
Change-Id: Iebbcb7ea8d870cccadad7dd1ce8aaccf8965b370
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/56301
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: James Price <jrprice@google.com>
Auto-Submit: David Neto <dneto@google.com>
diff --git a/src/reader/spirv/function.cc b/src/reader/spirv/function.cc
index 55c0a27..85ab480 100644
--- a/src/reader/spirv/function.cc
+++ b/src/reader/spirv/function.cc
@@ -935,14 +935,15 @@
   return body;
 }
 
-bool FunctionEmitter::EmitInputParameter(std::string var_name,
-                                         const Type* var_type,
-                                         ast::DecorationList* decos,
-                                         std::vector<int> index_prefix,
-                                         const Type* tip_type,
-                                         const Type* forced_param_type,
-                                         ast::VariableList* params,
-                                         ast::StatementList* statements) {
+bool FunctionEmitter::EmitPipelineInput(std::string var_name,
+                                        const Type* var_type,
+                                        ast::DecorationList* decos,
+                                        std::vector<int> index_prefix,
+                                        const Type* tip_type,
+                                        const Type* forced_param_type,
+                                        ast::VariableList* params,
+                                        ast::StatementList* statements) {
+  // TODO(dneto): Handle structs where the locations are annotated on members.
   tip_type = tip_type->UnwrapAlias();
   if (auto* ref_type = tip_type->As<Reference>()) {
     tip_type = ref_type->type;
@@ -954,8 +955,8 @@
     const Type* vec_ty = ty_.Vector(matrix_type->type, matrix_type->rows);
     for (int col = 0; col < num_columns; col++) {
       index_prefix.back() = col;
-      if (!EmitInputParameter(var_name, var_type, decos, index_prefix, vec_ty,
-                              forced_param_type, params, statements)) {
+      if (!EmitPipelineInput(var_name, var_type, decos, index_prefix, vec_ty,
+                             forced_param_type, params, statements)) {
         return false;
       }
     }
@@ -968,8 +969,8 @@
     const Type* elem_ty = array_type->type;
     for (int i = 0; i < static_cast<int>(array_type->size); i++) {
       index_prefix.back() = i;
-      if (!EmitInputParameter(var_name, var_type, decos, index_prefix, elem_ty,
-                              forced_param_type, params, statements)) {
+      if (!EmitPipelineInput(var_name, var_type, decos, index_prefix, elem_ty,
+                             forced_param_type, params, statements)) {
         return false;
       }
     }
@@ -979,9 +980,9 @@
     index_prefix.push_back(0);
     for (int i = 0; i < static_cast<int>(members.size()); ++i) {
       index_prefix.back() = i;
-      if (!EmitInputParameter(var_name, var_type, decos, index_prefix,
-                              members[i], forced_param_type, params,
-                              statements)) {
+      if (!EmitPipelineInput(var_name, var_type, decos, index_prefix,
+                             members[i], forced_param_type, params,
+                             statements)) {
         return false;
       }
     }
@@ -1047,6 +1048,120 @@
   return success();
 }
 
+bool FunctionEmitter::EmitPipelineOutput(std::string var_name,
+                                         const Type* var_type,
+                                         ast::DecorationList* decos,
+                                         std::vector<int> index_prefix,
+                                         const Type* tip_type,
+                                         const Type* forced_member_type,
+                                         ast::StructMemberList* return_members,
+                                         ast::ExpressionList* return_exprs) {
+  // TODO(dneto): Handle structs where the locations are annotated on members.
+  tip_type = tip_type->UnwrapAlias();
+  if (auto* ref_type = tip_type->As<Reference>()) {
+    tip_type = ref_type->type;
+  }
+
+  if (auto* matrix_type = tip_type->As<Matrix>()) {
+    index_prefix.push_back(0);
+    const auto num_columns = static_cast<int>(matrix_type->columns);
+    const Type* vec_ty = ty_.Vector(matrix_type->type, matrix_type->rows);
+    for (int col = 0; col < num_columns; col++) {
+      index_prefix.back() = col;
+      if (!EmitPipelineOutput(var_name, var_type, decos, index_prefix, vec_ty,
+                              forced_member_type, return_members,
+                              return_exprs)) {
+        return false;
+      }
+    }
+    return success();
+  } else if (auto* array_type = tip_type->As<Array>()) {
+    if (array_type->size == 0) {
+      return Fail() << "runtime-size array not allowed on pipeline IO";
+    }
+    index_prefix.push_back(0);
+    const Type* elem_ty = array_type->type;
+    for (int i = 0; i < static_cast<int>(array_type->size); i++) {
+      index_prefix.back() = i;
+      if (!EmitPipelineOutput(var_name, var_type, decos, index_prefix, elem_ty,
+                              forced_member_type, return_members,
+                              return_exprs)) {
+        return false;
+      }
+    }
+    return success();
+  } else if (auto* struct_type = tip_type->As<Struct>()) {
+    const auto& members = struct_type->members;
+    index_prefix.push_back(0);
+    for (int i = 0; i < static_cast<int>(members.size()); ++i) {
+      index_prefix.back() = i;
+      if (!EmitPipelineOutput(var_name, var_type, decos, index_prefix,
+                              members[i], forced_member_type, return_members,
+                              return_exprs)) {
+        return false;
+      }
+    }
+    return success();
+  }
+
+  const bool is_builtin = ast::HasDecoration<ast::BuiltinDecoration>(*decos);
+
+  const Type* member_type = is_builtin ? forced_member_type : tip_type;
+  // Derive the member name directly from the variable name.  They can't
+  // collide.
+  const auto member_name = namer_.MakeDerivedName(var_name);
+  // Create the member.
+  // TODO(dneto): Note: If the parameter has non-location decorations,
+  // then those decoration AST nodes  will be reused between multiple elements
+  // of a matrix, array, or structure.  Normally that's disallowed but currently
+  // the SPIR-V reader will make duplicates when the entire AST is cloned
+  // at the top level of the SPIR-V reader flow.  Consider rewriting this
+  // to avoid this node-sharing.
+  return_members->push_back(
+      builder_.Member(member_name, member_type->Build(builder_), *decos));
+
+  // Create an expression to evaluate the part of the variable indexed by
+  // the index_prefix.
+  ast::Expression* load_source = builder_.Expr(var_name);
+
+  // Index into the variable as needed to pick out the flattened member.
+  auto* current_type = var_type->UnwrapAlias()->UnwrapRef()->UnwrapAlias();
+  for (auto index : index_prefix) {
+    if (auto* matrix_type = current_type->As<Matrix>()) {
+      load_source = builder_.IndexAccessor(load_source, builder_.Expr(index));
+      current_type = ty_.Vector(matrix_type->type, matrix_type->rows);
+    } else if (auto* array_type = current_type->As<Array>()) {
+      load_source = builder_.IndexAccessor(load_source, builder_.Expr(index));
+      current_type = array_type->type->UnwrapAlias();
+    } else if (auto* struct_type = current_type->As<Struct>()) {
+      load_source = builder_.MemberAccessor(
+          load_source,
+          builder_.Expr(parser_impl_.GetMemberName(*struct_type, index)));
+      current_type = struct_type->members[index];
+    }
+  }
+
+  if (is_builtin && (tip_type != forced_member_type)) {
+    // The member will have the WGSL type, but we need bitcast to
+    // the variable store type.
+    load_source = create<ast::BitcastExpression>(
+        forced_member_type->Build(builder_), load_source);
+  }
+  return_exprs->push_back(load_source);
+
+  // Increment the location attribute, in case more parameters will follow.
+  for (auto*& deco : *decos) {
+    if (auto* loc_deco = deco->As<ast::LocationDecoration>()) {
+      // Replace this location decoration with a new one with one higher index.
+      // The old one doesn't leak because it's kept in the builder's AST node
+      // list.
+      deco = builder_.Location(loc_deco->source(), loc_deco->value() + 1);
+    }
+  }
+
+  return success();
+}
+
 bool FunctionEmitter::EmitEntryPointAsWrapper() {
   Source source;
 
@@ -1082,7 +1197,6 @@
     // variables must not have them.
 
     const auto var_name = namer_.GetName(var_id);
-    const auto var_sym = builder_.Symbols().Register(var_name);
 
     bool ok = true;
     if (HasBuiltinSampleMask(param_decos)) {
@@ -1091,13 +1205,12 @@
       auto* sample_mask_array_type =
           store_type->UnwrapRef()->UnwrapAlias()->As<Array>();
       TINT_ASSERT(Reader, sample_mask_array_type);
-      ok = EmitInputParameter(var_name, store_type, &param_decos, {0},
-                              sample_mask_array_type->type, forced_param_type,
-                              &(decl.params), &stmts);
+      ok = EmitPipelineInput(var_name, store_type, &param_decos, {0},
+                             sample_mask_array_type->type, forced_param_type,
+                             &(decl.params), &stmts);
     } else {
       // The normal path.
-      ok =
-          EmitInputParameter(var_name, store_type, &param_decos, {}, store_type,
+      ok = EmitPipelineInput(var_name, store_type, &param_decos, {}, store_type,
                              forced_param_type, &(decl.params), &stmts);
     }
     if (!ok) {
@@ -1127,30 +1240,34 @@
     const auto return_struct_sym =
         builder_.Symbols().Register(return_struct_name);
 
+    // Define the structure.
+    std::vector<ast::StructMember*> return_members;
+    ast::ExpressionList return_exprs;
+
     const auto& builtin_position_info = parser_impl_.GetBuiltInPositionInfo();
 
-    // Define the structure.
-    ast::ExpressionList return_exprs;
-    std::vector<ast::StructMember*> return_members;
     for (uint32_t var_id : ep_info_->outputs) {
-      const Type* param_type = nullptr;
-      const Type* store_type = nullptr;
-      ast::DecorationList out_decos;
       if (var_id == builtin_position_info.per_vertex_var_id) {
         // The SPIR-V gl_PerVertex variable has already been remapped to
-        // a gl_Position variable.
-        // Substitute the type.
-        store_type = param_type = ty_.Vector(ty_.F32(), 4);
-        out_decos.emplace_back(
-            create<ast::BuiltinDecoration>(source, ast::Builtin::kPosition));
+        // a gl_Position variable.  Substitute the type.
+        const Type* param_type = ty_.Vector(ty_.F32(), 4);
+        ast::DecorationList out_decos{
+            create<ast::BuiltinDecoration>(source, ast::Builtin::kPosition)};
+
+        const auto var_name = namer_.GetName(var_id);
+        return_members.push_back(
+            builder_.Member(var_name, param_type->Build(builder_), out_decos));
+        return_exprs.push_back(builder_.Expr(var_name));
+
       } else {
         const auto* var = def_use_mgr_->GetDef(var_id);
         TINT_ASSERT(Reader, var != nullptr);
         TINT_ASSERT(Reader, var->opcode() == SpvOpVariable);
-        store_type = GetVariableStoreType(*var);
-        param_type = store_type;
-        if (!parser_impl_.ConvertDecorationsForVariable(var_id, &param_type,
-                                                        &out_decos, true)) {
+        const Type* store_type = GetVariableStoreType(*var);
+        const Type* forced_member_type = store_type;
+        ast::DecorationList out_decos;
+        if (!parser_impl_.ConvertDecorationsForVariable(
+                var_id, &forced_member_type, &out_decos, true)) {
           // This occurs, and is not an error, for the PointSize builtin.
           if (!success()) {
             // But exit early if an error was logged.
@@ -1158,45 +1275,29 @@
           }
           continue;
         }
-      }
 
-      // TODO(dneto): flatten structs and arrays to vectors or scalars.
-      // The Per-vertex structure is already flattened.
-
-      // The member name is the same as the variable name, which is already
-      // unique across all module-scope declarations.
-      const auto var_name = namer_.GetName(var_id);
-      const auto var_sym = builder_.Symbols().Register(var_name);
-
-      // Form the member type.
-      // Reuse the var name for the member name. They can't clash.
-      ast::StructMember* return_member = create<ast::StructMember>(
-          Source{}, var_sym, param_type->Build(builder_), out_decos);
-      return_members.push_back(return_member);
-
-      ast::Expression* return_member_value =
-          create<ast::IdentifierExpression>(source, var_sym);
-      if (HasBuiltinSampleMask(out_decos)) {
-        // In Vulkan SPIR-V, the sample mask is an array. In WGSL it's a scalar.
-        // Get the first element only.
-        return_member_value = create<ast::ArrayAccessorExpression>(
-            source, return_member_value, parser_impl_.MakeNullValue(ty_.I32()));
-        if (const auto* arr_ty = store_type->UnwrapAlias()->As<Array>()) {
-          if (arr_ty->type->IsSignedScalarOrVector()) {
-            // sample_mask is unsigned in WGSL. Bitcast it.
-            return_member_value = create<ast::BitcastExpression>(
-                source, param_type->Build(builder_), return_member_value);
-          }
+        const auto var_name = namer_.GetName(var_id);
+        bool ok = true;
+        if (HasBuiltinSampleMask(out_decos)) {
+          // In Vulkan SPIR-V, the sample mask is an array. In WGSL it's a
+          // scalar. Use the first element only.
+          auto* sample_mask_array_type =
+              store_type->UnwrapRef()->UnwrapAlias()->As<Array>();
+          TINT_ASSERT(Reader, sample_mask_array_type);
+          ok = EmitPipelineOutput(var_name, store_type, &out_decos, {0},
+                                  sample_mask_array_type->type,
+                                  forced_member_type, &return_members,
+                                  &return_exprs);
         } else {
-          // Vulkan SPIR-V requires this. Validation should have failed already.
-          return Fail()
-                 << "expected SampleMask to be an array of integer scalars";
+          // The normal path.
+          ok = EmitPipelineOutput(var_name, store_type, &out_decos, {},
+                                  store_type, forced_member_type,
+                                  &return_members, &return_exprs);
         }
-      } else {
-        // No other builtin outputs need signedness conversion.
+        if (!ok) {
+          return false;
+        }
       }
-      // Save the expression.
-      return_exprs.push_back(return_member_value);
     }
 
     if (return_members.empty()) {
diff --git a/src/reader/spirv/function.h b/src/reader/spirv/function.h
index b4319ed..3a287a4 100644
--- a/src/reader/spirv/function.h
+++ b/src/reader/spirv/function.h
@@ -428,14 +428,43 @@
   /// @param params The parameter list where the new parameter is appended.
   /// @param statements The statement list where the assignment is appended.
   /// @returns false if emission failed
-  bool EmitInputParameter(std::string var_name,
+  bool EmitPipelineInput(std::string var_name,
+                         const Type* var_type,
+                         ast::DecorationList* decos,
+                         std::vector<int> index_prefix,
+                         const Type* tip_type,
+                         const Type* forced_param_type,
+                         ast::VariableList* params,
+                         ast::StatementList* statements);
+
+  /// Creates one or more struct members from an output variable, and the
+  /// expressions that compute the value they contribute to the entry point
+  /// return value.  The part of the output variable is specfied
+  /// by the `index_prefix`, which successively indexes into the variable.
+  /// Assumes the variable has already been created in the Private storage
+  /// class.
+  /// @param var_name The name of the variable
+  /// @param var_type The store type of the variable
+  /// @param decos The variable's decorations
+  /// @param index_prefix Indices stepping into the variable, indicating
+  /// what part of the variable to populate.
+  /// @param tip_type The type of the component inside variable, after indexing
+  /// with the indices in `index_prefix`.
+  /// @param forced_member_type The type forced by WGSL, if the variable is a
+  /// builtin, otherwise the same as var_type.
+  /// @param return_members The struct member list where the new member is
+  /// added.
+  /// @param return_exprs The expression list where the return expression is
+  /// added.
+  /// @returns false if emission failed
+  bool EmitPipelineOutput(std::string var_name,
                           const Type* var_type,
                           ast::DecorationList* decos,
                           std::vector<int> index_prefix,
                           const Type* tip_type,
-                          const Type* forced_param_type,
-                          ast::VariableList* params,
-                          ast::StatementList* statements);
+                          const Type* forced_member_type,
+                          ast::StructMemberList* return_members,
+                          ast::ExpressionList* return_exprs);
 
   /// Create an ast::BlockStatement representing the body of the function.
   /// This creates the statement stack, which is non-empty for the lifetime
diff --git a/src/reader/spirv/parser_impl_module_var_test.cc b/src/reader/spirv/parser_impl_module_var_test.cc
index 4243eb5..78e1ea9 100644
--- a/src/reader/spirv/parser_impl_module_var_test.cc
+++ b/src/reader/spirv/parser_impl_module_var_test.cc
@@ -3373,7 +3373,7 @@
   const std::string expected = R"(Module{
   Struct main_out {
     StructMember{[[ BuiltinDecoration{sample_mask}
- ]] x_1: __u32}
+ ]] x_1_1: __u32}
   }
   Variable{
     x_1
@@ -3438,7 +3438,7 @@
   const std::string expected = R"(Module{
   Struct main_out {
     StructMember{[[ BuiltinDecoration{sample_mask}
- ]] x_1: __u32}
+ ]] x_1_1: __u32}
   }
   Variable{
     x_1
@@ -3503,7 +3503,7 @@
   const std::string expected = R"(Module{
   Struct main_out {
     StructMember{[[ BuiltinDecoration{sample_mask}
- ]] x_1: __u32}
+ ]] x_1_1: __u32}
   }
   Variable{
     x_1
@@ -3567,7 +3567,7 @@
   const std::string expected = R"(Module{
   Struct main_out {
     StructMember{[[ BuiltinDecoration{sample_mask}
- ]] x_1: __u32}
+ ]] x_1_1: __u32}
   }
   Variable{
     x_1
@@ -3634,7 +3634,7 @@
   const std::string expected = R"(Module{
   Struct main_out {
     StructMember{[[ BuiltinDecoration{sample_mask}
- ]] x_1: __u32}
+ ]] x_1_1: __u32}
   }
   Variable{
     x_1
@@ -3701,7 +3701,7 @@
   const std::string expected = R"(Module{
   Struct main_out {
     StructMember{[[ BuiltinDecoration{sample_mask}
- ]] x_1: __u32}
+ ]] x_1_1: __u32}
   }
   Variable{
     x_1
@@ -3844,7 +3844,7 @@
   Arr_3 -> __array__i32_2_stride_4
   Struct main_out {
     StructMember{[[ BuiltinDecoration{sample_mask}
- ]] x_1: __u32}
+ ]] x_1_1: __u32}
   }
   Variable{
     x_1
@@ -5549,9 +5549,9 @@
       R"(
   Struct main_out {
     StructMember{[[ LocationDecoration{0}
- ]] x_2: __u32}
+ ]] x_2_1: __u32}
     StructMember{[[ LocationDecoration{40}
- ]] x_4: __u32}
+ ]] x_4_1: __u32}
   }
   Variable{
     x_1
@@ -5953,7 +5953,7 @@
   const std::string expected = R"(Module{
   Struct main_out {
     StructMember{[[ BuiltinDecoration{sample_mask}
- ]] x_1: __u32}
+ ]] x_1_1: __u32}
   }
   Variable{
     x_1
@@ -6026,7 +6026,7 @@
   const std::string expected = R"(Module{
   Struct main_out {
     StructMember{[[ BuiltinDecoration{sample_mask}
- ]] x_1: __u32}
+ ]] x_1_1: __u32}
   }
   Variable{
     x_1
@@ -6100,7 +6100,7 @@
   const std::string expected = R"(Module{
   Struct main_out {
     StructMember{[[ BuiltinDecoration{frag_depth}
- ]] x_1: __f32}
+ ]] x_1_1: __f32}
   }
   Variable{
     x_1
@@ -6326,7 +6326,7 @@
   const std::string expected = R"(Module{
   Struct main_out {
     StructMember{[[ BuiltinDecoration{position}
- ]] x_2: __vec_4__f32}
+ ]] x_2_1: __vec_4__f32}
   }
   Variable{
     x_1
@@ -6453,7 +6453,7 @@
   const std::string expected = R"(Module{
   Struct main_out {
     StructMember{[[ BuiltinDecoration{position}
- ]] x_2: __vec_4__f32}
+ ]] x_2_1: __vec_4__f32}
   }
   Variable{
     x_1
@@ -6573,7 +6573,7 @@
   }
   Struct main_out {
     StructMember{[[ BuiltinDecoration{position}
- ]] x_2: __vec_4__f32}
+ ]] x_2_1: __vec_4__f32}
   }
   Variable{
     x_1
@@ -6686,7 +6686,7 @@
   const std::string expected = R"(Module{
   Struct main_out {
     StructMember{[[ BuiltinDecoration{position}
- ]] x_2: __vec_4__f32}
+ ]] x_2_1: __vec_4__f32}
   }
   Variable{
     x_1
@@ -6805,7 +6805,292 @@
   EXPECT_EQ(got, expected) << got;
 }
 
-// TODO(dneto):  flatting structures
+TEST_F(SpvModuleScopeVarParserTest, Output_FlattenArray_OneLevel) {
+  const std::string assembly = R"(
+    OpCapability Shader
+    OpMemoryModel Logical Simple
+    OpEntryPoint Vertex %main "main" %1 %2
+    OpDecorate %1 Location 4
+    OpDecorate %2 BuiltIn Position
+
+    %void = OpTypeVoid
+    %voidfn = OpTypeFunction %void
+    %float = OpTypeFloat 32
+    %v4float = OpTypeVector %float 4
+    %uint = OpTypeInt 32 0
+    %uint_0 = OpConstant %uint 0
+    %uint_1 = OpConstant %uint 1
+    %uint_3 = OpConstant %uint 3
+    %arr = OpTypeArray %float %uint_3
+    %11 = OpTypePointer Output %arr
+
+    %1 = OpVariable %11 Output
+
+    %12 = OpTypePointer Output %v4float
+    %2 = OpVariable %12 Output
+
+    %main = OpFunction %void None %voidfn
+    %entry = OpLabel
+    OpReturn
+    OpFunctionEnd
+)";
+  auto p = parser(test::Assemble(assembly));
+
+  ASSERT_TRUE(p->Parse()) << p->error() << assembly;
+  EXPECT_TRUE(p->error().empty());
+
+  const auto got = p->program().to_str();
+  const std::string expected = R"(Module{
+  Struct main_out {
+    StructMember{[[ LocationDecoration{4}
+ ]] x_1_1: __f32}
+    StructMember{[[ LocationDecoration{5}
+ ]] x_1_2: __f32}
+    StructMember{[[ LocationDecoration{6}
+ ]] x_1_3: __f32}
+    StructMember{[[ BuiltinDecoration{position}
+ ]] x_2_1: __vec_4__f32}
+  }
+  Variable{
+    x_1
+    private
+    undefined
+    __array__f32_3
+  }
+  Variable{
+    x_2
+    private
+    undefined
+    __vec_4__f32
+  }
+  Function main_1 -> __void
+  ()
+  {
+    Return{}
+  }
+  Function main -> __type_name_main_out
+  StageDecoration{vertex}
+  ()
+  {
+    Call[not set]{
+      Identifier[not set]{main_1}
+      (
+      )
+    }
+    Return{
+      {
+        TypeConstructor[not set]{
+          __type_name_main_out
+          ArrayAccessor[not set]{
+            Identifier[not set]{x_1}
+            ScalarConstructor[not set]{0}
+          }
+          ArrayAccessor[not set]{
+            Identifier[not set]{x_1}
+            ScalarConstructor[not set]{1}
+          }
+          ArrayAccessor[not set]{
+            Identifier[not set]{x_1}
+            ScalarConstructor[not set]{2}
+          }
+          Identifier[not set]{x_2}
+        }
+      }
+    }
+  }
+}
+)";
+  EXPECT_EQ(got, expected) << got;
+}
+
+TEST_F(SpvModuleScopeVarParserTest, Output_FlattenMatrix) {
+  const std::string assembly = R"(
+    OpCapability Shader
+    OpMemoryModel Logical Simple
+    OpEntryPoint Vertex %main "main" %1 %2
+    OpDecorate %1 Location 9
+    OpDecorate %2 BuiltIn Position
+
+    %void = OpTypeVoid
+    %voidfn = OpTypeFunction %void
+    %float = OpTypeFloat 32
+    %v4float = OpTypeVector %float 4
+    %m2v4float = OpTypeMatrix %v4float 2
+    %uint = OpTypeInt 32 0
+
+    %11 = OpTypePointer Output %m2v4float
+
+    %1 = OpVariable %11 Output
+
+    %12 = OpTypePointer Output %v4float
+    %2 = OpVariable %12 Output
+
+    %main = OpFunction %void None %voidfn
+    %entry = OpLabel
+    OpReturn
+    OpFunctionEnd
+)";
+  auto p = parser(test::Assemble(assembly));
+
+  ASSERT_TRUE(p->Parse()) << p->error() << assembly;
+  EXPECT_TRUE(p->error().empty());
+
+  const auto got = p->program().to_str();
+  const std::string expected = R"(Module{
+  Struct main_out {
+    StructMember{[[ LocationDecoration{9}
+ ]] x_1_1: __vec_4__f32}
+    StructMember{[[ LocationDecoration{10}
+ ]] x_1_2: __vec_4__f32}
+    StructMember{[[ BuiltinDecoration{position}
+ ]] x_2_1: __vec_4__f32}
+  }
+  Variable{
+    x_1
+    private
+    undefined
+    __mat_4_2__f32
+  }
+  Variable{
+    x_2
+    private
+    undefined
+    __vec_4__f32
+  }
+  Function main_1 -> __void
+  ()
+  {
+    Return{}
+  }
+  Function main -> __type_name_main_out
+  StageDecoration{vertex}
+  ()
+  {
+    Call[not set]{
+      Identifier[not set]{main_1}
+      (
+      )
+    }
+    Return{
+      {
+        TypeConstructor[not set]{
+          __type_name_main_out
+          ArrayAccessor[not set]{
+            Identifier[not set]{x_1}
+            ScalarConstructor[not set]{0}
+          }
+          ArrayAccessor[not set]{
+            Identifier[not set]{x_1}
+            ScalarConstructor[not set]{1}
+          }
+          Identifier[not set]{x_2}
+        }
+      }
+    }
+  }
+}
+)";
+  EXPECT_EQ(got, expected) << got;
+}
+
+TEST_F(SpvModuleScopeVarParserTest, Output_FlattenStruct) {
+  const std::string assembly = R"(
+    OpCapability Shader
+    OpMemoryModel Logical Simple
+    OpEntryPoint Vertex %main "main" %1 %2
+
+    OpName %strct "Communicators"
+    OpMemberName %strct 0 "alice"
+    OpMemberName %strct 1 "bob"
+
+    OpDecorate %1 Location 9
+    OpDecorate %2 BuiltIn Position
+
+
+    %void = OpTypeVoid
+    %voidfn = OpTypeFunction %void
+    %float = OpTypeFloat 32
+    %v4float = OpTypeVector %float 4
+    %strct = OpTypeStruct %float %v4float
+
+    %11 = OpTypePointer Output %strct
+
+    %1 = OpVariable %11 Output
+
+    %12 = OpTypePointer Output %v4float
+    %2 = OpVariable %12 Output
+
+    %main = OpFunction %void None %voidfn
+    %entry = OpLabel
+    OpReturn
+    OpFunctionEnd
+)";
+  auto p = parser(test::Assemble(assembly));
+
+  ASSERT_TRUE(p->Parse()) << p->error() << assembly;
+  EXPECT_TRUE(p->error().empty());
+
+  const auto got = p->program().to_str();
+  const std::string expected = R"(Module{
+  Struct Communicators {
+    StructMember{alice: __f32}
+    StructMember{bob: __vec_4__f32}
+  }
+  Struct main_out {
+    StructMember{[[ LocationDecoration{9}
+ ]] x_1_1: __f32}
+    StructMember{[[ LocationDecoration{10}
+ ]] x_1_2: __vec_4__f32}
+    StructMember{[[ BuiltinDecoration{position}
+ ]] x_2_1: __vec_4__f32}
+  }
+  Variable{
+    x_1
+    private
+    undefined
+    __type_name_Communicators
+  }
+  Variable{
+    x_2
+    private
+    undefined
+    __vec_4__f32
+  }
+  Function main_1 -> __void
+  ()
+  {
+    Return{}
+  }
+  Function main -> __type_name_main_out
+  StageDecoration{vertex}
+  ()
+  {
+    Call[not set]{
+      Identifier[not set]{main_1}
+      (
+      )
+    }
+    Return{
+      {
+        TypeConstructor[not set]{
+          __type_name_main_out
+          MemberAccessor[not set]{
+            Identifier[not set]{x_1}
+            Identifier[not set]{alice}
+          }
+          MemberAccessor[not set]{
+            Identifier[not set]{x_1}
+            Identifier[not set]{bob}
+          }
+          Identifier[not set]{x_2}
+        }
+      }
+    }
+  }
+}
+)";
+  EXPECT_EQ(got, expected) << got;
+}
 
 }  // namespace
 }  // namespace spirv
diff --git a/test/bug/tint/749.spvasm.expected.hlsl b/test/bug/tint/749.spvasm.expected.hlsl
index f7e8761..0de0f40 100644
--- a/test/bug/tint/749.spvasm.expected.hlsl
+++ b/test/bug/tint/749.spvasm.expected.hlsl
@@ -1545,13 +1545,13 @@
 }
 
 struct main_out {
-  float4 x_GLF_color;
+  float4 x_GLF_color_1;
 };
 struct tint_symbol_1 {
   float4 gl_FragCoord_param : SV_Position;
 };
 struct tint_symbol_2 {
-  float4 x_GLF_color : SV_Target0;
+  float4 x_GLF_color_1 : SV_Target0;
 };
 
 tint_symbol_2 main(tint_symbol_1 tint_symbol) {
@@ -1559,6 +1559,6 @@
   gl_FragCoord = gl_FragCoord_param;
   main_1();
   const main_out tint_symbol_3 = {x_GLF_color};
-  const tint_symbol_2 tint_symbol_83 = {tint_symbol_3.x_GLF_color};
+  const tint_symbol_2 tint_symbol_83 = {tint_symbol_3.x_GLF_color_1};
   return tint_symbol_83;
 }
diff --git a/test/bug/tint/749.spvasm.expected.msl b/test/bug/tint/749.spvasm.expected.msl
index 9e417f0..b11eedc 100644
--- a/test/bug/tint/749.spvasm.expected.msl
+++ b/test/bug/tint/749.spvasm.expected.msl
@@ -11,10 +11,10 @@
   /* 0x0000 */ packed_float2 resolution;
 };
 struct main_out {
-  float4 x_GLF_color;
+  float4 x_GLF_color_1;
 };
 struct tint_symbol_2 {
-  float4 x_GLF_color [[color(0)]];
+  float4 x_GLF_color_1 [[color(0)]];
 };
 
 void swap_i1_i1_(thread int* const i, thread int* const j, thread QuicksortObject* const tint_symbol_83) {
@@ -1553,8 +1553,8 @@
   thread float4 tint_symbol_91 = 0.0f;
   tint_symbol_89 = gl_FragCoord_param;
   main_1(x_188, &(tint_symbol_90), &(tint_symbol_89), &(tint_symbol_91));
-  main_out const tint_symbol_3 = {.x_GLF_color=tint_symbol_91};
-  tint_symbol_2 const tint_symbol_82 = {.x_GLF_color=tint_symbol_3.x_GLF_color};
+  main_out const tint_symbol_3 = {.x_GLF_color_1=tint_symbol_91};
+  tint_symbol_2 const tint_symbol_82 = {.x_GLF_color_1=tint_symbol_3.x_GLF_color_1};
   return tint_symbol_82;
 }
 
diff --git a/test/bug/tint/749.spvasm.expected.spvasm b/test/bug/tint/749.spvasm.expected.spvasm
index 1ad4312..2dc95c7 100644
--- a/test/bug/tint/749.spvasm.expected.spvasm
+++ b/test/bug/tint/749.spvasm.expected.spvasm
@@ -47,7 +47,7 @@
                OpName %i_2 "i_2"
                OpName %uv "uv"
                OpName %main_out "main_out"
-               OpMemberName %main_out 0 "x_GLF_color"
+               OpMemberName %main_out 0 "x_GLF_color_1"
                OpName %tint_symbol_3 "tint_symbol_3"
                OpName %tint_symbol_1 "tint_symbol_1"
                OpName %main "main"
diff --git a/test/bug/tint/749.spvasm.expected.wgsl b/test/bug/tint/749.spvasm.expected.wgsl
index f24a266..ef02a83 100644
--- a/test/bug/tint/749.spvasm.expected.wgsl
+++ b/test/bug/tint/749.spvasm.expected.wgsl
@@ -1494,7 +1494,7 @@
 
 struct main_out {
   [[location(0)]]
-  x_GLF_color : vec4<f32>;
+  x_GLF_color_1 : vec4<f32>;
 };
 
 [[stage(fragment)]]