[spirv-reader] Handle gl_Position

Emits it as a module-level variable.  Deconstruct and throw away
the gl_PerVertex struct.

Not handled: unusual patterns that are technically valid but
which don't occur in practice:
- loading, storing, or producing intermediate values of the whole structure.
- multiple definitions of the per-vertex structure (e.g. if someone had
  put both a vertex shader and a tessellation shader in the same
  module.)

Bug: tint:3, tint:99
Change-Id: I3ad9ff6ab780a002367f01f385bfa7d6ddba6db9
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/24880
Reviewed-by: dan sinclair <dsinclair@chromium.org>
diff --git a/src/reader/spirv/function.cc b/src/reader/spirv/function.cc
index c6ed309..f62c967 100644
--- a/src/reader/spirv/function.cc
+++ b/src/reader/spirv/function.cc
@@ -2512,9 +2512,21 @@
 
 bool FunctionEmitter::EmitStatement(const spvtools::opt::Instruction& inst) {
   const auto result_id = inst.result_id();
+  const auto type_id = inst.type_id();
+
+  if (type_id != 0) {
+    const auto& builtin_position_info = parser_impl_.GetBuiltInPositionInfo();
+    if ((type_id == builtin_position_info.struct_type_id) ||
+        (type_id == builtin_position_info.pointer_type_id)) {
+      return Fail() << "operations producing a per-vertex structure are not "
+                       "supported: "
+                    << inst.PrettyPrint();
+    }
+  }
+
   // Handle combinatorial instructions.
-  auto combinatorial_expr = MaybeEmitCombinatorialValue(inst);
   const auto* def_info = GetDefInfo(result_id);
+  auto combinatorial_expr = MaybeEmitCombinatorialValue(inst);
   if (combinatorial_expr.expr != nullptr) {
     if (def_info == nullptr) {
       return Fail() << "internal error: result ID %" << result_id
@@ -2542,9 +2554,19 @@
       return true;
 
     case SpvOpStore: {
+      const auto ptr_id = inst.GetSingleWordInOperand(0);
+      const auto value_id = inst.GetSingleWordInOperand(1);
+      const auto ptr_type_id = def_use_mgr_->GetDef(ptr_id)->type_id();
+      const auto& builtin_position_info = parser_impl_.GetBuiltInPositionInfo();
+      if (ptr_type_id == builtin_position_info.pointer_type_id) {
+        return Fail()
+               << "storing to the whole per-vertex structure is not supported: "
+               << inst.PrettyPrint();
+      }
+
       // TODO(dneto): Order of evaluation?
-      auto lhs = MakeExpression(inst.GetSingleWordInOperand(0));
-      auto rhs = MakeExpression(inst.GetSingleWordInOperand(1));
+      auto lhs = MakeExpression(ptr_id);
+      auto rhs = MakeExpression(value_id);
       AddStatement(std::make_unique<ast::AssignmentStatement>(
           std::move(lhs.expr), std::move(rhs.expr)));
       return success();
@@ -2737,7 +2759,56 @@
   static const char* swizzles[] = {"x", "y", "z", "w"};
 
   const auto base_id = inst.GetSingleWordInOperand(0);
-  const auto ptr_ty_id = def_use_mgr_->GetDef(base_id)->type_id();
+  auto ptr_ty_id = def_use_mgr_->GetDef(base_id)->type_id();
+  uint32_t first_index = 1;
+  const auto num_in_operands = inst.NumInOperands();
+
+  // If the variable was originally gl_PerVertex, then in the AST we
+  // have instead emitted a gl_Position variable.
+  {
+    const auto& builtin_position_info = parser_impl_.GetBuiltInPositionInfo();
+    if (base_id == builtin_position_info.per_vertex_var_id) {
+      // We only support the Position member.
+      const auto* member_index_inst =
+          def_use_mgr_->GetDef(inst.GetSingleWordInOperand(first_index));
+      if (member_index_inst == nullptr) {
+        Fail()
+            << "first index of access chain does not reference an instruction: "
+            << inst.PrettyPrint();
+        return {};
+      }
+      const auto* member_index_const =
+          constant_mgr_->GetConstantFromInst(member_index_inst);
+      if (member_index_const == nullptr) {
+        Fail() << "first index of access chain into per-vertex structure is "
+                  "not a constant: "
+               << inst.PrettyPrint();
+        return {};
+      }
+      const auto* member_index_const_int = member_index_const->AsIntConstant();
+      if (member_index_const_int == nullptr) {
+        Fail() << "first index of access chain into per-vertex structure is "
+                  "not a constant integer: "
+               << inst.PrettyPrint();
+        return {};
+      }
+      const auto member_index_value =
+          member_index_const_int->GetZeroExtendedValue();
+      if (member_index_value != builtin_position_info.member_index) {
+        Fail() << "accessing per-vertex member " << member_index_value
+               << " is not supported. Only Position is supported";
+        return {};
+      }
+
+      // Skip past the member index that gets us to Position.
+      first_index = first_index + 1;
+      // Replace the gl_PerVertex reference with the gl_Position reference
+      current_expr.expr =
+          std::make_unique<ast::IdentifierExpression>(namer_.Name(base_id));
+      ptr_ty_id = builtin_position_info.member_pointer_type_id;
+    }
+  }
+
   const auto* ptr_type = type_mgr_->GetType(ptr_ty_id);
   if (!ptr_type || !ptr_type->AsPointer()) {
     Fail() << "Access chain %" << inst.result_id()
@@ -2745,8 +2816,7 @@
     return {};
   }
   const auto* pointee_type = ptr_type->AsPointer()->pointee_type();
-  const auto num_in_operands = inst.NumInOperands();
-  for (uint32_t index = 1; index < num_in_operands; ++index) {
+  for (uint32_t index = first_index; index < num_in_operands; ++index) {
     const auto* index_const =
         constants[index] ? constants[index]->AsIntConstant() : nullptr;
     const int64_t index_const_val =
diff --git a/src/reader/spirv/parser_impl.cc b/src/reader/spirv/parser_impl.cc
index 6794a37..27cac0a 100644
--- a/src/reader/spirv/parser_impl.cc
+++ b/src/reader/spirv/parser_impl.cc
@@ -36,6 +36,7 @@
 #include "src/ast/as_expression.h"
 #include "src/ast/binary_expression.h"
 #include "src/ast/bool_literal.h"
+#include "src/ast/builtin.h"
 #include "src/ast/builtin_decoration.h"
 #include "src/ast/decorated_variable.h"
 #include "src/ast/float_literal.h"
@@ -280,8 +281,8 @@
   auto save = [this, type_id, spirv_type](ast::type::Type* type) {
     if (type != nullptr) {
       id_to_type_[type_id] = type;
+      MaybeGenerateAlias(type_id, spirv_type);
     }
-    MaybeGenerateAlias(type_id, spirv_type);
     return type;
   };
 
@@ -305,7 +306,7 @@
     case spvtools::opt::analysis::Type::kStruct:
       return save(ConvertType(type_id, spirv_type->AsStruct()));
     case spvtools::opt::analysis::Type::kPointer:
-      return save(ConvertType(spirv_type->AsPointer()));
+      return save(ConvertType(type_id, spirv_type->AsPointer()));
     case spvtools::opt::analysis::Type::kFunction:
       // Tint doesn't have a Function type.
       // We need to convert the result type and parameter types.
@@ -489,6 +490,7 @@
       case SpvOpName:
         namer_.SuggestSanitizedName(inst.GetSingleWordInOperand(0),
                                     inst.GetInOperand(1).AsString());
+
         break;
       case SpvOpMemberName:
         namer_.SuggestSanitizedMemberName(inst.GetSingleWordInOperand(0),
@@ -690,19 +692,43 @@
   const auto members = struct_ty->element_types();
   for (uint32_t member_index = 0; member_index < members.size();
        ++member_index) {
-    auto* ast_member_ty = ConvertType(type_mgr_->GetId(members[member_index]));
+    const auto member_type_id = type_mgr_->GetId(members[member_index]);
+    auto* ast_member_ty = ConvertType(member_type_id);
     if (ast_member_ty == nullptr) {
       // Already emitted diagnostics.
       return nullptr;
     }
     ast::StructMemberDecorationList ast_member_decorations;
-    for (auto& deco : GetDecorationsForMember(type_id, member_index)) {
-      auto ast_member_decoration = ConvertMemberDecoration(deco);
-      if (ast_member_decoration == nullptr) {
-        // Already emitted diagnostics.
+    for (auto& decoration : GetDecorationsForMember(type_id, member_index)) {
+      if (decoration.empty()) {
+        Fail() << "malformed SPIR-V decoration: it's empty";
         return nullptr;
       }
-      ast_member_decorations.push_back(std::move(ast_member_decoration));
+      if ((decoration[0] == SpvDecorationBuiltIn) && (decoration.size() > 1)) {
+        switch (decoration[1]) {
+          case SpvBuiltInPosition:
+            // Record this built-in variable specially.
+            builtin_position_.struct_type_id = type_id;
+            builtin_position_.member_index = member_index;
+            builtin_position_.member_type_id = member_type_id;
+            // Don't map the struct type.  But this is not an error either.
+            return nullptr;
+          case SpvBuiltInPointSize:     // not supported in WGSL
+          case SpvBuiltInCullDistance:  // not supported in WGSL
+          case SpvBuiltInClipDistance:  // not supported in WGSL
+          default:
+            break;
+        }
+        Fail() << "unrecognized builtin " << decoration[1];
+        return nullptr;
+      } else {
+        auto ast_member_decoration = ConvertMemberDecoration(decoration);
+        if (ast_member_decoration == nullptr) {
+          // Already emitted diagnostics.
+          return nullptr;
+        }
+        ast_member_decorations.push_back(std::move(ast_member_decoration));
+      }
     }
     const auto member_name = namer_.GetMemberName(type_id, member_index);
     auto ast_struct_member = std::make_unique<ast::StructMember>(
@@ -723,20 +749,27 @@
 }
 
 ast::type::Type* ParserImpl::ConvertType(
-    const spvtools::opt::analysis::Pointer* ptr_ty) {
-  auto* ast_elem_ty = ConvertType(type_mgr_->GetId(ptr_ty->pointee_type()));
-  if (ast_elem_ty == nullptr) {
-    Fail() << "SPIR-V pointer type with ID " << type_mgr_->GetId(ptr_ty)
-           << " has invalid pointee type "
-           << type_mgr_->GetId(ptr_ty->pointee_type());
+    uint32_t type_id,
+    const spvtools::opt::analysis::Pointer*) {
+  const auto* inst = def_use_mgr_->GetDef(type_id);
+  const auto pointee_ty_id = inst->GetSingleWordInOperand(1);
+  const auto storage_class = SpvStorageClass(inst->GetSingleWordInOperand(0));
+  if (pointee_ty_id == builtin_position_.struct_type_id) {
+    builtin_position_.pointer_type_id = type_id;
+    builtin_position_.storage_class = storage_class;
     return nullptr;
   }
-  auto ast_storage_class =
-      enum_converter_.ToStorageClass(ptr_ty->storage_class());
+  auto* ast_elem_ty = ConvertType(pointee_ty_id);
+  if (ast_elem_ty == nullptr) {
+    Fail() << "SPIR-V pointer type with ID " << type_id
+           << " has invalid pointee type " << pointee_ty_id;
+    return nullptr;
+  }
+  auto ast_storage_class = enum_converter_.ToStorageClass(storage_class);
   if (ast_storage_class == ast::StorageClass::kNone) {
-    Fail() << "SPIR-V pointer type with ID " << type_mgr_->GetId(ptr_ty)
+    Fail() << "SPIR-V pointer type with ID " << type_id
            << " has invalid storage class "
-           << static_cast<uint32_t>(ptr_ty->storage_class());
+           << static_cast<uint32_t>(storage_class);
     return nullptr;
   }
   return ctx_.type_mgr().Get(
@@ -754,6 +787,13 @@
     }
     ConvertType(type_or_const.result_id());
   }
+  // Manufacture a type for the gl_Position varible if we have to.
+  if ((builtin_position_.struct_type_id != 0) &&
+      (builtin_position_.member_pointer_type_id == 0)) {
+    builtin_position_.member_pointer_type_id = type_mgr_->FindPointerToType(
+        builtin_position_.member_type_id, builtin_position_.storage_class);
+    ConvertType(builtin_position_.member_pointer_type_id);
+  }
   return success_;
 }
 
@@ -809,12 +849,22 @@
     }
     const auto& var = type_or_value;
     const auto spirv_storage_class = var.GetSingleWordInOperand(0);
+
+    uint32_t type_id = var.type_id();
+    if ((type_id == builtin_position_.pointer_type_id) &&
+        ((spirv_storage_class == SpvStorageClassInput) ||
+         (spirv_storage_class == SpvStorageClassOutput))) {
+      // Skip emitting gl_PerVertex.
+      builtin_position_.per_vertex_var_id = var.result_id();
+      continue;
+    }
+
     auto ast_storage_class = enum_converter_.ToStorageClass(
         static_cast<SpvStorageClass>(spirv_storage_class));
     if (!success_) {
       return false;
     }
-    auto* ast_type = id_to_type_[var.type_id()];
+    auto* ast_type = id_to_type_[type_id];
     if (ast_type == nullptr) {
       return Fail() << "internal error: failed to register Tint AST type for "
                        "SPIR-V type with ID: "
@@ -837,6 +887,23 @@
     // TODO(dneto): initializers (a.k.a. constructor expression)
     ast_module_.AddGlobalVariable(std::move(ast_var));
   }
+
+  // Emit gl_Position instead of gl_PerVertex
+  if (builtin_position_.per_vertex_var_id) {
+    // Make sure the variable has a name.
+    namer_.SuggestSanitizedName(builtin_position_.per_vertex_var_id,
+                                "gl_Position");
+    auto var = std::make_unique<ast::DecoratedVariable>(MakeVariable(
+        builtin_position_.per_vertex_var_id,
+        enum_converter_.ToStorageClass(builtin_position_.storage_class),
+        ConvertType(builtin_position_.member_type_id)));
+    ast::VariableDecorationList decos;
+    decos.push_back(
+        std::make_unique<ast::BuiltinDecoration>(ast::Builtin::kPosition));
+    var->set_decorations(std::move(decos));
+
+    ast_module_.AddGlobalVariable(std::move(var));
+  }
   return success_;
 }
 
diff --git a/src/reader/spirv/parser_impl.h b/src/reader/spirv/parser_impl.h
index 281adaa..6fc60a5 100644
--- a/src/reader/spirv/parser_impl.h
+++ b/src/reader/spirv/parser_impl.h
@@ -133,6 +133,8 @@
   std::string GlslStd450Prefix() const { return "std::glsl"; }
 
   /// Converts a SPIR-V type to a Tint type, and saves it for fast lookup.
+  /// If the type is only used for builtins, then register that specially,
+  /// and return null.
   /// On failure, logs an error and returns null.  This should only be called
   /// after the internal representation of the module has been built.
   /// @param type_id the SPIR-V ID of a type.
@@ -316,6 +318,30 @@
   /// @returns the registered boolean type.
   ast::type::Type* BoolType() const { return bool_type_; }
 
+  /// Bookkeeping used for tracking the "position" builtin variable.
+  struct BuiltInPositionInfo {
+    /// The ID for the gl_PerVertex struct containing the Position builtin.
+    uint32_t struct_type_id = 0;
+    /// The member index for the Position builtin within the struct.
+    uint32_t member_index = 0;
+    /// The ID for the member type, which should map to vec4<f32>.
+    uint32_t member_type_id = 0;
+    /// The ID of the type of a pointer to the struct in the Output storage
+    /// class class.
+    uint32_t pointer_type_id = 0;
+    /// The SPIR-V storage class.
+    SpvStorageClass storage_class = SpvStorageClassOutput;
+    /// The ID of the type of a pointer to the Position member.
+    uint32_t member_pointer_type_id = 0;
+    /// The ID of the gl_PerVertex variable, if it was declared.
+    /// We'll use this for the gl_Position variable instead.
+    uint32_t per_vertex_var_id = 0;
+  };
+  /// @returns info about the gl_Position builtin variable.
+  const BuiltInPositionInfo& GetBuiltInPositionInfo() {
+    return builtin_position_;
+  }
+
  private:
   /// Converts a specific SPIR-V type to a Tint type. Integer case
   ast::type::Type* ConvertType(const spvtools::opt::analysis::Integer* int_ty);
@@ -347,8 +373,12 @@
       uint32_t type_id,
       const spvtools::opt::analysis::Struct* struct_ty);
   /// Converts a specific SPIR-V type to a Tint type. Pointer case
+  /// The pointer to gl_PerVertex maps to nullptr, and instead is recorded
+  /// in member |builtin_position_|.
+  /// @param type_id the SPIR-V ID for the type.
   /// @param ptr_ty the Tint type
-  ast::type::Type* ConvertType(const spvtools::opt::analysis::Pointer* ptr_ty);
+  ast::type::Type* ConvertType(uint32_t type_id,
+                               const spvtools::opt::analysis::Pointer* ptr_ty);
 
   /// Applies SPIR-V decorations to the given array or runtime-array type.
   /// @param spv_type the SPIR-V aray or runtime-array type.
@@ -402,6 +432,13 @@
   std::unordered_map<ast::type::Type*, ast::type::Type*> signed_type_for_;
   // Maps an signed type corresponding to the given unsigned type.
   std::unordered_map<ast::type::Type*, ast::type::Type*> unsigned_type_for_;
+
+  // Bookkeeping for the gl_Position builtin.
+  // In Vulkan SPIR-V, it's the 0 member of the gl_PerVertex structure.
+  // But in WGSL we make a module-scope variable:
+  //    [[position]] var<in> gl_Position : vec4<f32>;
+  // The builtin variable was detected if and only if the struct_id is non-zero.
+  BuiltInPositionInfo builtin_position_;
 };
 
 }  // namespace spirv
diff --git a/src/reader/spirv/parser_impl_module_var_test.cc b/src/reader/spirv/parser_impl_module_var_test.cc
index b0c9d00..11e0f4a 100644
--- a/src/reader/spirv/parser_impl_module_var_test.cc
+++ b/src/reader/spirv/parser_impl_module_var_test.cc
@@ -24,6 +24,7 @@
 namespace spirv {
 namespace {
 
+using ::testing::Eq;
 using ::testing::HasSubstr;
 using ::testing::Not;
 
@@ -169,7 +170,7 @@
   })"));
 }
 
-TEST_F(SpvParserTest, ModuleScopeVar_BuiltinVerteIndex) {
+TEST_F(SpvParserTest, ModuleScopeVar_BuiltinVertexIndex) {
   auto* p = parser(test::Assemble(R"(
     OpDecorate %52 BuiltIn VertexIndex
     %uint = OpTypeInt 32 0
@@ -191,6 +192,299 @@
   })"));
 }
 
+std::string PerVertexPreamble() {
+  return R"(
+    OpCapability Shader
+    OpCapability Linkage ; so we don't have to declare an entry point
+    OpMemoryModel Logical Simple
+
+    OpMemberDecorate %10 0 BuiltIn Position
+    OpMemberDecorate %10 1 BuiltIn PointSize
+    OpMemberDecorate %10 2 BuiltIn ClipDistance
+    OpMemberDecorate %10 3 BuiltIn CullDistance
+    %void = OpTypeVoid
+    %voidfn = OpTypeFunction %void
+    %float = OpTypeFloat 32
+    %12 = OpTypeVector %float 4
+    %uint = OpTypeInt 32 0
+    %uint_0 = OpConstant %uint 0
+    %uint_1 = OpConstant %uint 1
+    %arr = OpTypeArray %float %uint_1
+    %10 = OpTypeStruct %12 %float %arr %arr
+    %11 = OpTypePointer Output %10
+    %1 = OpVariable %11 Output
+)";
+}
+
+TEST_F(SpvParserTest, ModuleScopeVar_BuiltinPosition_MapsToModuleScopeVec4Var) {
+  // In Vulkan SPIR-V, Position is the first member of gl_PerVertex
+  const std::string assembly = PerVertexPreamble();
+  auto* p = parser(test::Assemble(assembly));
+
+  EXPECT_TRUE(p->BuildAndParseInternalModule()) << assembly;
+  EXPECT_TRUE(p->error().empty()) << p->error();
+  const auto& position_info = p->GetBuiltInPositionInfo();
+  EXPECT_EQ(position_info.struct_type_id, 10u);
+  EXPECT_EQ(position_info.member_index, 0u);
+  EXPECT_EQ(position_info.member_type_id, 12u);
+  EXPECT_EQ(position_info.pointer_type_id, 11u);
+  EXPECT_EQ(position_info.storage_class, SpvStorageClassOutput);
+  EXPECT_EQ(position_info.per_vertex_var_id, 1u);
+  const auto module_str = p->module().to_str();
+  EXPECT_THAT(module_str, HasSubstr(R"(
+  DecoratedVariable{
+    Decorations{
+      BuiltinDecoration{position}
+    }
+    gl_Position
+    out
+    __vec_4__f32
+  })"))
+      << module_str;
+}
+
+TEST_F(SpvParserTest,
+       ModuleScopeVar_BuiltinPosition_StoreWholeStruct_NotSupported) {
+  // Glslang does not generate this code pattern.
+  const std::string assembly = PerVertexPreamble() + R"(
+  %nil = OpConstantNull %10 ; the whole struct
+
+  %main = OpFunction %void None %voidfn
+  %entry = OpLabel
+  OpStore %1 %nil  ; store the whole struct
+  OpReturn
+  OpFunctionEnd
+  )";
+  auto* p = parser(test::Assemble(assembly));
+  EXPECT_FALSE(p->BuildAndParseInternalModule()) << assembly;
+  EXPECT_THAT(p->error(), Eq("storing to the whole per-vertex structure is not "
+                             "supported: OpStore %1 %9"))
+      << p->error();
+}
+
+TEST_F(SpvParserTest,
+       ModuleScopeVar_BuiltinPosition_IntermediateWholeStruct_NotSupported) {
+  const std::string assembly = PerVertexPreamble() + R"(
+  %main = OpFunction %void None %voidfn
+  %entry = OpLabel
+  %1000 = OpUndef %10
+  OpReturn
+  OpFunctionEnd
+  )";
+  auto* p = parser(test::Assemble(assembly));
+  EXPECT_FALSE(p->BuildAndParseInternalModule()) << assembly;
+  EXPECT_THAT(p->error(), Eq("operations producing a per-vertex structure are "
+                             "not supported: %1000 = OpUndef %10"))
+      << p->error();
+}
+
+TEST_F(SpvParserTest,
+       ModuleScopeVar_BuiltinPosition_IntermediatePtrWholeStruct_NotSupported) {
+  const std::string assembly = PerVertexPreamble() + R"(
+  %main = OpFunction %void None %voidfn
+  %entry = OpLabel
+  %1000 = OpUndef %11
+  OpReturn
+  OpFunctionEnd
+  )";
+  auto* p = parser(test::Assemble(assembly));
+  EXPECT_FALSE(p->BuildAndParseInternalModule());
+  EXPECT_THAT(p->error(), Eq("operations producing a per-vertex structure are "
+                             "not supported: %1000 = OpUndef %11"))
+      << p->error();
+}
+
+TEST_F(SpvParserTest, ModuleScopeVar_BuiltinPosition_StorePosition) {
+  const std::string assembly = PerVertexPreamble() + R"(
+  %ptr_v4float = OpTypePointer Output %12
+  %nil = OpConstantNull %12
+
+  %main = OpFunction %void None %voidfn
+  %entry = OpLabel
+  %100 = OpAccessChain %ptr_v4float %1 %uint_0 ; address of the Position member
+  OpStore %100 %nil
+  OpReturn
+  OpFunctionEnd
+  )";
+  auto* p = parser(test::Assemble(assembly));
+  EXPECT_TRUE(p->BuildAndParseInternalModule());
+  EXPECT_TRUE(p->error().empty());
+  const auto module_str = p->module().to_str();
+  EXPECT_THAT(module_str, HasSubstr(R"(
+    Assignment{
+      Identifier{gl_Position}
+      TypeConstructor{
+        __vec_4__f32
+        ScalarConstructor{0.000000}
+        ScalarConstructor{0.000000}
+        ScalarConstructor{0.000000}
+        ScalarConstructor{0.000000}
+      }
+    })"))
+      << module_str;
+}
+
+TEST_F(SpvParserTest,
+       ModuleScopeVar_BuiltinPosition_StorePositionMember_OneAccessChain) {
+  const std::string assembly = PerVertexPreamble() + R"(
+  %ptr_float = OpTypePointer Output %float
+  %nil = OpConstantNull %float
+
+  %main = OpFunction %void None %voidfn
+  %entry = OpLabel
+  %100 = OpAccessChain %ptr_float %1 %uint_0 %uint_1 ; address of the Position.y member
+  OpStore %100 %nil
+  OpReturn
+  OpFunctionEnd
+  )";
+  auto* p = parser(test::Assemble(assembly));
+  EXPECT_TRUE(p->BuildAndParseInternalModule());
+  EXPECT_TRUE(p->error().empty());
+  const auto module_str = p->module().to_str();
+  EXPECT_THAT(module_str, HasSubstr(R"(
+    Assignment{
+      MemberAccessor{
+        Identifier{gl_Position}
+        Identifier{y}
+      }
+      ScalarConstructor{0.000000}
+    })"))
+      << module_str;
+}
+
+TEST_F(SpvParserTest,
+       ModuleScopeVar_BuiltinPosition_StorePositionMember_TwoAccessChain) {
+  // The algorithm is smart enough to collapse it down.
+  const std::string assembly = PerVertexPreamble() + R"(
+  %ptr_v4float = OpTypePointer Output %12
+  %ptr_float = OpTypePointer Output %float
+  %nil = OpConstantNull %float
+
+  %main = OpFunction %void None %voidfn
+  %entry = OpLabel
+  %100 = OpAccessChain %ptr_v4float %1 %uint_0 ; address of the Position member
+  %101 = OpAccessChain %ptr_float %100 %uint_1 ; address of the Position.y member
+  OpStore %101 %nil
+  OpReturn
+  OpFunctionEnd
+  )";
+  auto* p = parser(test::Assemble(assembly));
+  EXPECT_TRUE(p->BuildAndParseInternalModule());
+  EXPECT_TRUE(p->error().empty());
+  const auto module_str = p->module().to_str();
+  EXPECT_THAT(module_str, HasSubstr(R"(
+  {
+    Assignment{
+      MemberAccessor{
+        Identifier{gl_Position}
+        Identifier{y}
+      }
+      ScalarConstructor{0.000000}
+    }
+    Return{}
+  })"))
+      << module_str;
+}
+
+TEST_F(SpvParserTest, ModuleScopeVar_BuiltinPointSize_NotSupported) {
+  const std::string assembly = PerVertexPreamble() + R"(
+  %ptr_v4float = OpTypePointer Output %12
+  %nil = OpConstantNull %12
+
+  %main = OpFunction %void None %voidfn
+  %entry = OpLabel
+  %100 = OpAccessChain %ptr_v4float %1 %uint_1 ; address of the PointSize member
+  OpStore %100 %nil
+  OpReturn
+  OpFunctionEnd
+  )";
+  auto* p = parser(test::Assemble(assembly));
+  EXPECT_FALSE(p->BuildAndParseInternalModule());
+  EXPECT_THAT(p->error(), Eq("accessing per-vertex member 1 is not supported. "
+                             "Only Position is supported"));
+}
+
+TEST_F(SpvParserTest, ModuleScopeVar_BuiltinClipDistance_NotSupported) {
+  const std::string assembly = PerVertexPreamble() + R"(
+  %ptr_float = OpTypePointer Output %float
+  %nil = OpConstantNull %float
+  %uint_2 = OpConstant %uint 2
+
+  %main = OpFunction %void None %voidfn
+  %entry = OpLabel
+; address of the first entry in ClipDistance
+  %100 = OpAccessChain %ptr_float %1 %uint_2 %uint_0
+  OpStore %100 %nil
+  OpReturn
+  OpFunctionEnd
+  )";
+  auto* p = parser(test::Assemble(assembly));
+  EXPECT_FALSE(p->BuildAndParseInternalModule());
+  EXPECT_THAT(p->error(), Eq("accessing per-vertex member 2 is not supported. "
+                             "Only Position is supported"));
+}
+
+TEST_F(SpvParserTest, ModuleScopeVar_BuiltinCullDistance_NotSupported) {
+  const std::string assembly = PerVertexPreamble() + R"(
+  %ptr_float = OpTypePointer Output %float
+  %nil = OpConstantNull %float
+  %uint_3 = OpConstant %uint 3
+
+  %main = OpFunction %void None %voidfn
+  %entry = OpLabel
+; address of the first entry in CullDistance
+  %100 = OpAccessChain %ptr_float %1 %uint_3 %uint_0
+  OpStore %100 %nil
+  OpReturn
+  OpFunctionEnd
+  )";
+  auto* p = parser(test::Assemble(assembly));
+  EXPECT_FALSE(p->BuildAndParseInternalModule());
+  EXPECT_THAT(p->error(), Eq("accessing per-vertex member 3 is not supported. "
+                             "Only Position is supported"));
+}
+
+TEST_F(SpvParserTest, ModuleScopeVar_BuiltinPerVertex_MemberIndex_NotConstant) {
+  const std::string assembly = PerVertexPreamble() + R"(
+  %ptr_float = OpTypePointer Output %float
+  %nil = OpConstantNull %float
+
+  %main = OpFunction %void None %voidfn
+  %entry = OpLabel
+  %sum = OpIAdd %uint %uint_0 %uint_0
+  %100 = OpAccessChain %ptr_float %1 %sum
+  OpStore %100 %nil
+  OpReturn
+  OpFunctionEnd
+  )";
+  auto* p = parser(test::Assemble(assembly));
+  EXPECT_FALSE(p->BuildAndParseInternalModule());
+  EXPECT_THAT(p->error(),
+              Eq("first index of access chain into per-vertex structure is not "
+                 "a constant: %100 = OpAccessChain %9 %1 %16"));
+}
+
+TEST_F(SpvParserTest,
+       ModuleScopeVar_BuiltinPerVertex_MemberIndex_NotConstantInteger) {
+  const std::string assembly = PerVertexPreamble() + R"(
+  %ptr_float = OpTypePointer Output %float
+  %nil = OpConstantNull %float
+
+  %main = OpFunction %void None %voidfn
+  %entry = OpLabel
+; nil is bad here!
+  %100 = OpAccessChain %ptr_float %1 %nil
+  OpStore %100 %nil
+  OpReturn
+  OpFunctionEnd
+  )";
+  auto* p = parser(test::Assemble(assembly));
+  EXPECT_FALSE(p->BuildAndParseInternalModule());
+  EXPECT_THAT(p->error(),
+              Eq("first index of access chain into per-vertex structure is not "
+                 "a constant integer: %100 = OpAccessChain %9 %1 %13"));
+}
+
 TEST_F(SpvParserTest, ModuleScopeVar_ScalarInitializers) {
   auto* p = parser(test::Assemble(CommonTypes() + R"(
      %1 = OpVariable %ptr_bool Private %true