[inspector] Extract UBO information

Also includes adding in sizing information for various types.

BUG=tint:257

Change-Id: Iaaa8a7c28851d14790285b5bd14636bf3ae2b9b0
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/30704
Commit-Queue: Ryan Harrison <rharrison@chromium.org>
Commit-Queue: dan sinclair <dsinclair@chromium.org>
Reviewed-by: dan sinclair <dsinclair@chromium.org>
diff --git a/src/ast/type/alias_type.cc b/src/ast/type/alias_type.cc
index cce05e3..d8f67d9 100644
--- a/src/ast/type/alias_type.cc
+++ b/src/ast/type/alias_type.cc
@@ -35,6 +35,10 @@
   return "__alias_" + name_ + subtype_->type_name();
 }
 
+uint64_t AliasType::MinBufferBindingSize() const {
+  return subtype_->MinBufferBindingSize();
+}
+
 }  // namespace type
 }  // namespace ast
 }  // namespace tint
diff --git a/src/ast/type/alias_type.h b/src/ast/type/alias_type.h
index d407306..bc7505a 100644
--- a/src/ast/type/alias_type.h
+++ b/src/ast/type/alias_type.h
@@ -23,7 +23,7 @@
 namespace ast {
 namespace type {
 
-/// A type alias type. Holds a name a pointer to another type.
+/// A type alias type. Holds a name and pointer to another type.
 class AliasType : public Type {
  public:
   /// Constructor
@@ -45,6 +45,10 @@
   /// @returns the name for this type
   std::string type_name() const override;
 
+  /// @returns minimum size required for this type, in bytes.
+  ///          0 for non-host shareable types.
+  uint64_t MinBufferBindingSize() const override;
+
  private:
   std::string name_;
   Type* subtype_ = nullptr;
diff --git a/src/ast/type/array_type.cc b/src/ast/type/array_type.cc
index 71b3861..179f6de 100644
--- a/src/ast/type/array_type.cc
+++ b/src/ast/type/array_type.cc
@@ -33,6 +33,18 @@
   return true;
 }
 
+uint64_t ArrayType::MinBufferBindingSize() const {
+  if (IsRuntimeArray()) {
+    return array_stride();
+  }
+
+  if (has_array_stride()) {
+    return size_ * array_stride();
+  }
+
+  return size_ * type()->MinBufferBindingSize();
+}
+
 uint32_t ArrayType::array_stride() const {
   for (const auto& deco : decos_) {
     if (deco->IsStride()) {
diff --git a/src/ast/type/array_type.h b/src/ast/type/array_type.h
index 9a84d1e..fceca8e 100644
--- a/src/ast/type/array_type.h
+++ b/src/ast/type/array_type.h
@@ -46,6 +46,10 @@
   /// i.e. the size is determined at runtime
   bool IsRuntimeArray() const { return size_ == 0; }
 
+  /// @returns minimum size required for this type, in bytes.
+  ///          0 for non-host shareable types.
+  uint64_t MinBufferBindingSize() const override;
+
   /// Sets the array decorations
   /// @param decos the decorations to set
   void set_decorations(ast::ArrayDecorationList decos) {
diff --git a/src/ast/type/f32_type.cc b/src/ast/type/f32_type.cc
index bac892b..2306bd0 100644
--- a/src/ast/type/f32_type.cc
+++ b/src/ast/type/f32_type.cc
@@ -30,6 +30,10 @@
   return "__f32";
 }
 
+uint64_t F32Type::MinBufferBindingSize() const {
+  return 4;
+}
+
 }  // namespace type
 }  // namespace ast
 }  // namespace tint
diff --git a/src/ast/type/f32_type.h b/src/ast/type/f32_type.h
index a975837..8a0a5b6 100644
--- a/src/ast/type/f32_type.h
+++ b/src/ast/type/f32_type.h
@@ -37,6 +37,10 @@
 
   /// @returns the name for this type
   std::string type_name() const override;
+
+  /// @returns minimum size required for this type, in bytes.
+  ///          0 for non-host shareable types.
+  uint64_t MinBufferBindingSize() const override;
 };
 
 }  // namespace type
diff --git a/src/ast/type/i32_type.cc b/src/ast/type/i32_type.cc
index fd5ab0c..772f5cf 100644
--- a/src/ast/type/i32_type.cc
+++ b/src/ast/type/i32_type.cc
@@ -30,6 +30,10 @@
   return "__i32";
 }
 
+uint64_t I32Type::MinBufferBindingSize() const {
+  return 4;
+}
+
 }  // namespace type
 }  // namespace ast
 }  // namespace tint
diff --git a/src/ast/type/i32_type.h b/src/ast/type/i32_type.h
index 3f8a213..6c6b625 100644
--- a/src/ast/type/i32_type.h
+++ b/src/ast/type/i32_type.h
@@ -37,6 +37,10 @@
 
   /// @returns the name for this type
   std::string type_name() const override;
+
+  /// @returns minimum size required for this type, in bytes.
+  ///          0 for non-host shareable types.
+  uint64_t MinBufferBindingSize() const override;
 };
 
 }  // namespace type
diff --git a/src/ast/type/matrix_type.cc b/src/ast/type/matrix_type.cc
index d34208c..bd60c6c 100644
--- a/src/ast/type/matrix_type.cc
+++ b/src/ast/type/matrix_type.cc
@@ -28,6 +28,8 @@
   assert(columns < 5);
 }
 
+MatrixType::~MatrixType() = default;
+
 bool MatrixType::IsMatrix() const {
   return true;
 }
@@ -37,7 +39,9 @@
          subtype_->type_name();
 }
 
-MatrixType::~MatrixType() = default;
+uint64_t MatrixType::MinBufferBindingSize() const {
+  return rows_ * columns_ * subtype_->MinBufferBindingSize();
+}
 
 }  // namespace type
 }  // namespace ast
diff --git a/src/ast/type/matrix_type.h b/src/ast/type/matrix_type.h
index 35cd932..6a42d35 100644
--- a/src/ast/type/matrix_type.h
+++ b/src/ast/type/matrix_type.h
@@ -48,6 +48,10 @@
   /// @returns the name for this type
   std::string type_name() const override;
 
+  /// @returns minimum size required for this type, in bytes.
+  ///          0 for non-host shareable types.
+  uint64_t MinBufferBindingSize() const override;
+
  private:
   Type* subtype_ = nullptr;
   uint32_t rows_ = 2;
diff --git a/src/ast/type/pointer_type.cc b/src/ast/type/pointer_type.cc
index e6083c8..507dbed 100644
--- a/src/ast/type/pointer_type.cc
+++ b/src/ast/type/pointer_type.cc
@@ -25,6 +25,10 @@
   return true;
 }
 
+uint64_t PointerType::MinBufferBindingSize() const {
+  return 4;
+}
+
 std::string PointerType::type_name() const {
   std::ostringstream out;
   out << "__ptr_" << storage_class_ << subtype_->type_name();
diff --git a/src/ast/type/pointer_type.h b/src/ast/type/pointer_type.h
index fa97975..dbf7e5c 100644
--- a/src/ast/type/pointer_type.h
+++ b/src/ast/type/pointer_type.h
@@ -39,6 +39,10 @@
   /// @returns true if the type is a pointer type
   bool IsPointer() const override;
 
+  /// @returns minimum size required for this type, in bytes.
+  ///          0 for non-host shareable types.
+  uint64_t MinBufferBindingSize() const override;
+
   /// @returns the pointee type
   Type* type() const { return subtype_; }
   /// @returns the storage class of the pointer
diff --git a/src/ast/type/struct_type.cc b/src/ast/type/struct_type.cc
index 8f0a017..c52fdb5 100644
--- a/src/ast/type/struct_type.cc
+++ b/src/ast/type/struct_type.cc
@@ -25,6 +25,8 @@
 
 StructType::StructType(StructType&&) = default;
 
+StructType::~StructType() = default;
+
 bool StructType::IsStruct() const {
   return true;
 }
@@ -33,7 +35,21 @@
   return "__struct_" + name_;
 }
 
-StructType::~StructType() = default;
+uint64_t StructType::MinBufferBindingSize() const {
+  if (!struct_->members().size()) {
+    return 0;
+  }
+
+  const auto& last_member = struct_->members().back();
+
+  // If there is no offset, then this is not a host-shareable struct, returning
+  // 0 indicates this to the caller.
+  if (!last_member->has_offset_decoration()) {
+    return 0;
+  }
+
+  return last_member->offset() + last_member->type()->MinBufferBindingSize();
+}
 
 }  // namespace type
 }  // namespace ast
diff --git a/src/ast/type/struct_type.h b/src/ast/type/struct_type.h
index 9621adb..ec5a7a1 100644
--- a/src/ast/type/struct_type.h
+++ b/src/ast/type/struct_type.h
@@ -48,9 +48,13 @@
   /// @returns the struct name
   Struct* impl() const { return struct_.get(); }
 
-  /// @returns the name for th type
+  /// @returns the name for the type
   std::string type_name() const override;
 
+  /// @returns minimum size required for this type, in bytes.
+  ///          0 for non-host shareable types.
+  uint64_t MinBufferBindingSize() const override;
+
  private:
   std::string name_;
   std::unique_ptr<Struct> struct_;
diff --git a/src/ast/type/type.cc b/src/ast/type/type.cc
index 7565d73..ee8fa42 100644
--- a/src/ast/type/type.cc
+++ b/src/ast/type/type.cc
@@ -109,6 +109,10 @@
   return false;
 }
 
+uint64_t Type::MinBufferBindingSize() const {
+  return 0;
+}
+
 bool Type::is_scalar() {
   return is_float_scalar() || is_integer_scalar() || IsBool();
 }
diff --git a/src/ast/type/type.h b/src/ast/type/type.h
index 5d0dbe7..aabde8d 100644
--- a/src/ast/type/type.h
+++ b/src/ast/type/type.h
@@ -72,6 +72,10 @@
   /// @returns the name for this type. The |type_name| is unique over all types.
   virtual std::string type_name() const = 0;
 
+  /// @returns minimum size required for this type, in bytes.
+  ///          0 for non-host shareable types.
+  virtual uint64_t MinBufferBindingSize() const;
+
   /// @returns the pointee type if this is a pointer, |this| otherwise
   Type* UnwrapPtrIfNeeded();
 
diff --git a/src/ast/type/u32_type.cc b/src/ast/type/u32_type.cc
index 2bb7a3f..ebc65f5 100644
--- a/src/ast/type/u32_type.cc
+++ b/src/ast/type/u32_type.cc
@@ -32,6 +32,10 @@
   return "__u32";
 }
 
+uint64_t U32Type::MinBufferBindingSize() const {
+  return 4;
+}
+
 }  // namespace type
 }  // namespace ast
 }  // namespace tint
diff --git a/src/ast/type/u32_type.h b/src/ast/type/u32_type.h
index 21dd9ea..27b40db 100644
--- a/src/ast/type/u32_type.h
+++ b/src/ast/type/u32_type.h
@@ -37,6 +37,10 @@
 
   /// @returns the name for th type
   std::string type_name() const override;
+
+  /// @returns minimum size required for this type, in bytes.
+  ///          0 for non-host shareable types.
+  uint64_t MinBufferBindingSize() const override;
 };
 
 }  // namespace type
diff --git a/src/ast/type/vector_type.cc b/src/ast/type/vector_type.cc
index 2335b94..645ac2f 100644
--- a/src/ast/type/vector_type.cc
+++ b/src/ast/type/vector_type.cc
@@ -38,6 +38,10 @@
   return "__vec_" + std::to_string(size_) + subtype_->type_name();
 }
 
+uint64_t VectorType::MinBufferBindingSize() const {
+  return size_ * subtype_->MinBufferBindingSize();
+}
+
 }  // namespace type
 }  // namespace ast
 }  // namespace tint
diff --git a/src/ast/type/vector_type.h b/src/ast/type/vector_type.h
index 6ea2c8f..3862274 100644
--- a/src/ast/type/vector_type.h
+++ b/src/ast/type/vector_type.h
@@ -45,6 +45,10 @@
   /// @returns the name for th type
   std::string type_name() const override;
 
+  /// @returns minimum size required for this type, in bytes.
+  ///          0 for non-host shareable types.
+  uint64_t MinBufferBindingSize() const override;
+
  private:
   Type* subtype_ = nullptr;
   uint32_t size_ = 2;
diff --git a/src/inspector/inspector.cc b/src/inspector/inspector.cc
index b5d68f7..239dcbb 100644
--- a/src/inspector/inspector.cc
+++ b/src/inspector/inspector.cc
@@ -25,6 +25,7 @@
 #include "src/ast/null_literal.h"
 #include "src/ast/scalar_constructor_expression.h"
 #include "src/ast/sint_literal.h"
+#include "src/ast/type/struct_type.h"
 #include "src/ast/uint_literal.h"
 
 namespace tint {
@@ -134,5 +135,43 @@
   return result;
 }
 
+std::vector<ResourceBinding> Inspector::GetUniformBufferResourceBindings(
+    const std::string& entry_point) {
+  auto* func = module_.FindFunctionByName(entry_point);
+  if (!func) {
+    error_ += entry_point + " was not found!";
+    return {};
+  }
+
+  if (!func->IsEntryPoint()) {
+    error_ += entry_point + " is not an entry point!";
+    return {};
+  }
+
+  std::vector<ResourceBinding> result;
+
+  for (auto& ruv : func->referenced_uniform_variables()) {
+    ResourceBinding entry;
+    ast::Variable* var = nullptr;
+    ast::Function::BindingInfo binding_info;
+    std::tie(var, binding_info) = ruv;
+    if (!var->type()->IsStruct()) {
+      continue;
+    }
+
+    if (!var->type()->AsStruct()->IsBlockDecorated()) {
+      continue;
+    }
+
+    entry.bind_group = binding_info.set->value();
+    entry.binding = binding_info.binding->value();
+    entry.min_buffer_binding_size = var->type()->MinBufferBindingSize();
+
+    result.push_back(std::move(entry));
+  }
+
+  return result;
+}
+
 }  // namespace inspector
 }  // namespace tint
diff --git a/src/inspector/inspector.h b/src/inspector/inspector.h
index 9559fac..a0d82f2 100644
--- a/src/inspector/inspector.h
+++ b/src/inspector/inspector.h
@@ -29,6 +29,16 @@
 namespace tint {
 namespace inspector {
 
+/// Container for information about how a resource is bound
+struct ResourceBinding {
+  /// Bind group the binding belongs
+  uint32_t bind_group;
+  /// Identifier to identify this binding within the bind group
+  uint32_t binding;
+  /// Minimum size required for this binding, in bytes.
+  uint64_t min_buffer_binding_size;
+};
+
 /// Extracts information from a module
 class Inspector {
  public:
@@ -48,6 +58,11 @@
   /// @returns map of const_id to initial value
   std::map<uint32_t, Scalar> GetConstantIDs();
 
+  /// @param entry_point name of the entry point to get information about.
+  /// @returns vector of all of the bindings for Uniform buffers.
+  std::vector<ResourceBinding> GetUniformBufferResourceBindings(
+      const std::string& entry_point);
+
  private:
   const ast::Module& module_;
   std::string error_;
diff --git a/src/inspector/inspector_test.cc b/src/inspector/inspector_test.cc
index 9a895cc..d136ad8 100644
--- a/src/inspector/inspector_test.cc
+++ b/src/inspector/inspector_test.cc
@@ -24,17 +24,27 @@
 #include "src/ast/float_literal.h"
 #include "src/ast/function.h"
 #include "src/ast/identifier_expression.h"
+#include "src/ast/member_accessor_expression.h"
 #include "src/ast/null_literal.h"
 #include "src/ast/pipeline_stage.h"
 #include "src/ast/return_statement.h"
 #include "src/ast/scalar_constructor_expression.h"
 #include "src/ast/sint_literal.h"
 #include "src/ast/stage_decoration.h"
+#include "src/ast/struct_decoration.h"
+#include "src/ast/struct_member.h"
+#include "src/ast/struct_member_decoration.h"
+#include "src/ast/struct_member_offset_decoration.h"
+#include "src/ast/type/array_type.h"
 #include "src/ast/type/bool_type.h"
 #include "src/ast/type/f32_type.h"
 #include "src/ast/type/i32_type.h"
+#include "src/ast/type/matrix_type.h"
+#include "src/ast/type/pointer_type.h"
+#include "src/ast/type/struct_type.h"
 #include "src/ast/type/type.h"
 #include "src/ast/type/u32_type.h"
+#include "src/ast/type/vector_type.h"
 #include "src/ast/type/void_type.h"
 #include "src/ast/uint_literal.h"
 #include "src/ast/variable_decl_statement.h"
@@ -58,7 +68,7 @@
   /// Generates an empty function
   /// @param name name of the function created
   /// @returns a function object
-  std::unique_ptr<ast::Function> GenerateEmptyBodyFunction(std::string name) {
+  std::unique_ptr<ast::Function> MakeEmptyBodyFunction(std::string name) {
     auto body = std::make_unique<ast::BlockStatement>();
     body->append(std::make_unique<ast::ReturnStatement>());
     std::unique_ptr<ast::Function> func =
@@ -71,9 +81,8 @@
   /// @param caller name of the function created
   /// @param callee name of the function to be called
   /// @returns a function object
-  std::unique_ptr<ast::Function> GenerateCallerBodyFunction(
-      std::string caller,
-      std::string callee) {
+  std::unique_ptr<ast::Function> MakeCallerBodyFunction(std::string caller,
+                                                        std::string callee) {
     auto body = std::make_unique<ast::BlockStatement>();
     auto ident_expr = std::make_unique<ast::IdentifierExpression>(callee);
     auto call_expr = std::make_unique<ast::CallExpression>(
@@ -89,7 +98,7 @@
   /// Add In/Out variables to the global variables
   /// @param inout_vars tuples of {in, out} that will be added as entries to the
   ///                   global variables
-  void CreateInOutVariables(
+  void AddInOutVariables(
       std::vector<std::tuple<std::string, std::string>> inout_vars) {
     for (auto inout : inout_vars) {
       std::string in, out;
@@ -108,7 +117,7 @@
   /// @param inout_vars tuples of {in, out} that will be converted into out = in
   ///                   calls in the function body
   /// @returns a function object
-  std::unique_ptr<ast::Function> GenerateInOutVariableBodyFunction(
+  std::unique_ptr<ast::Function> MakeInOutVariableBodyFunction(
       std::string name,
       std::vector<std::tuple<std::string, std::string>> inout_vars) {
     auto body = std::make_unique<ast::BlockStatement>();
@@ -133,7 +142,7 @@
   /// @param inout_vars tuples of {in, out} that will be converted into out = in
   ///                   calls in the function body
   /// @returns a function object
-  std::unique_ptr<ast::Function> GenerateInOutVariableCallerBodyFunction(
+  std::unique_ptr<ast::Function> MakeInOutVariableCallerBodyFunction(
       std::string caller,
       std::string callee,
       std::vector<std::tuple<std::string, std::string>> inout_vars) {
@@ -163,10 +172,10 @@
   /// @param val value to initialize the variable with, if NULL no initializer
   ///            will be added.
   template <class T>
-  void CreateConstantID(std::string name,
-                        uint32_t id,
-                        ast::type::Type* type,
-                        T* val) {
+  void AddConstantID(std::string name,
+                     uint32_t id,
+                     ast::type::Type* type,
+                     T* val) {
     auto dvar = std::make_unique<ast::DecoratedVariable>(
         std::make_unique<ast::Variable>(name, ast::StorageClass::kNone, type));
     dvar->set_is_const(true);
@@ -180,35 +189,53 @@
     mod()->AddGlobalVariable(std::move(dvar));
   }
 
+  /// Generates an ast::Literal for the given value
+  /// @tparam T C++ type of the literal, must agree with type
+  /// @returns a Literal of the expected type and value
   template <class T>
   std::unique_ptr<ast::Literal> MakeLiteral(ast::type::Type*, T*) {
     return nullptr;
   }
 
+  /// @param type AST type of the literal, must resolve to BoolLiteral
+  /// @param val scalar value for the literal to contain
+  /// @returns a Literal of the expected type and value
   template <>
   std::unique_ptr<ast::Literal> MakeLiteral<bool>(ast::type::Type* type,
                                                   bool* val) {
     return std::make_unique<ast::BoolLiteral>(type, *val);
   }
 
+  /// @param type AST type of the literal, must resolve to UIntLiteral
+  /// @param val scalar value for the literal to contain
+  /// @returns a Literal of the expected type and value
   template <>
   std::unique_ptr<ast::Literal> MakeLiteral<uint32_t>(ast::type::Type* type,
                                                       uint32_t* val) {
     return std::make_unique<ast::UintLiteral>(type, *val);
   }
 
+  /// @param type AST type of the literal, must resolve to IntLiteral
+  /// @param val scalar value for the literal to contain
+  /// @returns a Literal of the expected type and value
   template <>
   std::unique_ptr<ast::Literal> MakeLiteral<int32_t>(ast::type::Type* type,
                                                      int32_t* val) {
     return std::make_unique<ast::SintLiteral>(type, *val);
   }
 
+  /// @param type AST type of the literal, must resolve to FloattLiteral
+  /// @param val scalar value for the literal to contain
+  /// @returns a Literal of the expected type and value
   template <>
   std::unique_ptr<ast::Literal> MakeLiteral<float>(ast::type::Type* type,
                                                    float* val) {
     return std::make_unique<ast::FloatLiteral>(type, *val);
   }
 
+  /// @param vec Vector of strings to be searched
+  /// @param str String to be searching for
+  /// @returns true if str is in vec, otherwise false
   bool ContainsString(const std::vector<std::string>& vec,
                       const std::string& str) {
     for (auto& s : vec) {
@@ -219,6 +246,105 @@
     return false;
   }
 
+  /// Builds a string for accessing a member in a generated struct
+  /// @param idx index of member
+  /// @param type type of member
+  /// @returns a string for the member
+  std::string StructMemberName(size_t idx, ast::type::Type* type) {
+    return std::to_string(idx) + type->type_name();
+  }
+
+  /// Generates a struct type appropriate for using in a UBO
+  /// @param name name for the type
+  /// @param members_info a vector of {type, offset} where each entry is the
+  ///                     type and offset of a member of the struct
+  /// @returns a struct type suitable to use for a UBO
+  std::unique_ptr<ast::type::StructType> MakeUBOStructType(
+      const std::string& name,
+      std::vector<std::tuple<ast::type::Type*, uint32_t>> members_info) {
+    ast::StructMemberList members;
+    for (auto& member_info : members_info) {
+      ast::type::Type* type;
+      uint32_t offset;
+      std::tie(type, offset) = member_info;
+
+      ast::StructMemberDecorationList deco;
+      deco.push_back(
+          std::make_unique<ast::StructMemberOffsetDecoration>(offset));
+
+      members.push_back(std::make_unique<ast::StructMember>(
+          StructMemberName(members.size(), type), type, std::move(deco)));
+    }
+    ast::StructDecorationList decos;
+    decos.push_back(ast::StructDecoration::kBlock);
+
+    auto str =
+        std::make_unique<ast::Struct>(std::move(decos), std::move(members));
+
+    return std::make_unique<ast::type::StructType>(name, std::move(str));
+  }
+
+  /// Adds a UBO variable to the module
+  /// @param name the name of the variable
+  /// @param struct_type the type to use
+  /// @param set the binding group/set to use for the UBO
+  /// @param binding the binding number to use for the UBO
+  void AddUBO(const std::string& name,
+              ast::type::StructType* struct_type,
+              uint32_t set,
+              uint32_t binding) {
+    auto var = std::make_unique<ast::DecoratedVariable>(
+        std::make_unique<ast::Variable>(name, ast::StorageClass::kUniform,
+                                        struct_type));
+    ast::VariableDecorationList decorations;
+
+    decorations.push_back(std::make_unique<ast::BindingDecoration>(binding));
+    decorations.push_back(std::make_unique<ast::SetDecoration>(set));
+    var->set_decorations(std::move(decorations));
+
+    mod()->AddGlobalVariable(std::move(var));
+  }
+
+  /// Generates a function that references a specific UBO
+  /// @param func_name name of the function created
+  /// @param ubo_name name of the UBO to be accessed
+  /// @param member_idx index of the member to access
+  /// @returns a function that references all of the UBO members specified
+  std::unique_ptr<ast::Function> MakeUBOReferenceBodyFunction(
+      std::string func_name,
+      std::string ubo_name,
+      std::vector<std::tuple<size_t, ast::type::Type*>> members) {
+    auto body = std::make_unique<ast::BlockStatement>();
+
+    for (auto member : members) {
+      size_t member_idx;
+      ast::type::Type* member_type;
+      std::tie(member_idx, member_type) = member;
+      std::string member_name = StructMemberName(member_idx, member_type);
+      body->append(std::make_unique<ast::VariableDeclStatement>(
+          std::make_unique<ast::Variable>(
+              "local" + member_name, ast::StorageClass::kNone, member_type)));
+    }
+
+    for (auto member : members) {
+      size_t member_idx;
+      ast::type::Type* member_type;
+      std::tie(member_idx, member_type) = member;
+      std::string member_name = StructMemberName(member_idx, member_type);
+      body->append(std::make_unique<ast::AssignmentStatement>(
+          std::make_unique<ast::IdentifierExpression>("local" + member_name),
+          std::make_unique<ast::MemberAccessorExpression>(
+              std::make_unique<ast::IdentifierExpression>(ubo_name),
+              std::make_unique<ast::IdentifierExpression>(member_name))));
+    }
+
+    body->append(std::make_unique<ast::ReturnStatement>());
+    auto func = std::make_unique<ast::Function>(func_name, ast::VariableList(),
+                                                void_type());
+    func->set_body(std::move(body));
+    return func;
+  }
+
   ast::Module* mod() { return &mod_; }
   TypeDeterminer* td() { return td_.get(); }
   Inspector* inspector() { return inspector_.get(); }
@@ -227,6 +353,14 @@
   ast::type::F32Type* f32_type() { return &f32_type_; }
   ast::type::I32Type* i32_type() { return &i32_type_; }
   ast::type::U32Type* u32_type() { return &u32_type_; }
+  ast::type::ArrayType* u32_array_type(uint32_t count) {
+    static std::map<uint32_t, std::unique_ptr<ast::type::ArrayType>> memo;
+    if (memo.find(count) == memo.end()) {
+      memo[count] = std::make_unique<ast::type::ArrayType>(u32_type(), count);
+    }
+
+    return memo[count].get();
+  }
   ast::type::VoidType* void_type() { return &void_type_; }
 
  private:
@@ -245,6 +379,8 @@
 class InspectorTest : public InspectorHelper, public testing::Test {};
 
 class InspectorGetEntryPointTest : public InspectorTest {};
+class InspectorGetConstantIDsTest : public InspectorTest {};
+class InspectorGetUniformBufferResourceBindings : public InspectorTest {};
 
 TEST_F(InspectorGetEntryPointTest, NoFunctions) {
   auto result = inspector()->GetEntryPoints();
@@ -254,7 +390,7 @@
 }
 
 TEST_F(InspectorGetEntryPointTest, NoEntryPoints) {
-  mod()->AddFunction(GenerateEmptyBodyFunction("foo"));
+  mod()->AddFunction(MakeEmptyBodyFunction("foo"));
 
   auto result = inspector()->GetEntryPoints();
   ASSERT_FALSE(inspector()->has_error());
@@ -263,7 +399,7 @@
 }
 
 TEST_F(InspectorGetEntryPointTest, OneEntryPoint) {
-  auto foo = GenerateEmptyBodyFunction("foo");
+  auto foo = MakeEmptyBodyFunction("foo");
   foo->add_decoration(
       std::make_unique<ast::StageDecoration>(ast::PipelineStage::kVertex));
   mod()->AddFunction(std::move(foo));
@@ -277,12 +413,12 @@
 }
 
 TEST_F(InspectorGetEntryPointTest, MultipleEntryPoints) {
-  auto foo = GenerateEmptyBodyFunction("foo");
+  auto foo = MakeEmptyBodyFunction("foo");
   foo->add_decoration(
       std::make_unique<ast::StageDecoration>(ast::PipelineStage::kVertex));
   mod()->AddFunction(std::move(foo));
 
-  auto bar = GenerateEmptyBodyFunction("bar");
+  auto bar = MakeEmptyBodyFunction("bar");
   bar->add_decoration(
       std::make_unique<ast::StageDecoration>(ast::PipelineStage::kCompute));
   mod()->AddFunction(std::move(bar));
@@ -298,15 +434,15 @@
 }
 
 TEST_F(InspectorGetEntryPointTest, MixFunctionsAndEntryPoints) {
-  auto func = GenerateEmptyBodyFunction("func");
+  auto func = MakeEmptyBodyFunction("func");
   mod()->AddFunction(std::move(func));
 
-  auto foo = GenerateCallerBodyFunction("foo", "func");
+  auto foo = MakeCallerBodyFunction("foo", "func");
   foo->add_decoration(
       std::make_unique<ast::StageDecoration>(ast::PipelineStage::kVertex));
   mod()->AddFunction(std::move(foo));
 
-  auto bar = GenerateCallerBodyFunction("bar", "func");
+  auto bar = MakeCallerBodyFunction("bar", "func");
   bar->add_decoration(
       std::make_unique<ast::StageDecoration>(ast::PipelineStage::kFragment));
   mod()->AddFunction(std::move(bar));
@@ -322,7 +458,7 @@
 }
 
 TEST_F(InspectorGetEntryPointTest, DefaultWorkgroupSize) {
-  auto foo = GenerateCallerBodyFunction("foo", "func");
+  auto foo = MakeCallerBodyFunction("foo", "func");
   foo->add_decoration(
       std::make_unique<ast::StageDecoration>(ast::PipelineStage::kVertex));
   mod()->AddFunction(std::move(foo));
@@ -339,7 +475,7 @@
 }
 
 TEST_F(InspectorGetEntryPointTest, NonDefaultWorkgroupSize) {
-  auto foo = GenerateEmptyBodyFunction("foo");
+  auto foo = MakeEmptyBodyFunction("foo");
   foo->add_decoration(
       std::make_unique<ast::StageDecoration>(ast::PipelineStage::kCompute));
   foo->add_decoration(std::make_unique<ast::WorkgroupDecoration>(8u, 2u, 1u));
@@ -357,10 +493,10 @@
 }
 
 TEST_F(InspectorGetEntryPointTest, NoInOutVariables) {
-  auto func = GenerateEmptyBodyFunction("func");
+  auto func = MakeEmptyBodyFunction("func");
   mod()->AddFunction(std::move(func));
 
-  auto foo = GenerateCallerBodyFunction("foo", "func");
+  auto foo = MakeCallerBodyFunction("foo", "func");
   foo->add_decoration(
       std::make_unique<ast::StageDecoration>(ast::PipelineStage::kVertex));
   mod()->AddFunction(std::move(foo));
@@ -374,9 +510,9 @@
 }
 
 TEST_F(InspectorGetEntryPointTest, EntryPointInOutVariables) {
-  CreateInOutVariables({{"in_var", "out_var"}});
+  AddInOutVariables({{"in_var", "out_var"}});
 
-  auto foo = GenerateInOutVariableBodyFunction("foo", {{"in_var", "out_var"}});
+  auto foo = MakeInOutVariableBodyFunction("foo", {{"in_var", "out_var"}});
   foo->add_decoration(
       std::make_unique<ast::StageDecoration>(ast::PipelineStage::kVertex));
   mod()->AddFunction(std::move(foo));
@@ -395,13 +531,12 @@
 }
 
 TEST_F(InspectorGetEntryPointTest, FunctionInOutVariables) {
-  CreateInOutVariables({{"in_var", "out_var"}});
+  AddInOutVariables({{"in_var", "out_var"}});
 
-  auto func =
-      GenerateInOutVariableBodyFunction("func", {{"in_var", "out_var"}});
+  auto func = MakeInOutVariableBodyFunction("func", {{"in_var", "out_var"}});
   mod()->AddFunction(std::move(func));
 
-  auto foo = GenerateCallerBodyFunction("foo", "func");
+  auto foo = MakeCallerBodyFunction("foo", "func");
   foo->add_decoration(
       std::make_unique<ast::StageDecoration>(ast::PipelineStage::kVertex));
   mod()->AddFunction(std::move(foo));
@@ -420,14 +555,13 @@
 }
 
 TEST_F(InspectorGetEntryPointTest, RepeatedInOutVariables) {
-  CreateInOutVariables({{"in_var", "out_var"}});
+  AddInOutVariables({{"in_var", "out_var"}});
 
-  auto func =
-      GenerateInOutVariableBodyFunction("func", {{"in_var", "out_var"}});
+  auto func = MakeInOutVariableBodyFunction("func", {{"in_var", "out_var"}});
   mod()->AddFunction(std::move(func));
 
-  auto foo = GenerateInOutVariableCallerBodyFunction("foo", "func",
-                                                     {{"in_var", "out_var"}});
+  auto foo = MakeInOutVariableCallerBodyFunction("foo", "func",
+                                                 {{"in_var", "out_var"}});
   foo->add_decoration(
       std::make_unique<ast::StageDecoration>(ast::PipelineStage::kVertex));
   mod()->AddFunction(std::move(foo));
@@ -446,9 +580,9 @@
 }
 
 TEST_F(InspectorGetEntryPointTest, EntryPointMultipleInOutVariables) {
-  CreateInOutVariables({{"in_var", "out_var"}, {"in2_var", "out2_var"}});
+  AddInOutVariables({{"in_var", "out_var"}, {"in2_var", "out2_var"}});
 
-  auto foo = GenerateInOutVariableBodyFunction(
+  auto foo = MakeInOutVariableBodyFunction(
       "foo", {{"in_var", "out_var"}, {"in2_var", "out2_var"}});
   foo->add_decoration(
       std::make_unique<ast::StageDecoration>(ast::PipelineStage::kVertex));
@@ -470,13 +604,13 @@
 }
 
 TEST_F(InspectorGetEntryPointTest, FunctionMultipleInOutVariables) {
-  CreateInOutVariables({{"in_var", "out_var"}, {"in2_var", "out2_var"}});
+  AddInOutVariables({{"in_var", "out_var"}, {"in2_var", "out2_var"}});
 
-  auto func = GenerateInOutVariableBodyFunction(
+  auto func = MakeInOutVariableBodyFunction(
       "func", {{"in_var", "out_var"}, {"in2_var", "out2_var"}});
   mod()->AddFunction(std::move(func));
 
-  auto foo = GenerateCallerBodyFunction("foo", "func");
+  auto foo = MakeCallerBodyFunction("foo", "func");
   foo->add_decoration(
       std::make_unique<ast::StageDecoration>(ast::PipelineStage::kVertex));
   mod()->AddFunction(std::move(foo));
@@ -497,14 +631,14 @@
 }
 
 TEST_F(InspectorGetEntryPointTest, MultipleEntryPointsInOutVariables) {
-  CreateInOutVariables({{"in_var", "out_var"}, {"in2_var", "out2_var"}});
+  AddInOutVariables({{"in_var", "out_var"}, {"in2_var", "out2_var"}});
 
-  auto foo = GenerateInOutVariableBodyFunction("foo", {{"in_var", "out2_var"}});
+  auto foo = MakeInOutVariableBodyFunction("foo", {{"in_var", "out2_var"}});
   foo->add_decoration(
       std::make_unique<ast::StageDecoration>(ast::PipelineStage::kVertex));
   mod()->AddFunction(std::move(foo));
 
-  auto bar = GenerateInOutVariableBodyFunction("bar", {{"in2_var", "out_var"}});
+  auto bar = MakeInOutVariableBodyFunction("bar", {{"in2_var", "out_var"}});
   bar->add_decoration(
       std::make_unique<ast::StageDecoration>(ast::PipelineStage::kCompute));
   mod()->AddFunction(std::move(bar));
@@ -530,19 +664,18 @@
 }
 
 TEST_F(InspectorGetEntryPointTest, MultipleEntryPointsSharedInOutVariables) {
-  CreateInOutVariables({{"in_var", "out_var"}, {"in2_var", "out2_var"}});
+  AddInOutVariables({{"in_var", "out_var"}, {"in2_var", "out2_var"}});
 
-  auto func =
-      GenerateInOutVariableBodyFunction("func", {{"in2_var", "out2_var"}});
+  auto func = MakeInOutVariableBodyFunction("func", {{"in2_var", "out2_var"}});
   mod()->AddFunction(std::move(func));
 
-  auto foo = GenerateInOutVariableCallerBodyFunction("foo", "func",
-                                                     {{"in_var", "out_var"}});
+  auto foo = MakeInOutVariableCallerBodyFunction("foo", "func",
+                                                 {{"in_var", "out_var"}});
   foo->add_decoration(
       std::make_unique<ast::StageDecoration>(ast::PipelineStage::kVertex));
   mod()->AddFunction(std::move(foo));
 
-  auto bar = GenerateCallerBodyFunction("bar", "func");
+  auto bar = MakeCallerBodyFunction("bar", "func");
   bar->add_decoration(
       std::make_unique<ast::StageDecoration>(ast::PipelineStage::kCompute));
   mod()->AddFunction(std::move(bar));
@@ -569,12 +702,12 @@
   EXPECT_EQ("out2_var", result[1].output_variables[0]);
 }
 
-TEST_F(InspectorGetEntryPointTest, BoolConstantIDs) {
+TEST_F(InspectorGetConstantIDsTest, Bool) {
   bool val_true = true;
   bool val_false = false;
-  CreateConstantID<bool>("foo", 1, bool_type(), nullptr);
-  CreateConstantID<bool>("bar", 20, bool_type(), &val_true);
-  CreateConstantID<bool>("baz", 300, bool_type(), &val_false);
+  AddConstantID<bool>("foo", 1, bool_type(), nullptr);
+  AddConstantID<bool>("bar", 20, bool_type(), &val_true);
+  AddConstantID<bool>("baz", 300, bool_type(), &val_false);
 
   auto result = inspector()->GetConstantIDs();
   ASSERT_EQ(3u, result.size());
@@ -591,10 +724,10 @@
   EXPECT_FALSE(result[300].AsBool());
 }
 
-TEST_F(InspectorGetEntryPointTest, U32ConstantIDs) {
+TEST_F(InspectorGetConstantIDsTest, U32) {
   uint32_t val = 42;
-  CreateConstantID<uint32_t>("foo", 1, u32_type(), nullptr);
-  CreateConstantID<uint32_t>("bar", 20, u32_type(), &val);
+  AddConstantID<uint32_t>("foo", 1, u32_type(), nullptr);
+  AddConstantID<uint32_t>("bar", 20, u32_type(), &val);
 
   auto result = inspector()->GetConstantIDs();
   ASSERT_EQ(2u, result.size());
@@ -607,12 +740,12 @@
   EXPECT_EQ(42u, result[20].AsU32());
 }
 
-TEST_F(InspectorGetEntryPointTest, I32ConstantIDs) {
+TEST_F(InspectorGetConstantIDsTest, I32) {
   int32_t val_neg = -42;
   int32_t val_pos = 42;
-  CreateConstantID<int32_t>("foo", 1, i32_type(), nullptr);
-  CreateConstantID<int32_t>("bar", 20, i32_type(), &val_neg);
-  CreateConstantID<int32_t>("baz", 300, i32_type(), &val_pos);
+  AddConstantID<int32_t>("foo", 1, i32_type(), nullptr);
+  AddConstantID<int32_t>("bar", 20, i32_type(), &val_neg);
+  AddConstantID<int32_t>("baz", 300, i32_type(), &val_pos);
 
   auto result = inspector()->GetConstantIDs();
   ASSERT_EQ(3u, result.size());
@@ -629,14 +762,14 @@
   EXPECT_EQ(42, result[300].AsI32());
 }
 
-TEST_F(InspectorGetEntryPointTest, FloatConstantIDs) {
+TEST_F(InspectorGetConstantIDsTest, Float) {
   float val_zero = 0.0f;
   float val_neg = -10.0f;
   float val_pos = 15.0f;
-  CreateConstantID<float>("foo", 1, f32_type(), nullptr);
-  CreateConstantID<float>("bar", 20, f32_type(), &val_zero);
-  CreateConstantID<float>("baz", 300, f32_type(), &val_neg);
-  CreateConstantID<float>("x", 4000, f32_type(), &val_pos);
+  AddConstantID<float>("foo", 1, f32_type(), nullptr);
+  AddConstantID<float>("bar", 20, f32_type(), &val_zero);
+  AddConstantID<float>("baz", 300, f32_type(), &val_neg);
+  AddConstantID<float>("x", 4000, f32_type(), &val_pos);
 
   auto result = inspector()->GetConstantIDs();
   ASSERT_EQ(4u, result.size());
@@ -657,6 +790,200 @@
   EXPECT_FLOAT_EQ(15.0, result[4000].AsFloat());
 }
 
+TEST_F(InspectorGetUniformBufferResourceBindings, MissingEntryPoint) {
+  auto result = inspector()->GetUniformBufferResourceBindings("ep_func");
+  ASSERT_TRUE(inspector()->has_error());
+  std::string error = inspector()->error();
+  EXPECT_TRUE(error.find("not found") != std::string::npos);
+}
+
+TEST_F(InspectorGetUniformBufferResourceBindings, NonEntryPointFunc) {
+  auto foo_type = MakeUBOStructType("foo_type", {{i32_type(), 0}});
+  AddUBO("foo_ubo", foo_type.get(), 0, 0);
+
+  auto ubo_func =
+      MakeUBOReferenceBodyFunction("ubo_func", "foo_ubo", {{0, i32_type()}});
+  mod()->AddFunction(std::move(ubo_func));
+
+  auto ep_func = MakeCallerBodyFunction("ep_func", "ubo_func");
+  ep_func->add_decoration(
+      std::make_unique<ast::StageDecoration>(ast::PipelineStage::kVertex));
+  mod()->AddFunction(std::move(ep_func));
+
+  ASSERT_TRUE(td()->Determine()) << td()->error();
+
+  auto result = inspector()->GetUniformBufferResourceBindings("ubo_func");
+  std::string error = inspector()->error();
+  EXPECT_TRUE(error.find("not an entry point") != std::string::npos);
+}
+
+TEST_F(InspectorGetUniformBufferResourceBindings, MissingBlockDeco) {
+  ast::StructMemberList members;
+  ast::StructMemberDecorationList deco;
+  deco.push_back(std::make_unique<ast::StructMemberOffsetDecoration>(0));
+
+  members.push_back(std::make_unique<ast::StructMember>(
+      StructMemberName(members.size(), i32_type()), i32_type(),
+      std::move(deco)));
+
+  ast::StructDecorationList decos;
+
+  auto str =
+      std::make_unique<ast::Struct>(std::move(decos), std::move(members));
+  auto foo_type =
+      std::make_unique<ast::type::StructType>("foo_type", std::move(str));
+
+  AddUBO("foo_ubo", foo_type.get(), 0, 0);
+
+  auto ubo_func =
+      MakeUBOReferenceBodyFunction("ubo_func", "foo_ubo", {{0, i32_type()}});
+  mod()->AddFunction(std::move(ubo_func));
+
+  auto ep_func = MakeCallerBodyFunction("ep_func", "ubo_func");
+  ep_func->add_decoration(
+      std::make_unique<ast::StageDecoration>(ast::PipelineStage::kVertex));
+  mod()->AddFunction(std::move(ep_func));
+
+  ASSERT_TRUE(td()->Determine()) << td()->error();
+
+  auto result = inspector()->GetUniformBufferResourceBindings("ep_func");
+  ASSERT_FALSE(inspector()->has_error());
+  EXPECT_EQ(0u, result.size());
+}
+
+TEST_F(InspectorGetUniformBufferResourceBindings, Simple) {
+  auto foo_type = MakeUBOStructType("foo_type", {{i32_type(), 0}});
+  AddUBO("foo_ubo", foo_type.get(), 0, 0);
+
+  auto ubo_func =
+      MakeUBOReferenceBodyFunction("ubo_func", "foo_ubo", {{0, i32_type()}});
+  mod()->AddFunction(std::move(ubo_func));
+
+  auto ep_func = MakeCallerBodyFunction("ep_func", "ubo_func");
+  ep_func->add_decoration(
+      std::make_unique<ast::StageDecoration>(ast::PipelineStage::kVertex));
+  mod()->AddFunction(std::move(ep_func));
+
+  ASSERT_TRUE(td()->Determine()) << td()->error();
+
+  auto result = inspector()->GetUniformBufferResourceBindings("ep_func");
+  ASSERT_FALSE(inspector()->has_error());
+  ASSERT_EQ(1u, result.size());
+
+  EXPECT_EQ(0u, result[0].bind_group);
+  EXPECT_EQ(0u, result[0].binding);
+  EXPECT_EQ(4u, result[0].min_buffer_binding_size);
+}
+
+TEST_F(InspectorGetUniformBufferResourceBindings, MultipleMembers) {
+  auto foo_type = MakeUBOStructType(
+      "foo_type", {{i32_type(), 0}, {u32_type(), 4}, {f32_type(), 8}});
+  AddUBO("foo_ubo", foo_type.get(), 0, 0);
+
+  auto ubo_func = MakeUBOReferenceBodyFunction(
+      "ubo_func", "foo_ubo",
+      {{0, i32_type()}, {1, u32_type()}, {2, f32_type()}});
+  mod()->AddFunction(std::move(ubo_func));
+
+  auto ep_func = MakeCallerBodyFunction("ep_func", "ubo_func");
+  ep_func->add_decoration(
+      std::make_unique<ast::StageDecoration>(ast::PipelineStage::kVertex));
+  mod()->AddFunction(std::move(ep_func));
+
+  ASSERT_TRUE(td()->Determine()) << td()->error();
+
+  auto result = inspector()->GetUniformBufferResourceBindings("ep_func");
+  ASSERT_FALSE(inspector()->has_error());
+  ASSERT_EQ(1u, result.size());
+
+  EXPECT_EQ(0u, result[0].bind_group);
+  EXPECT_EQ(0u, result[0].binding);
+  EXPECT_EQ(12u, result[0].min_buffer_binding_size);
+}
+
+TEST_F(InspectorGetUniformBufferResourceBindings, MultipleUBOs) {
+  auto ubo_type = MakeUBOStructType(
+      "ubo_type", {{i32_type(), 0}, {u32_type(), 4}, {f32_type(), 8}});
+  AddUBO("ubo_foo", ubo_type.get(), 0, 0);
+  AddUBO("ubo_bar", ubo_type.get(), 0, 1);
+  AddUBO("ubo_baz", ubo_type.get(), 2, 0);
+
+  auto AddReferenceFunc = [this](const std::string& func_name,
+                                 const std::string& var_name) {
+    auto ubo_func = MakeUBOReferenceBodyFunction(
+        func_name, var_name,
+        {{0, i32_type()}, {1, u32_type()}, {2, f32_type()}});
+    mod()->AddFunction(std::move(ubo_func));
+  };
+  AddReferenceFunc("ubo_foo_func", "ubo_foo");
+  AddReferenceFunc("ubo_bar_func", "ubo_bar");
+  AddReferenceFunc("ubo_baz_func", "ubo_baz");
+
+  auto AddFuncCall = [](ast::BlockStatement* body, const std::string& callee) {
+    auto ident_expr = std::make_unique<ast::IdentifierExpression>(callee);
+    auto call_expr = std::make_unique<ast::CallExpression>(
+        std::move(ident_expr), ast::ExpressionList());
+    body->append(std::make_unique<ast::CallStatement>(std::move(call_expr)));
+  };
+  auto body = std::make_unique<ast::BlockStatement>();
+
+  AddFuncCall(body.get(), "ubo_foo_func");
+  AddFuncCall(body.get(), "ubo_bar_func");
+  AddFuncCall(body.get(), "ubo_baz_func");
+
+  body->append(std::make_unique<ast::ReturnStatement>());
+  std::unique_ptr<ast::Function> func = std::make_unique<ast::Function>(
+      "ep_func", ast::VariableList(), void_type());
+  func->set_body(std::move(body));
+
+  func->add_decoration(
+      std::make_unique<ast::StageDecoration>(ast::PipelineStage::kVertex));
+  mod()->AddFunction(std::move(func));
+
+  ASSERT_TRUE(td()->Determine()) << td()->error();
+
+  auto result = inspector()->GetUniformBufferResourceBindings("ep_func");
+  ASSERT_FALSE(inspector()->has_error());
+  ASSERT_EQ(3u, result.size());
+
+  EXPECT_EQ(0u, result[0].bind_group);
+  EXPECT_EQ(0u, result[0].binding);
+  EXPECT_EQ(12u, result[0].min_buffer_binding_size);
+
+  EXPECT_EQ(0u, result[1].bind_group);
+  EXPECT_EQ(1u, result[1].binding);
+  EXPECT_EQ(12u, result[1].min_buffer_binding_size);
+
+  EXPECT_EQ(2u, result[2].bind_group);
+  EXPECT_EQ(0u, result[2].binding);
+  EXPECT_EQ(12u, result[2].min_buffer_binding_size);
+}
+
+TEST_F(InspectorGetUniformBufferResourceBindings, ContainingArray) {
+  auto foo_type =
+      MakeUBOStructType("foo_type", {{i32_type(), 0}, {u32_array_type(4), 4}});
+  AddUBO("foo_ubo", foo_type.get(), 0, 0);
+
+  auto ubo_func =
+      MakeUBOReferenceBodyFunction("ubo_func", "foo_ubo", {{0, i32_type()}});
+  mod()->AddFunction(std::move(ubo_func));
+
+  auto ep_func = MakeCallerBodyFunction("ep_func", "ubo_func");
+  ep_func->add_decoration(
+      std::make_unique<ast::StageDecoration>(ast::PipelineStage::kVertex));
+  mod()->AddFunction(std::move(ep_func));
+
+  ASSERT_TRUE(td()->Determine()) << td()->error();
+
+  auto result = inspector()->GetUniformBufferResourceBindings("ep_func");
+  ASSERT_FALSE(inspector()->has_error());
+  ASSERT_EQ(1u, result.size());
+
+  EXPECT_EQ(0u, result[0].bind_group);
+  EXPECT_EQ(0u, result[0].binding);
+  EXPECT_EQ(20u, result[0].min_buffer_binding_size);
+}
+
 }  // namespace
 }  // namespace inspector
 }  // namespace tint