Resolver: Validate resource binding decorations

Validate that:
* That resource variables have resource bindings
* Only resource variables have resource bindings
* That a [[binding]] decoration is paired with a [[group]]
* That binding points are not reused in the same entry point

Fixed: tint:235
Fixed: tint:645
Bug: tint:645
Change-Id: I2542934b4c6a2b4bbde48242932c04c796033a90
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/50500
Commit-Queue: Ben Clayton <bclayton@google.com>
Reviewed-by: David Neto <dneto@google.com>
diff --git a/src/BUILD.gn b/src/BUILD.gn
index af5eb73..5d0ae2a 100644
--- a/src/BUILD.gn
+++ b/src/BUILD.gn
@@ -448,6 +448,7 @@
     "sem/access_control_type.cc",
     "sem/access_control_type.h",
     "sem/array.h",
+    "sem/binding_point.h",
     "sem/bool_type.cc",
     "sem/bool_type.h",
     "sem/call.h",
@@ -496,7 +497,6 @@
     "symbol_table.cc",
     "symbol_table.h",
     "traits.h",
-    "transform/binding_point.h",
     "transform/binding_remapper.cc",
     "transform/binding_remapper.h",
     "transform/bound_array_accessors.cc",
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 2a90384..dc55ff0 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -232,6 +232,7 @@
   scope_stack.h
   sem/array.cc
   sem/array.h
+  sem/binding_point.h
   sem/call_target.cc
   sem/call_target.h
   sem/call.cc
@@ -258,7 +259,6 @@
   symbol.h
   traits.h
   typepair.h
-  transform/binding_point.h
   transform/binding_remapper.cc
   transform/binding_remapper.h
   transform/bound_array_accessors.cc
diff --git a/src/ast/module_clone_test.cc b/src/ast/module_clone_test.cc
index cf76d61..a1f4920 100644
--- a/src/ast/module_clone_test.cc
+++ b/src/ast/module_clone_test.cc
@@ -41,16 +41,16 @@
 
 var<private> g0 : u32 = 20u;
 var<private> g1 : f32 = 123.0;
-var g2 : texture_2d<f32>;
-var g3 : [[access(read)]] texture_storage_2d<r32uint>;
-var g4 : [[access(write)]] texture_storage_2d<rg32float>;
-var g5 : [[access(read)]] texture_storage_2d<r32uint>;
-var g6 : [[access(write)]] texture_storage_2d<rg32float>;
+[[group(0), binding(0)]] var g2 : texture_2d<f32>;
+[[group(1), binding(0)]] var g3 : [[access(read)]] texture_storage_2d<r32uint>;
+[[group(2), binding(0)]] var g4 : [[access(write)]] texture_storage_2d<rg32float>;
+[[group(3), binding(0)]] var g5 : [[access(read)]] texture_storage_2d<r32uint>;
+[[group(4), binding(0)]] var g6 : [[access(write)]] texture_storage_2d<rg32float>;
 
 var<private> g7 : vec3<f32>;
-[[group(10), binding(20)]] var<storage> g8 : [[access(write)]] S;
-[[group(10), binding(20)]] var<storage> g9 : [[access(read)]] S;
-[[group(10), binding(20)]] var<storage> g10 : [[access(read_write)]] S;
+[[group(0), binding(1)]] var<storage> g8 : [[access(write)]] S;
+[[group(1), binding(1)]] var<storage> g9 : [[access(read)]] S;
+[[group(2), binding(1)]] var<storage> g10 : [[access(read_write)]] S;
 
 fn f0(p0 : bool) -> f32 {
   if (p0) {
diff --git a/src/ast/storage_class.cc b/src/ast/storage_class.cc
index 62e29a4..2ec1ca2 100644
--- a/src/ast/storage_class.cc
+++ b/src/ast/storage_class.cc
@@ -17,53 +17,35 @@
 namespace tint {
 namespace ast {
 
-std::ostream& operator<<(std::ostream& out, StorageClass sc) {
+const char* str(StorageClass sc) {
   switch (sc) {
-    case StorageClass::kInvalid: {
-      out << "invalid";
-      break;
-    }
-    case StorageClass::kNone: {
-      out << "none";
-      break;
-    }
-    case StorageClass::kInput: {
-      out << "in";
-      break;
-    }
-    case StorageClass::kOutput: {
-      out << "out";
-      break;
-    }
-    case StorageClass::kUniform: {
-      out << "uniform";
-      break;
-    }
-    case StorageClass::kWorkgroup: {
-      out << "workgroup";
-      break;
-    }
-    case StorageClass::kUniformConstant: {
-      out << "uniform_constant";
-      break;
-    }
-    case StorageClass::kStorage: {
-      out << "storage";
-      break;
-    }
-    case StorageClass::kImage: {
-      out << "image";
-      break;
-    }
-    case StorageClass::kPrivate: {
-      out << "private";
-      break;
-    }
-    case StorageClass::kFunction: {
-      out << "function";
-      break;
-    }
+    case StorageClass::kInvalid:
+      return "invalid";
+    case StorageClass::kNone:
+      return "none";
+    case StorageClass::kInput:
+      return "in";
+    case StorageClass::kOutput:
+      return "out";
+    case StorageClass::kUniform:
+      return "uniform";
+    case StorageClass::kWorkgroup:
+      return "workgroup";
+    case StorageClass::kUniformConstant:
+      return "uniform_constant";
+    case StorageClass::kStorage:
+      return "storage";
+    case StorageClass::kImage:
+      return "image";
+    case StorageClass::kPrivate:
+      return "private";
+    case StorageClass::kFunction:
+      return "function";
   }
+  return "<unknown>";
+}
+std::ostream& operator<<(std::ostream& out, StorageClass sc) {
+  out << str(sc);
   return out;
 }
 
diff --git a/src/ast/storage_class.h b/src/ast/storage_class.h
index 8a5cace..4f263af 100644
--- a/src/ast/storage_class.h
+++ b/src/ast/storage_class.h
@@ -42,6 +42,10 @@
   return sc == ast::StorageClass::kUniform || sc == ast::StorageClass::kStorage;
 }
 
+/// @param sc the StorageClass
+/// @return the name of the given storage class
+const char* str(StorageClass sc);
+
 /// @param out the std::ostream to write to
 /// @param sc the StorageClass
 /// @return the std::ostream so calls can be chained
diff --git a/src/inspector/inspector_test.cc b/src/inspector/inspector_test.cc
index e5589c8..23e004e 100644
--- a/src/inspector/inspector_test.cc
+++ b/src/inspector/inspector_test.cc
@@ -381,7 +381,7 @@
   }
 
   void AddGlobalVariable(const std::string& name, ast::Type* type) {
-    Global(name, type, ast::StorageClass::kUniformConstant);
+    Global(name, type, ast::StorageClass::kPrivate);
   }
 
   /// Adds a depth texture variable to the program
diff --git a/src/resolver/assignment_validation_test.cc b/src/resolver/assignment_validation_test.cc
index f469a06..a1fc4ec 100644
--- a/src/resolver/assignment_validation_test.cc
+++ b/src/resolver/assignment_validation_test.cc
@@ -244,8 +244,16 @@
     return ty.access(ast::AccessControl::kReadOnly, tex_type);
   };
 
-  auto* var_a = Global("a", make_type(), ast::StorageClass::kNone);
-  auto* var_b = Global("b", make_type(), ast::StorageClass::kNone);
+  auto* var_a = Global("a", make_type(), ast::StorageClass::kNone, nullptr,
+                       {
+                           create<ast::BindingDecoration>(0),
+                           create<ast::GroupDecoration>(0),
+                       });
+  auto* var_b = Global("b", make_type(), ast::StorageClass::kNone, nullptr,
+                       {
+                           create<ast::BindingDecoration>(1),
+                           create<ast::GroupDecoration>(0),
+                       });
 
   WrapInFunction(Assign(Source{{12, 34}}, var_a, var_b));
 
diff --git a/src/resolver/decoration_validation_test.cc b/src/resolver/decoration_validation_test.cc
index 2ec17c5..f894498 100644
--- a/src/resolver/decoration_validation_test.cc
+++ b/src/resolver/decoration_validation_test.cc
@@ -43,46 +43,63 @@
   kStride,
   kStructBlock,
   kWorkgroup,
+
+  kBindingAndGroup,
 };
+
+bool IsBindingDecoration(DecorationKind kind) {
+  switch (kind) {
+    case DecorationKind::kBinding:
+    case DecorationKind::kGroup:
+    case DecorationKind::kBindingAndGroup:
+      return true;
+    default:
+      return false;
+  }
+}
+
 struct TestParams {
   DecorationKind kind;
   bool should_pass;
 };
 struct TestWithParams : ResolverTestWithParam<TestParams> {};
 
-static ast::Decoration* createDecoration(const Source& source,
-                                         ProgramBuilder& builder,
-                                         DecorationKind kind) {
+static ast::DecorationList createDecorations(const Source& source,
+                                             ProgramBuilder& builder,
+                                             DecorationKind kind) {
   switch (kind) {
     case DecorationKind::kAccess:
-      return builder.create<ast::AccessDecoration>(
-          source, ast::AccessControl::kReadOnly);
+      return {builder.create<ast::AccessDecoration>(
+          source, ast::AccessControl::kReadOnly)};
     case DecorationKind::kAlign:
-      return builder.create<ast::StructMemberAlignDecoration>(source, 4u);
+      return {builder.create<ast::StructMemberAlignDecoration>(source, 4u)};
     case DecorationKind::kBinding:
-      return builder.create<ast::BindingDecoration>(source, 1);
+      return {builder.create<ast::BindingDecoration>(source, 1u)};
     case DecorationKind::kBuiltin:
-      return builder.Builtin(source, ast::Builtin::kPosition);
+      return {builder.Builtin(source, ast::Builtin::kPosition)};
     case DecorationKind::kGroup:
-      return builder.create<ast::GroupDecoration>(source, 1u);
+      return {builder.create<ast::GroupDecoration>(source, 1u)};
     case DecorationKind::kLocation:
-      return builder.Location(source, 1);
+      return {builder.Location(source, 1)};
     case DecorationKind::kOverride:
-      return builder.create<ast::OverrideDecoration>(source, 0u);
+      return {builder.create<ast::OverrideDecoration>(source, 0u)};
     case DecorationKind::kOffset:
-      return builder.create<ast::StructMemberOffsetDecoration>(source, 4u);
+      return {builder.create<ast::StructMemberOffsetDecoration>(source, 4u)};
     case DecorationKind::kSize:
-      return builder.create<ast::StructMemberSizeDecoration>(source, 4u);
+      return {builder.create<ast::StructMemberSizeDecoration>(source, 4u)};
     case DecorationKind::kStage:
-      return builder.Stage(source, ast::PipelineStage::kCompute);
+      return {builder.Stage(source, ast::PipelineStage::kCompute)};
     case DecorationKind::kStride:
-      return builder.create<ast::StrideDecoration>(source, 4u);
+      return {builder.create<ast::StrideDecoration>(source, 4u)};
     case DecorationKind::kStructBlock:
-      return builder.create<ast::StructBlockDecoration>(source);
+      return {builder.create<ast::StructBlockDecoration>(source)};
     case DecorationKind::kWorkgroup:
-      return builder.create<ast::WorkgroupDecoration>(source, 1u, 1u, 1u);
+      return {builder.create<ast::WorkgroupDecoration>(source, 1u, 1u, 1u)};
+    case DecorationKind::kBindingAndGroup:
+      return {builder.create<ast::BindingDecoration>(source, 1u),
+              builder.create<ast::GroupDecoration>(source, 1u)};
   }
-  return nullptr;
+  return {};
 }
 
 using FunctionReturnTypeDecorationTest = TestWithParams;
@@ -91,7 +108,7 @@
 
   Func("main", ast::VariableList{}, ty.f32(), ast::StatementList{Return(1.f)},
        ast::DecorationList{Stage(ast::PipelineStage::kCompute)},
-       ast::DecorationList{createDecoration({}, *this, params.kind)});
+       createDecorations({}, *this, params.kind));
 
   if (params.should_pass) {
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -116,17 +133,15 @@
                     TestParams{DecorationKind::kStage, false},
                     TestParams{DecorationKind::kStride, false},
                     TestParams{DecorationKind::kStructBlock, false},
-                    TestParams{DecorationKind::kWorkgroup, false}));
+                    TestParams{DecorationKind::kWorkgroup, false},
+                    TestParams{DecorationKind::kBindingAndGroup, false}));
 
 using ArrayDecorationTest = TestWithParams;
 TEST_P(ArrayDecorationTest, IsValid) {
   auto& params = GetParam();
 
-  auto* arr =
-      ty.array(ty.f32(), 0,
-               {
-                   createDecoration(Source{{12, 34}}, *this, params.kind),
-               });
+  auto* arr = ty.array(ty.f32(), 0,
+                       createDecorations(Source{{12, 34}}, *this, params.kind));
   Structure("mystruct",
             {
                 Member("a", arr),
@@ -158,14 +173,15 @@
                     TestParams{DecorationKind::kStage, false},
                     TestParams{DecorationKind::kStride, true},
                     TestParams{DecorationKind::kStructBlock, false},
-                    TestParams{DecorationKind::kWorkgroup, false}));
+                    TestParams{DecorationKind::kWorkgroup, false},
+                    TestParams{DecorationKind::kBindingAndGroup, false}));
 
 using StructDecorationTest = TestWithParams;
 TEST_P(StructDecorationTest, IsValid) {
   auto& params = GetParam();
 
   Structure("mystruct", {},
-            {createDecoration(Source{{12, 34}}, *this, params.kind)});
+            createDecorations(Source{{12, 34}}, *this, params.kind));
 
   WrapInFunction();
 
@@ -192,16 +208,15 @@
                     TestParams{DecorationKind::kStage, false},
                     TestParams{DecorationKind::kStride, false},
                     TestParams{DecorationKind::kStructBlock, true},
-                    TestParams{DecorationKind::kWorkgroup, false}));
+                    TestParams{DecorationKind::kWorkgroup, false},
+                    TestParams{DecorationKind::kBindingAndGroup, false}));
 
 using StructMemberDecorationTest = TestWithParams;
 TEST_P(StructMemberDecorationTest, IsValid) {
   auto& params = GetParam();
 
-  ast::StructMemberList members{
-      Member("a", ty.i32(),
-             ast::DecorationList{
-                 createDecoration(Source{{12, 34}}, *this, params.kind)})};
+  ast::StructMemberList members{Member(
+      "a", ty.i32(), createDecorations(Source{{12, 34}}, *this, params.kind))};
 
   Structure("mystruct", members);
 
@@ -230,15 +245,21 @@
                     TestParams{DecorationKind::kStage, false},
                     TestParams{DecorationKind::kStride, false},
                     TestParams{DecorationKind::kStructBlock, false},
-                    TestParams{DecorationKind::kWorkgroup, false}));
+                    TestParams{DecorationKind::kWorkgroup, false},
+                    TestParams{DecorationKind::kBindingAndGroup, false}));
 
 using VariableDecorationTest = TestWithParams;
 TEST_P(VariableDecorationTest, IsValid) {
   auto& params = GetParam();
 
-  Global("a", ty.f32(), ast::StorageClass::kInput, nullptr,
-         ast::DecorationList{
-             createDecoration(Source{{12, 34}}, *this, params.kind)});
+  if (IsBindingDecoration(params.kind)) {
+    Global("a", ty.sampler(ast::SamplerKind::kSampler),
+           ast::StorageClass::kNone, nullptr,
+           createDecorations(Source{{12, 34}}, *this, params.kind));
+  } else {
+    Global("a", ty.f32(), ast::StorageClass::kInput, nullptr,
+           createDecorations(Source{{12, 34}}, *this, params.kind));
+  }
 
   WrapInFunction();
 
@@ -246,8 +267,10 @@
     EXPECT_TRUE(r()->Resolve()) << r()->error();
   } else {
     EXPECT_FALSE(r()->Resolve()) << r()->error();
-    EXPECT_EQ(r()->error(),
-              "12:34 error: decoration is not valid for variables");
+    if (!IsBindingDecoration(params.kind)) {
+      EXPECT_EQ(r()->error(),
+                "12:34 error: decoration is not valid for variables");
+    }
   }
 }
 INSTANTIATE_TEST_SUITE_P(
@@ -255,9 +278,9 @@
     VariableDecorationTest,
     testing::Values(TestParams{DecorationKind::kAccess, false},
                     TestParams{DecorationKind::kAlign, false},
-                    TestParams{DecorationKind::kBinding, true},
+                    TestParams{DecorationKind::kBinding, false},
                     TestParams{DecorationKind::kBuiltin, true},
-                    TestParams{DecorationKind::kGroup, true},
+                    TestParams{DecorationKind::kGroup, false},
                     TestParams{DecorationKind::kLocation, true},
                     TestParams{DecorationKind::kOverride, false},
                     TestParams{DecorationKind::kOffset, false},
@@ -265,15 +288,15 @@
                     TestParams{DecorationKind::kStage, false},
                     TestParams{DecorationKind::kStride, false},
                     TestParams{DecorationKind::kStructBlock, false},
-                    TestParams{DecorationKind::kWorkgroup, false}));
+                    TestParams{DecorationKind::kWorkgroup, false},
+                    TestParams{DecorationKind::kBindingAndGroup, true}));
 
 using ConstantDecorationTest = TestWithParams;
 TEST_P(ConstantDecorationTest, IsValid) {
   auto& params = GetParam();
 
-  GlobalConst("a", ty.f32(), nullptr,
-              ast::DecorationList{
-                  createDecoration(Source{{12, 34}}, *this, params.kind)});
+  GlobalConst("a", ty.f32(), Expr(1.23f),
+              createDecorations(Source{{12, 34}}, *this, params.kind));
 
   WrapInFunction();
 
@@ -300,16 +323,17 @@
                     TestParams{DecorationKind::kStage, false},
                     TestParams{DecorationKind::kStride, false},
                     TestParams{DecorationKind::kStructBlock, false},
-                    TestParams{DecorationKind::kWorkgroup, false}));
+                    TestParams{DecorationKind::kWorkgroup, false},
+                    TestParams{DecorationKind::kBindingAndGroup, false}));
 
 using FunctionDecorationTest = TestWithParams;
 TEST_P(FunctionDecorationTest, IsValid) {
   auto& params = GetParam();
 
-  Func("foo", ast::VariableList{}, ty.void_(), ast::StatementList{},
-       ast::DecorationList{
-           Stage(ast::PipelineStage::kCompute),
-           createDecoration(Source{{12, 34}}, *this, params.kind)});
+  ast::DecorationList decos =
+      createDecorations(Source{{12, 34}}, *this, params.kind);
+  decos.emplace_back(Stage(ast::PipelineStage::kCompute));
+  Func("foo", ast::VariableList{}, ty.void_(), ast::StatementList{}, decos);
 
   if (params.should_pass) {
     EXPECT_TRUE(r()->Resolve()) << r()->error();
@@ -334,7 +358,8 @@
                     // Skip kStage as we always apply it in this test
                     TestParams{DecorationKind::kStride, false},
                     TestParams{DecorationKind::kStructBlock, false},
-                    TestParams{DecorationKind::kWorkgroup, true}));
+                    TestParams{DecorationKind::kWorkgroup, true},
+                    TestParams{DecorationKind::kBindingAndGroup, false}));
 
 }  // namespace
 }  // namespace DecorationTests
@@ -480,5 +505,158 @@
 }  // namespace
 }  // namespace StructBlockTests
 
+namespace ResourceTests {
+namespace {
+
+using ResourceDecorationTest = ResolverTest;
+TEST_F(ResourceDecorationTest, UniformBufferMissingBinding) {
+  auto* s = Structure("S", {Member("x", ty.i32())},
+                      {create<ast::StructBlockDecoration>()});
+  Global(Source{{12, 34}}, "G", s, ast::StorageClass::kUniform);
+
+  EXPECT_FALSE(r()->Resolve());
+  EXPECT_EQ(r()->error(),
+            "12:34 error: resource variables require [[group]] and [[binding]] "
+            "decorations");
+}
+
+TEST_F(ResourceDecorationTest, StorageBufferMissingBinding) {
+  auto* s = Structure("S", {Member("x", ty.i32())},
+                      {create<ast::StructBlockDecoration>()});
+  auto ac = ty.access(ast::AccessControl::kReadOnly, s);
+  Global(Source{{12, 34}}, "G", ac, ast::StorageClass::kStorage);
+
+  EXPECT_FALSE(r()->Resolve());
+  EXPECT_EQ(r()->error(),
+            "12:34 error: resource variables require [[group]] and [[binding]] "
+            "decorations");
+}
+
+TEST_F(ResourceDecorationTest, TextureMissingBinding) {
+  Global(Source{{12, 34}}, "G", ty.depth_texture(ast::TextureDimension::k2d),
+         ast::StorageClass::kNone);
+
+  EXPECT_FALSE(r()->Resolve());
+  EXPECT_EQ(r()->error(),
+            "12:34 error: resource variables require [[group]] and [[binding]] "
+            "decorations");
+}
+
+TEST_F(ResourceDecorationTest, SamplerMissingBinding) {
+  Global(Source{{12, 34}}, "G", ty.sampler(ast::SamplerKind::kSampler),
+         ast::StorageClass::kNone);
+
+  EXPECT_FALSE(r()->Resolve());
+  EXPECT_EQ(r()->error(),
+            "12:34 error: resource variables require [[group]] and [[binding]] "
+            "decorations");
+}
+
+TEST_F(ResourceDecorationTest, BindingPairMissingBinding) {
+  Global(Source{{12, 34}}, "G", ty.sampler(ast::SamplerKind::kSampler),
+         ast::StorageClass::kNone, nullptr,
+         {
+             create<ast::GroupDecoration>(1),
+         });
+
+  EXPECT_FALSE(r()->Resolve());
+  EXPECT_EQ(r()->error(),
+            "12:34 error: resource variables require [[group]] and [[binding]] "
+            "decorations");
+}
+
+TEST_F(ResourceDecorationTest, BindingPairMissingGroup) {
+  Global(Source{{12, 34}}, "G", ty.sampler(ast::SamplerKind::kSampler),
+         ast::StorageClass::kNone, nullptr,
+         {
+             create<ast::BindingDecoration>(1),
+         });
+
+  EXPECT_FALSE(r()->Resolve());
+  EXPECT_EQ(r()->error(),
+            "12:34 error: resource variables require [[group]] and [[binding]] "
+            "decorations");
+}
+
+TEST_F(ResourceDecorationTest, BindingPointUsedTwiceByEntryPoint) {
+  Global(Source{{12, 34}}, "A",
+         ty.sampled_texture(ast::TextureDimension::k2d, ty.f32()),
+         ast::StorageClass::kNone, nullptr,
+         {
+             create<ast::BindingDecoration>(1),
+             create<ast::GroupDecoration>(2),
+         });
+  Global(Source{{56, 78}}, "B",
+         ty.sampled_texture(ast::TextureDimension::k2d, ty.f32()),
+         ast::StorageClass::kNone, nullptr,
+         {
+             create<ast::BindingDecoration>(1),
+             create<ast::GroupDecoration>(2),
+         });
+
+  Func("F", {}, ty.void_(),
+       {
+           Decl(Var("a", ty.vec4<f32>(), ast::StorageClass::kFunction,
+                    Call("textureLoad", "A", vec2<i32>(1, 2), 0))),
+           Decl(Var("b", ty.vec4<f32>(), ast::StorageClass::kFunction,
+                    Call("textureLoad", "B", vec2<i32>(1, 2), 0))),
+       },
+       {Stage(ast::PipelineStage::kFragment)});
+
+  EXPECT_FALSE(r()->Resolve());
+  EXPECT_EQ(
+      r()->error(),
+      R"(56:78 error: entry point 'F' references multiple variables that use the same resource binding [[group(2), binding(1)]]
+12:34 note: first resource binding usage declared here)");
+}
+
+TEST_F(ResourceDecorationTest, BindingPointUsedTwiceByDifferentEntryPoints) {
+  Global(Source{{12, 34}}, "A",
+         ty.sampled_texture(ast::TextureDimension::k2d, ty.f32()),
+         ast::StorageClass::kNone, nullptr,
+         {
+             create<ast::BindingDecoration>(1),
+             create<ast::GroupDecoration>(2),
+         });
+  Global(Source{{56, 78}}, "B",
+         ty.sampled_texture(ast::TextureDimension::k2d, ty.f32()),
+         ast::StorageClass::kNone, nullptr,
+         {
+             create<ast::BindingDecoration>(1),
+             create<ast::GroupDecoration>(2),
+         });
+
+  Func("F_A", {}, ty.void_(),
+       {
+           Decl(Var("a", ty.vec4<f32>(), ast::StorageClass::kFunction,
+                    Call("textureLoad", "A", vec2<i32>(1, 2), 0))),
+       },
+       {Stage(ast::PipelineStage::kFragment)});
+  Func("F_B", {}, ty.void_(),
+       {
+           Decl(Var("b", ty.vec4<f32>(), ast::StorageClass::kFunction,
+                    Call("textureLoad", "B", vec2<i32>(1, 2), 0))),
+       },
+       {Stage(ast::PipelineStage::kFragment)});
+
+  EXPECT_TRUE(r()->Resolve()) << r()->error();
+}
+
+TEST_F(ResourceDecorationTest, BindingPointOnNonResource) {
+  Global(Source{{12, 34}}, "G", ty.f32(), ast::StorageClass::kPrivate, nullptr,
+         {
+             create<ast::BindingDecoration>(1),
+             create<ast::GroupDecoration>(2),
+         });
+
+  EXPECT_FALSE(r()->Resolve());
+  EXPECT_EQ(r()->error(),
+            "12:34 error: non-resource variables must not have [[group]] or "
+            "[[binding]] decorations");
+}
+
+}  // namespace
+}  // namespace ResourceTests
+
 }  // namespace resolver
 }  // namespace tint
diff --git a/src/resolver/host_shareable_validation_test.cc b/src/resolver/host_shareable_validation_test.cc
index 25bcfa5..1f5fb08 100644
--- a/src/resolver/host_shareable_validation_test.cc
+++ b/src/resolver/host_shareable_validation_test.cc
@@ -30,7 +30,11 @@
   auto* s = Structure("S", {Member(Source{{12, 34}}, "x", ty.bool_())},
                       {create<ast::StructBlockDecoration>()});
   auto a = ty.access(ast::AccessControl::kReadOnly, s);
-  Global(Source{{56, 78}}, "g", a, ast::StorageClass::kStorage);
+  Global(Source{{56, 78}}, "g", a, ast::StorageClass::kStorage, nullptr,
+         {
+             create<ast::BindingDecoration>(0),
+             create<ast::GroupDecoration>(0),
+         });
 
   ASSERT_FALSE(r()->Resolve());
 
@@ -45,7 +49,11 @@
   auto* s = Structure("S", {Member(Source{{12, 34}}, "x", ty.vec3<bool>())},
                       {create<ast::StructBlockDecoration>()});
   auto a = ty.access(ast::AccessControl::kReadOnly, s);
-  Global(Source{{56, 78}}, "g", a, ast::StorageClass::kStorage);
+  Global(Source{{56, 78}}, "g", a, ast::StorageClass::kStorage, nullptr,
+         {
+             create<ast::BindingDecoration>(0),
+             create<ast::GroupDecoration>(0),
+         });
 
   ASSERT_FALSE(r()->Resolve());
 
@@ -64,7 +72,11 @@
   auto ac = ty.access(ast::AccessControl::kReadOnly, s);
   auto* a2 = ty.alias("a2", ac);
   AST().AddConstructedType(a2);
-  Global(Source{{56, 78}}, "g", a2, ast::StorageClass::kStorage);
+  Global(Source{{56, 78}}, "g", a2, ast::StorageClass::kStorage, nullptr,
+         {
+             create<ast::BindingDecoration>(0),
+             create<ast::GroupDecoration>(0),
+         });
 
   ASSERT_FALSE(r()->Resolve());
 
@@ -83,7 +95,11 @@
   auto* s = Structure("S", {Member(Source{{7, 8}}, "m", i3)},
                       {create<ast::StructBlockDecoration>()});
   auto a = ty.access(ast::AccessControl::kReadOnly, s);
-  Global(Source{{9, 10}}, "g", a, ast::StorageClass::kStorage);
+  Global(Source{{9, 10}}, "g", a, ast::StorageClass::kStorage, nullptr,
+         {
+             create<ast::BindingDecoration>(0),
+             create<ast::GroupDecoration>(0),
+         });
 
   ASSERT_FALSE(r()->Resolve());
 
@@ -122,7 +138,11 @@
   auto* s = Structure("S", {Member(Source{{7, 8}}, "m", i3)},
                       {create<ast::StructBlockDecoration>()});
   auto a = ty.access(ast::AccessControl::kReadOnly, s);
-  Global(Source{{9, 10}}, "g", a, ast::StorageClass::kStorage);
+  Global(Source{{9, 10}}, "g", a, ast::StorageClass::kStorage, nullptr,
+         {
+             create<ast::BindingDecoration>(0),
+             create<ast::GroupDecoration>(0),
+         });
 
   ASSERT_TRUE(r()->Resolve()) << r()->error();
 }
diff --git a/src/resolver/intrinsic_test.cc b/src/resolver/intrinsic_test.cc
index 58811f6..a821551 100644
--- a/src/resolver/intrinsic_test.cc
+++ b/src/resolver/intrinsic_test.cc
@@ -243,10 +243,17 @@
   void add_call_param(std::string name,
                       typ::Type type,
                       ast::ExpressionList* call_params) {
-    ast::StorageClass storage_class = type->UnwrapAll()->is_handle()
-                                          ? ast::StorageClass::kNone
-                                          : ast::StorageClass::kPrivate;
-    Global(name, type, storage_class);
+    if (type->UnwrapAll()->is_handle()) {
+      Global(name, type, ast::StorageClass::kNone, nullptr,
+             {
+                 create<ast::BindingDecoration>(0),
+                 create<ast::GroupDecoration>(0),
+             });
+
+    } else {
+      Global(name, type, ast::StorageClass::kPrivate);
+    }
+
     call_params->push_back(Expr(name));
   }
   typ::Type subtype(Texture type) {
@@ -763,7 +770,11 @@
   auto* str = Structure("S", {Member("x", ary)},
                         {create<ast::StructBlockDecoration>()});
   auto ac = ty.access(ast::AccessControl::kReadOnly, str);
-  Global("a", ac, ast::StorageClass::kStorage);
+  Global("a", ac, ast::StorageClass::kStorage, nullptr,
+         {
+             create<ast::BindingDecoration>(0),
+             create<ast::GroupDecoration>(0),
+         });
 
   auto* call = Call("arrayLength", MemberAccessor("a", "x"));
   WrapInFunction(call);
diff --git a/src/resolver/resolver.cc b/src/resolver/resolver.cc
index cfda9a5..4c6eb94 100644
--- a/src/resolver/resolver.cc
+++ b/src/resolver/resolver.cc
@@ -466,20 +466,10 @@
 
   for (auto* deco : var->decorations()) {
     Mark(deco);
-    if (var->is_const()) {
-      if (!deco->Is<ast::OverrideDecoration>()) {
-        diagnostics_.add_error("decoration is not valid for constants",
-                               deco->source());
-        return false;
-      }
-    } else if (!(deco->Is<ast::BindingDecoration>() ||
-                 deco->Is<ast::BuiltinDecoration>() ||
-                 deco->Is<ast::GroupDecoration>() ||
-                 deco->Is<ast::LocationDecoration>())) {
-      diagnostics_.add_error("decoration is not valid for variables",
-                             deco->source());
-      return false;
-    }
+  }
+
+  if (auto bp = var->binding_point()) {
+    info->binding_point = {bp.group->value(), bp.binding->value()};
   }
 
   if (var->has_constructor()) {
@@ -512,6 +502,53 @@
 }
 
 bool Resolver::ValidateGlobalVariable(const VariableInfo* info) {
+  for (auto* deco : info->declaration->decorations()) {
+    if (info->declaration->is_const()) {
+      if (!deco->Is<ast::OverrideDecoration>()) {
+        diagnostics_.add_error("decoration is not valid for constants",
+                               deco->source());
+        return false;
+      }
+    } else {
+      if (!(deco->Is<ast::BindingDecoration>() ||
+            deco->Is<ast::BuiltinDecoration>() ||
+            deco->Is<ast::GroupDecoration>() ||
+            deco->Is<ast::LocationDecoration>())) {
+        diagnostics_.add_error("decoration is not valid for variables",
+                               deco->source());
+        return false;
+      }
+    }
+  }
+
+  auto binding_point = info->declaration->binding_point();
+  switch (info->storage_class) {
+    case ast::StorageClass::kUniform:
+    case ast::StorageClass::kStorage:
+    case ast::StorageClass::kUniformConstant: {
+      // https://gpuweb.github.io/gpuweb/wgsl/#resource-interface
+      // Each resource variable must be declared with both group and binding
+      // attributes.
+      if (!binding_point) {
+        diagnostics_.add_error(
+            "resource variables require [[group]] and [[binding]] decorations",
+            info->declaration->source());
+        return false;
+      }
+      break;
+    }
+    default:
+      if (binding_point.binding || binding_point.group) {
+        // https://gpuweb.github.io/gpuweb/wgsl/#attribute-binding
+        // Must only be applied to a resource variable
+        diagnostics_.add_error(
+            "non-resource variables must not have [[group]] or [[binding]] "
+            "decorations",
+            info->declaration->source());
+        return false;
+      }
+  }
+
   switch (info->storage_class) {
     case ast::StorageClass::kStorage: {
       // https://gpuweb.github.io/gpuweb/wgsl/#variable-declaration
@@ -945,6 +982,33 @@
     }
   }
 
+  // Validate there are no resource variable binding collisions
+  std::unordered_map<sem::BindingPoint, const ast::Variable*> binding_points;
+  for (auto* var_info : info->referenced_module_vars) {
+    if (!var_info->declaration->binding_point()) {
+      continue;
+    }
+    auto bp = var_info->binding_point;
+    auto res = binding_points.emplace(bp, var_info->declaration);
+    if (!res.second) {
+      // https://gpuweb.github.io/gpuweb/wgsl/#resource-interface
+      // Bindings must not alias within a shader stage: two different
+      // variables in the resource interface of a given shader must not have
+      // the same group and binding values, when considered as a pair of
+      // values.
+      auto func_name = builder_->Symbols().NameFor(info->declaration->symbol());
+      diagnostics_.add_error("entry point '" + func_name +
+                                 "' references multiple variables that use the "
+                                 "same resource binding [[group(" +
+                                 std::to_string(bp.group) + "), binding(" +
+                                 std::to_string(bp.binding) + ")]]",
+                             var_info->declaration->source());
+      diagnostics_.add_note("first resource binding usage declared here",
+                            res.first->second->source());
+      return false;
+    }
+  }
+
   return true;
 }
 
diff --git a/src/resolver/resolver.h b/src/resolver/resolver.h
index c537eae..02dce4f 100644
--- a/src/resolver/resolver.h
+++ b/src/resolver/resolver.h
@@ -24,6 +24,7 @@
 #include "src/intrinsic_table.h"
 #include "src/program_builder.h"
 #include "src/scope_stack.h"
+#include "src/sem/binding_point.h"
 #include "src/sem/struct.h"
 #include "src/utils/unique_vector.h"
 
@@ -97,6 +98,7 @@
     std::string const type_name;
     ast::StorageClass storage_class;
     std::vector<ast::IdentifierExpression*> users;
+    sem::BindingPoint binding_point;
   };
 
   /// Structure holding semantic information about a function.
diff --git a/src/resolver/resolver_test.cc b/src/resolver/resolver_test.cc
index 3234348..fcfde03 100644
--- a/src/resolver/resolver_test.cc
+++ b/src/resolver/resolver_test.cc
@@ -665,7 +665,11 @@
   auto* my_var_b = Expr("my_var");
   auto* assign = Assign(my_var_a, my_var_b);
 
-  auto* var = Var("my_var", ty.f32(), ast::StorageClass::kNone);
+  auto* var = Var("my_var", ty.f32(), ast::StorageClass::kNone, nullptr,
+                  {
+                      create<ast::BindingDecoration>(0),
+                      create<ast::GroupDecoration>(0),
+                  });
 
   Func("my_func", ast::VariableList{}, ty.void_(),
        ast::StatementList{
@@ -770,7 +774,11 @@
 
   auto* in_var = Global("in_var", ty.f32(), ast::StorageClass::kInput);
   auto* out_var = Global("out_var", ty.f32(), ast::StorageClass::kOutput);
-  auto* sb_var = Global("sb_var", a, ast::StorageClass::kStorage);
+  auto* sb_var = Global("sb_var", a, ast::StorageClass::kStorage, nullptr,
+                        {
+                            create<ast::BindingDecoration>(0),
+                            create<ast::GroupDecoration>(0),
+                        });
   auto* wg_var = Global("wg_var", ty.f32(), ast::StorageClass::kWorkgroup);
   auto* priv_var = Global("priv_var", ty.f32(), ast::StorageClass::kPrivate);
 
@@ -806,7 +814,11 @@
 
   auto* in_var = Global("in_var", ty.f32(), ast::StorageClass::kInput);
   auto* out_var = Global("out_var", ty.f32(), ast::StorageClass::kOutput);
-  auto* sb_var = Global("sb_var", a, ast::StorageClass::kStorage);
+  auto* sb_var = Global("sb_var", a, ast::StorageClass::kStorage, nullptr,
+                        {
+                            create<ast::BindingDecoration>(0),
+                            create<ast::GroupDecoration>(0),
+                        });
   auto* wg_var = Global("wg_var", ty.f32(), ast::StorageClass::kWorkgroup);
   auto* priv_var = Global("priv_var", ty.f32(), ast::StorageClass::kPrivate);
 
@@ -1447,7 +1459,11 @@
                                          ast::UnaryOp::kNot));
 
 TEST_F(ResolverTest, StorageClass_SetsIfMissing) {
-  auto* var = Var("var", ty.i32(), ast::StorageClass::kNone);
+  auto* var = Var("var", ty.i32(), ast::StorageClass::kNone, nullptr,
+                  {
+                      create<ast::BindingDecoration>(0),
+                      create<ast::GroupDecoration>(0),
+                  });
 
   auto* stmt = Decl(var);
   Func("func", ast::VariableList{}, ty.void_(), ast::StatementList{stmt},
@@ -1460,7 +1476,11 @@
 
 TEST_F(ResolverTest, StorageClass_SetForSampler) {
   auto t = ty.sampler(ast::SamplerKind::kSampler);
-  auto* var = Global("var", t, ast::StorageClass::kNone);
+  auto* var = Global("var", t, ast::StorageClass::kNone, nullptr,
+                     {
+                         create<ast::BindingDecoration>(0),
+                         create<ast::GroupDecoration>(0),
+                     });
 
   EXPECT_TRUE(r()->Resolve()) << r()->error();
 
@@ -1471,7 +1491,11 @@
 TEST_F(ResolverTest, StorageClass_SetForTexture) {
   auto t = ty.sampled_texture(ast::TextureDimension::k1d, ty.f32());
   auto ac = ty.access(ast::AccessControl::Access::kReadOnly, t);
-  auto* var = Global("var", ac, ast::StorageClass::kNone);
+  auto* var = Global("var", ac, ast::StorageClass::kNone, nullptr,
+                     {
+                         create<ast::BindingDecoration>(0),
+                         create<ast::GroupDecoration>(0),
+                     });
 
   EXPECT_TRUE(r()->Resolve()) << r()->error();
 
diff --git a/src/resolver/storage_class_validation_test.cc b/src/resolver/storage_class_validation_test.cc
index 9c36597..0db6410 100644
--- a/src/resolver/storage_class_validation_test.cc
+++ b/src/resolver/storage_class_validation_test.cc
@@ -36,8 +36,12 @@
 }
 
 TEST_F(ResolverStorageClassValidationTest, StorageBufferBool) {
-  // var<storage> g : bool;
-  Global(Source{{56, 78}}, "g", ty.bool_(), ast::StorageClass::kStorage);
+  // var<storage> g : i32;
+  Global(Source{{56, 78}}, "g", ty.i32(), ast::StorageClass::kStorage, nullptr,
+         {
+             create<ast::BindingDecoration>(0),
+             create<ast::GroupDecoration>(0),
+         });
 
   ASSERT_FALSE(r()->Resolve());
 
@@ -49,7 +53,11 @@
 TEST_F(ResolverStorageClassValidationTest, StorageBufferPointer) {
   // var<storage> g : ptr<i32, input>;
   Global(Source{{56, 78}}, "g", ty.pointer<i32>(ast::StorageClass::kInput),
-         ast::StorageClass::kStorage);
+         ast::StorageClass::kStorage, nullptr,
+         {
+             create<ast::BindingDecoration>(0),
+             create<ast::GroupDecoration>(0),
+         });
 
   ASSERT_FALSE(r()->Resolve());
 
@@ -63,7 +71,11 @@
   auto* s = Structure("S", {Member("a", ty.f32())});
   auto* a = ty.array(s, 3);
   auto ac = ty.access(ast::AccessControl::kReadOnly, a);
-  Global(Source{{56, 78}}, "g", ac, ast::StorageClass::kStorage);
+  Global(Source{{56, 78}}, "g", ac, ast::StorageClass::kStorage, nullptr,
+         {
+             create<ast::BindingDecoration>(0),
+             create<ast::GroupDecoration>(0),
+         });
 
   ASSERT_FALSE(r()->Resolve());
 
@@ -77,7 +89,11 @@
   // var<storage> g : [[access(read)]] a;
   auto* a = ty.alias("a", ty.bool_());
   AST().AddConstructedType(a);
-  Global(Source{{56, 78}}, "g", a, ast::StorageClass::kStorage);
+  Global(Source{{56, 78}}, "g", a, ast::StorageClass::kStorage, nullptr,
+         {
+             create<ast::BindingDecoration>(0),
+             create<ast::GroupDecoration>(0),
+         });
 
   ASSERT_FALSE(r()->Resolve());
 
@@ -89,7 +105,11 @@
 TEST_F(ResolverStorageClassValidationTest, StorageBufferNoAccessControl) {
   // var<storage> g : S;
   auto* s = Structure("S", {Member(Source{{12, 34}}, "x", ty.i32())});
-  Global(Source{{56, 78}}, "g", s, ast::StorageClass::kStorage);
+  Global(Source{{56, 78}}, "g", s, ast::StorageClass::kStorage, nullptr,
+         {
+             create<ast::BindingDecoration>(0),
+             create<ast::GroupDecoration>(0),
+         });
 
   ASSERT_FALSE(r()->Resolve());
 
@@ -103,7 +123,11 @@
   // var<storage> g : [[access(read)]] S;
   auto* s = Structure(Source{{12, 34}}, "S", {Member("x", ty.i32())});
   auto a = ty.access(ast::AccessControl::kReadOnly, s);
-  Global(Source{{56, 78}}, "g", a, ast::StorageClass::kStorage);
+  Global(Source{{56, 78}}, "g", a, ast::StorageClass::kStorage, nullptr,
+         {
+             create<ast::BindingDecoration>(0),
+             create<ast::GroupDecoration>(0),
+         });
 
   ASSERT_FALSE(r()->Resolve());
 
@@ -119,7 +143,11 @@
   auto* s = Structure("S", {Member(Source{{12, 34}}, "x", ty.i32())},
                       {create<ast::StructBlockDecoration>()});
   auto a = ty.access(ast::AccessControl::kReadOnly, s);
-  Global(Source{{56, 78}}, "g", a, ast::StorageClass::kStorage);
+  Global(Source{{56, 78}}, "g", a, ast::StorageClass::kStorage, nullptr,
+         {
+             create<ast::BindingDecoration>(0),
+             create<ast::GroupDecoration>(0),
+         });
 
   ASSERT_TRUE(r()->Resolve());
 }
@@ -136,7 +164,11 @@
   auto ac = ty.access(ast::AccessControl::kReadOnly, a1);
   auto* a2 = ty.alias("a2", ac);
   AST().AddConstructedType(a2);
-  Global(Source{{56, 78}}, "g", a2, ast::StorageClass::kStorage);
+  Global(Source{{56, 78}}, "g", a2, ast::StorageClass::kStorage, nullptr,
+         {
+             create<ast::BindingDecoration>(0),
+             create<ast::GroupDecoration>(0),
+         });
 
   ASSERT_TRUE(r()->Resolve());
 }
@@ -145,7 +177,12 @@
 
 TEST_F(ResolverStorageClassValidationTest, UniformBufferBool) {
   // var<uniform> g : bool;
-  Global(Source{{56, 78}}, "g", ty.bool_(), ast::StorageClass::kUniform);
+  Global(Source{{56, 78}}, "g", ty.bool_(), ast::StorageClass::kUniform,
+         nullptr,
+         {
+             create<ast::BindingDecoration>(0),
+             create<ast::GroupDecoration>(0),
+         });
 
   ASSERT_FALSE(r()->Resolve());
 
@@ -157,7 +194,11 @@
 TEST_F(ResolverStorageClassValidationTest, UniformBufferPointer) {
   // var<uniform> g : ptr<i32, input>;
   Global(Source{{56, 78}}, "g", ty.pointer<i32>(ast::StorageClass::kInput),
-         ast::StorageClass::kUniform);
+         ast::StorageClass::kUniform, nullptr,
+         {
+             create<ast::BindingDecoration>(0),
+             create<ast::GroupDecoration>(0),
+         });
 
   ASSERT_FALSE(r()->Resolve());
 
@@ -171,7 +212,11 @@
   auto* s = Structure("S", {Member("a", ty.f32())});
   auto* a = ty.array(s, 3);
   auto ac = ty.access(ast::AccessControl::kReadOnly, a);
-  Global(Source{{56, 78}}, "g", ac, ast::StorageClass::kUniform);
+  Global(Source{{56, 78}}, "g", ac, ast::StorageClass::kUniform, nullptr,
+         {
+             create<ast::BindingDecoration>(0),
+             create<ast::GroupDecoration>(0),
+         });
 
   ASSERT_FALSE(r()->Resolve());
 
@@ -185,7 +230,11 @@
   // var<uniform> g : [[access(read)]] a;
   auto* a = ty.alias("a", ty.bool_());
   AST().AddConstructedType(a);
-  Global(Source{{56, 78}}, "g", a, ast::StorageClass::kUniform);
+  Global(Source{{56, 78}}, "g", a, ast::StorageClass::kUniform, nullptr,
+         {
+             create<ast::BindingDecoration>(0),
+             create<ast::GroupDecoration>(0),
+         });
 
   ASSERT_FALSE(r()->Resolve());
 
@@ -198,7 +247,11 @@
   // struct S { x : i32 };
   // var<uniform> g : S;
   auto* s = Structure(Source{{12, 34}}, "S", {Member("x", ty.i32())});
-  Global(Source{{56, 78}}, "g", s, ast::StorageClass::kUniform);
+  Global(Source{{56, 78}}, "g", s, ast::StorageClass::kUniform, nullptr,
+         {
+             create<ast::BindingDecoration>(0),
+             create<ast::GroupDecoration>(0),
+         });
 
   ASSERT_FALSE(r()->Resolve());
 
@@ -213,7 +266,11 @@
   // var<uniform> g :  S;
   auto* s = Structure("S", {Member(Source{{12, 34}}, "x", ty.i32())},
                       {create<ast::StructBlockDecoration>()});
-  Global(Source{{56, 78}}, "g", s, ast::StorageClass::kUniform);
+  Global(Source{{56, 78}}, "g", s, ast::StorageClass::kUniform, nullptr,
+         {
+             create<ast::BindingDecoration>(0),
+             create<ast::GroupDecoration>(0),
+         });
 
   ASSERT_TRUE(r()->Resolve());
 }
@@ -226,7 +283,11 @@
                       {create<ast::StructBlockDecoration>()});
   auto* a1 = ty.alias("a1", s);
   AST().AddConstructedType(a1);
-  Global(Source{{56, 78}}, "g", a1, ast::StorageClass::kUniform);
+  Global(Source{{56, 78}}, "g", a1, ast::StorageClass::kUniform, nullptr,
+         {
+             create<ast::BindingDecoration>(0),
+             create<ast::GroupDecoration>(0),
+         });
 
   ASSERT_TRUE(r()->Resolve());
 }
diff --git a/src/resolver/struct_storage_class_use_test.cc b/src/resolver/struct_storage_class_use_test.cc
index b432833..89f58bd 100644
--- a/src/resolver/struct_storage_class_use_test.cc
+++ b/src/resolver/struct_storage_class_use_test.cc
@@ -173,8 +173,16 @@
   auto* s = Structure("S", {Member("a", ty.f32())},
                       {create<ast::StructBlockDecoration>()});
   auto ac = ty.access(ast::AccessControl::kReadOnly, s);
-  Global("x", s, ast::StorageClass::kUniform);
-  Global("y", ac, ast::StorageClass::kStorage);
+  Global("x", s, ast::StorageClass::kUniform, nullptr,
+         {
+             create<ast::BindingDecoration>(0),
+             create<ast::GroupDecoration>(0),
+         });
+  Global("y", ac, ast::StorageClass::kStorage, nullptr,
+         {
+             create<ast::BindingDecoration>(1),
+             create<ast::GroupDecoration>(0),
+         });
   WrapInFunction(Var("g", s, ast::StorageClass::kFunction));
 
   ASSERT_TRUE(r()->Resolve()) << r()->error();
diff --git a/src/resolver/validation_test.cc b/src/resolver/validation_test.cc
index ba3dde3..53c087c 100644
--- a/src/resolver/validation_test.cc
+++ b/src/resolver/validation_test.cc
@@ -377,7 +377,12 @@
 
 TEST_F(ResolverValidationTest, StorageClass_SamplerExplicitStorageClass) {
   auto t = ty.sampler(ast::SamplerKind::kSampler);
-  Global(Source{{12, 34}}, "var", t, ast::StorageClass::kUniformConstant);
+  Global(Source{{12, 34}}, "var", t, ast::StorageClass::kUniformConstant,
+         nullptr,
+         {
+             create<ast::BindingDecoration>(0),
+             create<ast::GroupDecoration>(0),
+         });
 
   EXPECT_FALSE(r()->Resolve());
 
@@ -388,7 +393,12 @@
 
 TEST_F(ResolverValidationTest, StorageClass_TextureExplicitStorageClass) {
   auto t = ty.sampled_texture(ast::TextureDimension::k1d, ty.f32());
-  Global(Source{{12, 34}}, "var", t, ast::StorageClass::kUniformConstant);
+  Global(Source{{12, 34}}, "var", t, ast::StorageClass::kUniformConstant,
+         nullptr,
+         {
+             create<ast::BindingDecoration>(0),
+             create<ast::GroupDecoration>(0),
+         });
 
   EXPECT_FALSE(r()->Resolve()) << r()->error();
 
diff --git a/src/transform/binding_point.h b/src/sem/binding_point.h
similarity index 83%
rename from src/transform/binding_point.h
rename to src/sem/binding_point.h
index 131da44..e245f8b 100644
--- a/src/transform/binding_point.h
+++ b/src/sem/binding_point.h
@@ -12,8 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef SRC_TRANSFORM_BINDING_POINT_H_
-#define SRC_TRANSFORM_BINDING_POINT_H_
+#ifndef SRC_SEM_BINDING_POINT_H_
+#define SRC_SEM_BINDING_POINT_H_
 
 #include <stdint.h>
 
@@ -22,7 +22,7 @@
 #include "src/utils/hash.h"
 
 namespace tint {
-namespace transform {
+namespace sem {
 
 /// BindingPoint holds a group and binding index.
 struct BindingPoint {
@@ -46,25 +46,25 @@
   }
 };
 
-}  // namespace transform
+}  // namespace sem
 }  // namespace tint
 
 namespace std {
 
-/// Custom std::hash specialization for tint::transform::BindingPoint so
+/// Custom std::hash specialization for tint::sem::BindingPoint so
 /// BindingPoints can be used as keys for std::unordered_map and
 /// std::unordered_set.
 template <>
-class hash<tint::transform::BindingPoint> {
+class hash<tint::sem::BindingPoint> {
  public:
   /// @param binding_point the binding point to create a hash for
   /// @return the hash value
   inline std::size_t operator()(
-      const tint::transform::BindingPoint& binding_point) const {
+      const tint::sem::BindingPoint& binding_point) const {
     return tint::utils::Hash(binding_point.group, binding_point.binding);
   }
 };
 
 }  // namespace std
 
-#endif  // SRC_TRANSFORM_BINDING_POINT_H_
+#endif  // SRC_SEM_BINDING_POINT_H_
diff --git a/src/transform/binding_remapper.h b/src/transform/binding_remapper.h
index 4b6ef83..1f004b0 100644
--- a/src/transform/binding_remapper.h
+++ b/src/transform/binding_remapper.h
@@ -18,12 +18,15 @@
 #include <unordered_map>
 
 #include "src/ast/access_control.h"
-#include "src/transform/binding_point.h"
+#include "src/sem/binding_point.h"
 #include "src/transform/transform.h"
 
 namespace tint {
 namespace transform {
 
+/// BindingPoint is an alias to sem::BindingPoint
+using BindingPoint = sem::BindingPoint;
+
 /// BindingRemapper is a transform used to remap resource binding points and
 /// access controls.
 class BindingRemapper : public Transform {
diff --git a/src/transform/bound_array_accessors_test.cc b/src/transform/bound_array_accessors_test.cc
index c8cb2b4..00414f1 100644
--- a/src/transform/bound_array_accessors_test.cc
+++ b/src/transform/bound_array_accessors_test.cc
@@ -540,7 +540,7 @@
   a : f32;
   b : array<f32>;
 };
-var<storage> s : [[access(read)]] S;
+[[group(0), binding(0)]] var<storage> s : [[access(read)]] S;
 
 fn f() {
   var d : f32 = s.b[25];
@@ -554,7 +554,7 @@
   b : array<f32>;
 };
 
-var<storage> s : [[access(read)]] S;
+[[group(0), binding(0)]] var<storage> s : [[access(read)]] S;
 
 fn f() {
   var d : f32 = s.b[min(u32(25), (arrayLength(s.b) - 1u))];
@@ -601,7 +601,7 @@
   b : array<f32>;
 };
 
-var<storage> s : [[access(read)]] S;
+[[group(0), binding(0)]] var<storage> s : [[access(read)]] S;
 
 let c : u32 = 1u;
 
@@ -619,7 +619,7 @@
   b : array<f32>;
 };
 
-var<storage> s : [[access(read)]] S;
+[[group(0), binding(0)]] var<storage> s : [[access(read)]] S;
 
 let c : u32 = 1u;
 
diff --git a/src/transform/calculate_array_length_test.cc b/src/transform/calculate_array_length_test.cc
index 36bbfba..30083c3 100644
--- a/src/transform/calculate_array_length_test.cc
+++ b/src/transform/calculate_array_length_test.cc
@@ -30,7 +30,7 @@
   arr : array<i32>;
 };
 
-var<storage> sb : [[access(read)]] SB;
+[[group(0), binding(0)]] var<storage> sb : [[access(read)]] SB;
 
 [[stage(compute)]]
 fn main() {
@@ -48,7 +48,7 @@
 [[internal(intrinsic_buffer_size)]]
 fn tint_symbol(buffer : SB, result : ptr<function, u32>)
 
-var<storage> sb : [[access(read)]] SB;
+[[group(0), binding(0)]] var<storage> sb : [[access(read)]] SB;
 
 [[stage(compute)]]
 fn main() {
@@ -72,7 +72,7 @@
   arr : array<i32>;
 };
 
-var<storage> sb : [[access(read)]] SB;
+[[group(0), binding(0)]] var<storage> sb : [[access(read)]] SB;
 
 [[stage(compute)]]
 fn main() {
@@ -92,7 +92,7 @@
 [[internal(intrinsic_buffer_size)]]
 fn tint_symbol(buffer : SB, result : ptr<function, u32>)
 
-var<storage> sb : [[access(read)]] SB;
+[[group(0), binding(0)]] var<storage> sb : [[access(read)]] SB;
 
 [[stage(compute)]]
 fn main() {
@@ -119,7 +119,7 @@
   arr : [[stride(64)]] array<i32>;
 };
 
-var<storage> sb : [[access(read)]] SB;
+[[group(0), binding(0)]] var<storage> sb : [[access(read)]] SB;
 
 [[stage(compute)]]
 fn main() {
@@ -138,7 +138,7 @@
 [[internal(intrinsic_buffer_size)]]
 fn tint_symbol(buffer : SB, result : ptr<function, u32>)
 
-var<storage> sb : [[access(read)]] SB;
+[[group(0), binding(0)]] var<storage> sb : [[access(read)]] SB;
 
 [[stage(compute)]]
 fn main() {
@@ -162,7 +162,7 @@
   arr : array<i32>;
 };
 
-var<storage> sb : [[access(read)]] SB;
+[[group(0), binding(0)]] var<storage> sb : [[access(read)]] SB;
 
 [[stage(compute)]]
 fn main() {
@@ -186,7 +186,7 @@
 [[internal(intrinsic_buffer_size)]]
 fn tint_symbol(buffer : SB, result : ptr<function, u32>)
 
-var<storage> sb : [[access(read)]] SB;
+[[group(0), binding(0)]] var<storage> sb : [[access(read)]] SB;
 
 [[stage(compute)]]
 fn main() {
@@ -225,9 +225,9 @@
   arr2 : array<vec4<f32>>;
 };
 
-var<storage> sb1 : [[access(read)]] SB1;
+[[group(0), binding(0)]] var<storage> sb1 : [[access(read)]] SB1;
 
-var<storage> sb2 : [[access(read)]] SB2;
+[[group(0), binding(1)]] var<storage> sb2 : [[access(read)]] SB2;
 
 [[stage(compute)]]
 fn main() {
@@ -256,9 +256,9 @@
 [[internal(intrinsic_buffer_size)]]
 fn tint_symbol_3(buffer : SB2, result : ptr<function, u32>)
 
-var<storage> sb1 : [[access(read)]] SB1;
+[[group(0), binding(0)]] var<storage> sb1 : [[access(read)]] SB1;
 
-var<storage> sb2 : [[access(read)]] SB2;
+[[group(0), binding(1)]] var<storage> sb2 : [[access(read)]] SB2;
 
 [[stage(compute)]]
 fn main() {
diff --git a/src/transform/decompose_storage_access_test.cc b/src/transform/decompose_storage_access_test.cc
index dadf24c..9de479c 100644
--- a/src/transform/decompose_storage_access_test.cc
+++ b/src/transform/decompose_storage_access_test.cc
@@ -50,7 +50,7 @@
   v : array<vec3<f32>, 2>;
 };
 
-var<storage> sb : [[access(read_write)]] SB;
+[[group(0), binding(0)]] var<storage> sb : [[access(read_write)]] SB;
 
 [[stage(compute)]]
 fn main() {
@@ -182,7 +182,7 @@
   return array<vec3<f32>, 2>(tint_symbol_8(buffer, (offset + 0u)), tint_symbol_8(buffer, (offset + 16u)));
 }
 
-var<storage> sb : [[access(read_write)]] SB;
+[[group(0), binding(0)]] var<storage> sb : [[access(read_write)]] SB;
 
 [[stage(compute)]]
 fn main() {
@@ -244,7 +244,7 @@
   v : array<vec3<f32>, 2>;
 };
 
-var<storage> sb : [[access(read_write)]] SB;
+[[group(0), binding(0)]] var<storage> sb : [[access(read_write)]] SB;
 
 [[stage(compute)]]
 fn main() {
@@ -395,7 +395,7 @@
   tint_symbol_8(buffer, (offset + 16u), value[1u]);
 }
 
-var<storage> sb : [[access(read_write)]] SB;
+[[group(0), binding(0)]] var<storage> sb : [[access(read_write)]] SB;
 
 [[stage(compute)]]
 fn main() {
@@ -457,7 +457,7 @@
   v : array<vec3<f32>, 2>;
 };
 
-var<storage> sb : [[access(read_write)]] SB;
+[[group(0), binding(0)]] var<storage> sb : [[access(read_write)]] SB;
 
 [[stage(compute)]]
 fn main() {
@@ -572,7 +572,7 @@
   return SB(tint_symbol(buffer, (offset + 0u)), tint_symbol_1(buffer, (offset + 4u)), tint_symbol_2(buffer, (offset + 8u)), tint_symbol_3(buffer, (offset + 16u)), tint_symbol_4(buffer, (offset + 24u)), tint_symbol_5(buffer, (offset + 32u)), tint_symbol_6(buffer, (offset + 48u)), tint_symbol_7(buffer, (offset + 64u)), tint_symbol_8(buffer, (offset + 80u)), tint_symbol_9(buffer, (offset + 96u)), tint_symbol_10(buffer, (offset + 112u)), tint_symbol_11(buffer, (offset + 128u)), tint_symbol_12(buffer, (offset + 144u)), tint_symbol_13(buffer, (offset + 160u)), tint_symbol_14(buffer, (offset + 192u)), tint_symbol_15(buffer, (offset + 224u)), tint_symbol_16(buffer, (offset + 256u)), tint_symbol_17(buffer, (offset + 304u)), tint_symbol_18(buffer, (offset + 352u)), tint_symbol_19(buffer, (offset + 384u)), tint_symbol_20(buffer, (offset + 448u)), tint_symbol_21(buffer, (offset + 512u)));
 }
 
-var<storage> sb : [[access(read_write)]] SB;
+[[group(0), binding(0)]] var<storage> sb : [[access(read_write)]] SB;
 
 [[stage(compute)]]
 fn main() {
@@ -613,7 +613,7 @@
   v : array<vec3<f32>, 2>;
 };
 
-var<storage> sb : [[access(read_write)]] SB;
+[[group(0), binding(0)]] var<storage> sb : [[access(read_write)]] SB;
 
 [[stage(compute)]]
 fn main() {
@@ -768,7 +768,7 @@
   tint_symbol_21(buffer, (offset + 512u), value.v);
 }
 
-var<storage> sb : [[access(read_write)]] SB;
+[[group(0), binding(0)]] var<storage> sb : [[access(read_write)]] SB;
 
 [[stage(compute)]]
 fn main() {
@@ -802,7 +802,7 @@
   b : [[stride(256)]] array<S2>;
 };
 
-var<storage> sb : [[access(read_write)]] SB;
+[[group(0), binding(0)]] var<storage> sb : [[access(read_write)]] SB;
 
 [[stage(compute)]]
 fn main() {
@@ -840,7 +840,7 @@
 [[internal(intrinsic_load_f32)]]
 fn tint_symbol(buffer : [[access(read_write)]] SB, offset : u32) -> f32
 
-var<storage> sb : [[access(read_write)]] SB;
+[[group(0), binding(0)]] var<storage> sb : [[access(read_write)]] SB;
 
 [[stage(compute)]]
 fn main() {
@@ -874,7 +874,7 @@
   b : [[stride(256)]] array<S2>;
 };
 
-var<storage> sb : [[access(read_write)]] SB;
+[[group(0), binding(0)]] var<storage> sb : [[access(read_write)]] SB;
 
 [[stage(compute)]]
 fn main() {
@@ -908,7 +908,7 @@
 [[internal(intrinsic_load_f32)]]
 fn tint_symbol(buffer : [[access(read_write)]] SB, offset : u32) -> f32
 
-var<storage> sb : [[access(read_write)]] SB;
+[[group(0), binding(0)]] var<storage> sb : [[access(read_write)]] SB;
 
 [[stage(compute)]]
 fn main() {
@@ -953,7 +953,7 @@
   b : A2_Array;
 };
 
-var<storage> sb : [[access(read_write)]] SB;
+[[group(0), binding(0)]] var<storage> sb : [[access(read_write)]] SB;
 
 [[stage(compute)]]
 fn main() {
@@ -995,7 +995,7 @@
 [[internal(intrinsic_load_f32)]]
 fn tint_symbol(buffer : [[access(read_write)]] SB, offset : u32) -> f32
 
-var<storage> sb : [[access(read_write)]] SB;
+[[group(0), binding(0)]] var<storage> sb : [[access(read_write)]] SB;
 
 [[stage(compute)]]
 fn main() {
diff --git a/src/writer/hlsl/generator_impl_type_test.cc b/src/writer/hlsl/generator_impl_type_test.cc
index 6354625..e3252f0 100644
--- a/src/writer/hlsl/generator_impl_type_test.cc
+++ b/src/writer/hlsl/generator_impl_type_test.cc
@@ -183,7 +183,11 @@
                       },
                       {create<ast::StructBlockDecoration>()});
   Global("g", ty.access(ast::AccessControl::kReadWrite, s),
-         ast::StorageClass::kStorage);
+         ast::StorageClass::kStorage, nullptr,
+         {
+             create<ast::BindingDecoration>(0),
+             create<ast::GroupDecoration>(0),
+         });
 
   GeneratorImpl& gen = Build();
 
diff --git a/src/writer/msl/generator_impl_type_test.cc b/src/writer/msl/generator_impl_type_test.cc
index 80c64c4..b80e3e5 100644
--- a/src/writer/msl/generator_impl_type_test.cc
+++ b/src/writer/msl/generator_impl_type_test.cc
@@ -232,7 +232,11 @@
                 {create<ast::StructBlockDecoration>()});
 
   Global("G", ty.access(ast::AccessControl::kReadOnly, s),
-         ast::StorageClass::kStorage);
+         ast::StorageClass::kStorage, nullptr,
+         {
+             create<ast::BindingDecoration>(0),
+             create<ast::GroupDecoration>(0),
+         });
 
   GeneratorImpl& gen = Build();
 
@@ -338,7 +342,11 @@
                       {create<ast::StructBlockDecoration>()});
 
   Global("G", ty.access(ast::AccessControl::kReadOnly, s),
-         ast::StorageClass::kStorage);
+         ast::StorageClass::kStorage, nullptr,
+         {
+             create<ast::BindingDecoration>(0),
+             create<ast::GroupDecoration>(0),
+         });
 
   GeneratorImpl& gen = Build();
 
@@ -429,7 +437,11 @@
                 ast::DecorationList{create<ast::StructBlockDecoration>()});
 
   Global("G", ty.access(ast::AccessControl::kReadOnly, s),
-         ast::StorageClass::kStorage);
+         ast::StorageClass::kStorage, nullptr,
+         {
+             create<ast::BindingDecoration>(0),
+             create<ast::GroupDecoration>(0),
+         });
 
   GeneratorImpl& gen = Build();
 
@@ -532,7 +544,11 @@
       {create<ast::StructBlockDecoration>()});
 
   Global("G", ty.access(ast::AccessControl::kReadOnly, s),
-         ast::StorageClass::kStorage);
+         ast::StorageClass::kStorage, nullptr,
+         {
+             create<ast::BindingDecoration>(0),
+             create<ast::GroupDecoration>(0),
+         });
 
   GeneratorImpl& gen = Build();
 
@@ -594,7 +610,11 @@
                       {create<ast::StructBlockDecoration>()});
 
   Global("G", ty.access(ast::AccessControl::kReadOnly, s),
-         ast::StorageClass::kStorage);
+         ast::StorageClass::kStorage, nullptr,
+         {
+             create<ast::BindingDecoration>(0),
+             create<ast::GroupDecoration>(0),
+         });
 
   GeneratorImpl& gen = Build();
 
@@ -745,7 +765,11 @@
   auto ac = ty.access(params.ro ? ast::AccessControl::kReadOnly
                                 : ast::AccessControl::kWriteOnly,
                       s);
-  Global("test_var", ac, ast::StorageClass::kNone);
+  Global("test_var", ac, ast::StorageClass::kNone, nullptr,
+         {
+             create<ast::BindingDecoration>(0),
+             create<ast::GroupDecoration>(0),
+         });
 
   GeneratorImpl& gen = Build();
 
diff --git a/src/writer/spirv/builder_global_variable_test.cc b/src/writer/spirv/builder_global_variable_test.cc
index 4c28e9f..23e8b78 100644
--- a/src/writer/spirv/builder_global_variable_test.cc
+++ b/src/writer/spirv/builder_global_variable_test.cc
@@ -162,7 +162,8 @@
 }
 
 TEST_F(BuilderTest, GlobalVar_WithBindingAndGroup) {
-  auto* v = Global("var", ty.f32(), ast::StorageClass::kOutput, nullptr,
+  auto* v = Global("var", ty.sampler(ast::SamplerKind::kSampler),
+                   ast::StorageClass::kNone, nullptr,
                    ast::DecorationList{
                        create<ast::BindingDecoration>(2),
                        create<ast::GroupDecoration>(3),
@@ -176,10 +177,9 @@
   EXPECT_EQ(DumpInstructions(b.annots()), R"(OpDecorate %1 Binding 2
 OpDecorate %1 DescriptorSet 3
 )");
-  EXPECT_EQ(DumpInstructions(b.types()), R"(%3 = OpTypeFloat 32
-%2 = OpTypePointer Output %3
-%4 = OpConstantNull %3
-%1 = OpVariable %2 Output %4
+  EXPECT_EQ(DumpInstructions(b.types()), R"(%3 = OpTypeSampler
+%2 = OpTypePointer UniformConstant %3
+%1 = OpVariable %2 UniformConstant
 )");
 }
 
@@ -384,7 +384,11 @@
                       {create<ast::StructBlockDecoration>()});
   auto ac = ty.access(ast::AccessControl::kReadOnly, A);
 
-  auto* var = Global("b", ac, ast::StorageClass::kStorage);
+  auto* var = Global("b", ac, ast::StorageClass::kStorage, nullptr,
+                     {
+                         create<ast::BindingDecoration>(0),
+                         create<ast::GroupDecoration>(0),
+                     });
 
   spirv::Builder& b = Build();
 
@@ -395,6 +399,8 @@
 OpMemberDecorate %3 0 NonWritable
 OpMemberDecorate %3 1 Offset 4
 OpMemberDecorate %3 1 NonWritable
+OpDecorate %1 Binding 0
+OpDecorate %1 DescriptorSet 0
 )");
   EXPECT_EQ(DumpInstructions(b.debug()), R"(OpName %3 "A"
 OpMemberName %3 0 "a"
@@ -420,7 +426,11 @@
   auto* B = ty.alias("B", A);
   AST().AddConstructedType(B);
   auto ac = ty.access(ast::AccessControl::kReadOnly, B);
-  auto* var = Global("b", ac, ast::StorageClass::kStorage);
+  auto* var = Global("b", ac, ast::StorageClass::kStorage, nullptr,
+                     {
+                         create<ast::BindingDecoration>(0),
+                         create<ast::GroupDecoration>(0),
+                     });
 
   spirv::Builder& b = Build();
 
@@ -429,6 +439,8 @@
   EXPECT_EQ(DumpInstructions(b.annots()), R"(OpDecorate %3 Block
 OpMemberDecorate %3 0 Offset 0
 OpMemberDecorate %3 0 NonWritable
+OpDecorate %1 Binding 0
+OpDecorate %1 DescriptorSet 0
 )");
   EXPECT_EQ(DumpInstructions(b.debug()), R"(OpName %3 "A"
 OpMemberName %3 0 "a"
@@ -453,7 +465,11 @@
   auto ac = ty.access(ast::AccessControl::kReadOnly, A);
   auto* B = ty.alias("B", ac);
   AST().AddConstructedType(B);
-  auto* var = Global("b", B, ast::StorageClass::kStorage);
+  auto* var = Global("b", B, ast::StorageClass::kStorage, nullptr,
+                     {
+                         create<ast::BindingDecoration>(0),
+                         create<ast::GroupDecoration>(0),
+                     });
 
   spirv::Builder& b = Build();
 
@@ -462,6 +478,8 @@
   EXPECT_EQ(DumpInstructions(b.annots()), R"(OpDecorate %3 Block
 OpMemberDecorate %3 0 Offset 0
 OpMemberDecorate %3 0 NonWritable
+OpDecorate %1 Binding 0
+OpDecorate %1 DescriptorSet 0
 )");
   EXPECT_EQ(DumpInstructions(b.debug()), R"(OpName %3 "A"
 OpMemberName %3 0 "a"
@@ -486,8 +504,16 @@
   auto read = ty.access(ast::AccessControl::kReadOnly, A);
   auto rw = ty.access(ast::AccessControl::kReadWrite, A);
 
-  auto* var_b = Global("b", read, ast::StorageClass::kStorage);
-  auto* var_c = Global("c", rw, ast::StorageClass::kStorage);
+  auto* var_b = Global("b", read, ast::StorageClass::kStorage, nullptr,
+                       {
+                           create<ast::GroupDecoration>(0),
+                           create<ast::BindingDecoration>(0),
+                       });
+  auto* var_c = Global("c", rw, ast::StorageClass::kStorage, nullptr,
+                       {
+                           create<ast::GroupDecoration>(1),
+                           create<ast::BindingDecoration>(0),
+                       });
 
   spirv::Builder& b = Build();
 
@@ -498,8 +524,12 @@
             R"(OpDecorate %3 Block
 OpMemberDecorate %3 0 Offset 0
 OpMemberDecorate %3 0 NonWritable
+OpDecorate %1 DescriptorSet 0
+OpDecorate %1 Binding 0
 OpDecorate %7 Block
 OpMemberDecorate %7 0 Offset 0
+OpDecorate %5 DescriptorSet 1
+OpDecorate %5 Binding 0
 )");
   EXPECT_EQ(DumpInstructions(b.debug()), R"(OpName %3 "A"
 OpMemberName %3 0 "a"
@@ -526,13 +556,19 @@
 
   auto ac = ty.access(ast::AccessControl::kReadOnly, type);
 
-  auto* var_a = Global("a", ac, ast::StorageClass::kNone);
+  auto* var_a = Global("a", ac, ast::StorageClass::kNone, nullptr,
+                       {
+                           create<ast::BindingDecoration>(0),
+                           create<ast::GroupDecoration>(0),
+                       });
 
   spirv::Builder& b = Build();
 
   EXPECT_TRUE(b.GenerateGlobalVariable(var_a)) << b.error();
 
   EXPECT_EQ(DumpInstructions(b.annots()), R"(OpDecorate %1 NonWritable
+OpDecorate %1 Binding 0
+OpDecorate %1 DescriptorSet 0
 )");
   EXPECT_EQ(DumpInstructions(b.types()), R"(%4 = OpTypeInt 32 0
 %3 = OpTypeImage %4 2D 0 0 0 2 R32ui
@@ -549,13 +585,19 @@
 
   auto ac = ty.access(ast::AccessControl::kWriteOnly, type);
 
-  auto* var_a = Global("a", ac, ast::StorageClass::kNone);
+  auto* var_a = Global("a", ac, ast::StorageClass::kNone, nullptr,
+                       {
+                           create<ast::BindingDecoration>(0),
+                           create<ast::GroupDecoration>(0),
+                       });
 
   spirv::Builder& b = Build();
 
   EXPECT_TRUE(b.GenerateGlobalVariable(var_a)) << b.error();
 
   EXPECT_EQ(DumpInstructions(b.annots()), R"(OpDecorate %1 NonReadable
+OpDecorate %1 Binding 0
+OpDecorate %1 DescriptorSet 0
 )");
   EXPECT_EQ(DumpInstructions(b.types()), R"(%4 = OpTypeInt 32 0
 %3 = OpTypeImage %4 2D 0 0 0 2 R32ui
@@ -573,12 +615,20 @@
   auto type_a = ty.access(ast::AccessControl::kReadOnly,
                           ty.storage_texture(ast::TextureDimension::k2d,
                                              ast::ImageFormat::kR32Uint));
-  auto* var_a = Global("a", type_a, ast::StorageClass::kNone);
+  auto* var_a = Global("a", type_a, ast::StorageClass::kNone, nullptr,
+                       {
+                           create<ast::BindingDecoration>(0),
+                           create<ast::GroupDecoration>(0),
+                       });
 
   auto type_b = ty.access(ast::AccessControl::kWriteOnly,
                           ty.storage_texture(ast::TextureDimension::k2d,
                                              ast::ImageFormat::kR32Uint));
-  auto* var_b = Global("b", type_b, ast::StorageClass::kNone);
+  auto* var_b = Global("b", type_b, ast::StorageClass::kNone, nullptr,
+                       {
+                           create<ast::BindingDecoration>(1),
+                           create<ast::GroupDecoration>(0),
+                       });
 
   spirv::Builder& b = Build();
 
@@ -586,7 +636,11 @@
   EXPECT_TRUE(b.GenerateGlobalVariable(var_b)) << b.error();
 
   EXPECT_EQ(DumpInstructions(b.annots()), R"(OpDecorate %1 NonWritable
+OpDecorate %1 Binding 0
+OpDecorate %1 DescriptorSet 0
 OpDecorate %5 NonReadable
+OpDecorate %5 Binding 1
+OpDecorate %5 DescriptorSet 0
 )");
   // There must only be one OpTypeImage declaration with the same
   // arguments
diff --git a/src/writer/spirv/builder_intrinsic_test.cc b/src/writer/spirv/builder_intrinsic_test.cc
index 9e353c1..6ce0797 100644
--- a/src/writer/spirv/builder_intrinsic_test.cc
+++ b/src/writer/spirv/builder_intrinsic_test.cc
@@ -373,9 +373,17 @@
   auto s = ty.sampler(ast::SamplerKind::kComparisonSampler);
   auto t = ty.depth_texture(ast::TextureDimension::k2d);
 
-  auto* tex = Global("texture", t, ast::StorageClass::kNone);
+  auto* tex = Global("texture", t, ast::StorageClass::kNone, nullptr,
+                     {
+                         create<ast::BindingDecoration>(0),
+                         create<ast::GroupDecoration>(0),
+                     });
 
-  auto* sampler = Global("sampler", s, ast::StorageClass::kNone);
+  auto* sampler = Global("sampler", s, ast::StorageClass::kNone, nullptr,
+                         {
+                             create<ast::BindingDecoration>(1),
+                             create<ast::GroupDecoration>(0),
+                         });
 
   auto* expr1 = Call("textureSampleCompare", "texture", "sampler",
                      vec2<f32>(1.0f, 2.0f), 2.0f);
diff --git a/src/writer/spirv/builder_type_test.cc b/src/writer/spirv/builder_type_test.cc
index 477007b..fdb2d76 100644
--- a/src/writer/spirv/builder_type_test.cc
+++ b/src/writer/spirv/builder_type_test.cc
@@ -31,7 +31,11 @@
   auto* str = Structure("S", {Member("x", ary)},
                         {create<ast::StructBlockDecoration>()});
   auto ac = ty.access(ast::AccessControl::kReadOnly, str);
-  Global("a", ac, ast::StorageClass::kStorage);
+  Global("a", ac, ast::StorageClass::kStorage, nullptr,
+         {
+             create<ast::BindingDecoration>(0),
+             create<ast::GroupDecoration>(0),
+         });
 
   spirv::Builder& b = Build();
 
@@ -49,7 +53,11 @@
   auto* str = Structure("S", {Member("x", ary)},
                         {create<ast::StructBlockDecoration>()});
   auto ac = ty.access(ast::AccessControl::kReadOnly, str);
-  Global("a", ac, ast::StorageClass::kStorage);
+  Global("a", ac, ast::StorageClass::kStorage, nullptr,
+         {
+             create<ast::BindingDecoration>(0),
+             create<ast::GroupDecoration>(0),
+         });
 
   spirv::Builder& b = Build();
 
@@ -816,7 +824,11 @@
                               ast::ImageFormat::kR32Float);
   auto ac = ty.access(ast::AccessControl::kReadOnly, s);
 
-  Global("test_var", ac, ast::StorageClass::kNone);
+  Global("test_var", ac, ast::StorageClass::kNone, nullptr,
+         {
+             create<ast::BindingDecoration>(0),
+             create<ast::GroupDecoration>(0),
+         });
 
   spirv::Builder& b = Build();
 
@@ -832,7 +844,11 @@
                               ast::ImageFormat::kR32Float);
   auto ac = ty.access(ast::AccessControl::kReadOnly, s);
 
-  Global("test_var", ac, ast::StorageClass::kNone);
+  Global("test_var", ac, ast::StorageClass::kNone, nullptr,
+         {
+             create<ast::BindingDecoration>(0),
+             create<ast::GroupDecoration>(0),
+         });
 
   spirv::Builder& b = Build();
 
@@ -848,7 +864,11 @@
                               ast::ImageFormat::kR32Float);
   auto ac = ty.access(ast::AccessControl::kReadOnly, s);
 
-  Global("test_var", ac, ast::StorageClass::kNone);
+  Global("test_var", ac, ast::StorageClass::kNone, nullptr,
+         {
+             create<ast::BindingDecoration>(0),
+             create<ast::GroupDecoration>(0),
+         });
 
   spirv::Builder& b = Build();
 
@@ -864,7 +884,11 @@
                               ast::ImageFormat::kR32Float);
   auto ac = ty.access(ast::AccessControl::kReadOnly, s);
 
-  Global("test_var", ac, ast::StorageClass::kNone);
+  Global("test_var", ac, ast::StorageClass::kNone, nullptr,
+         {
+             create<ast::BindingDecoration>(0),
+             create<ast::GroupDecoration>(0),
+         });
 
   spirv::Builder& b = Build();
 
@@ -881,7 +905,11 @@
                               ast::ImageFormat::kR32Float);
   auto ac = ty.access(ast::AccessControl::kReadOnly, s);
 
-  Global("test_var", ac, ast::StorageClass::kNone);
+  Global("test_var", ac, ast::StorageClass::kNone, nullptr,
+         {
+             create<ast::BindingDecoration>(0),
+             create<ast::GroupDecoration>(0),
+         });
 
   spirv::Builder& b = Build();
 
@@ -898,7 +926,11 @@
                               ast::ImageFormat::kR32Sint);
   auto ac = ty.access(ast::AccessControl::kReadOnly, s);
 
-  Global("test_var", ac, ast::StorageClass::kNone);
+  Global("test_var", ac, ast::StorageClass::kNone, nullptr,
+         {
+             create<ast::BindingDecoration>(0),
+             create<ast::GroupDecoration>(0),
+         });
 
   spirv::Builder& b = Build();
 
@@ -915,7 +947,11 @@
                               ast::ImageFormat::kR32Uint);
   auto ac = ty.access(ast::AccessControl::kReadOnly, s);
 
-  Global("test_var", ac, ast::StorageClass::kNone);
+  Global("test_var", ac, ast::StorageClass::kNone, nullptr,
+         {
+             create<ast::BindingDecoration>(0),
+             create<ast::GroupDecoration>(0),
+         });
 
   spirv::Builder& b = Build();