writer/msl: Handle private and workgroup variables

Add a transform that pushes these into the entry point and then passes
them by pointer to any functions that need them.

Since WGSL does not allow non-function storage class at
function-scope, add a DisableValidation attribute to bypass this
check.

Fixed: tint/726
Change-Id: Ic1f4cd691a54c19e77a60e8ba178508e4249bfd9
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/51962
Kokoro: Kokoro <noreply+kokoro@google.com>
Commit-Queue: James Price <jrprice@google.com>
Auto-Submit: James Price <jrprice@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
diff --git a/src/ast/disable_validation_decoration.cc b/src/ast/disable_validation_decoration.cc
index ef93c0f..59eb291 100644
--- a/src/ast/disable_validation_decoration.cc
+++ b/src/ast/disable_validation_decoration.cc
@@ -34,6 +34,8 @@
       return "disable_validation__function_has_no_body";
     case DisabledValidation::kBindingPointCollision:
       return "disable_validation__binding_point_collision";
+    case DisabledValidation::kFunctionVarStorageClass:
+      return "disable_validation__function_var_storage_class";
   }
   return "<invalid>";
 }
diff --git a/src/ast/disable_validation_decoration.h b/src/ast/disable_validation_decoration.h
index a825230..c94f166 100644
--- a/src/ast/disable_validation_decoration.h
+++ b/src/ast/disable_validation_decoration.h
@@ -31,6 +31,9 @@
   /// When applied to a module-scoped variable, the validator will not complain
   /// if two resource variables have the same binding points.
   kBindingPointCollision,
+  /// When applied to a function-scoped variable, the validator will not
+  /// complain if the storage class is not `function`.
+  kFunctionVarStorageClass,
 };
 
 /// An internal decoration used to tell the validator to ignore specific
diff --git a/src/resolver/resolver.cc b/src/resolver/resolver.cc
index 8058ab0..15e7f3e 100644
--- a/src/resolver/resolver.cc
+++ b/src/resolver/resolver.cc
@@ -2360,7 +2360,10 @@
   }
 
   if (!var->is_const()) {
-    if (info->storage_class != ast::StorageClass::kFunction) {
+    if (info->storage_class != ast::StorageClass::kFunction &&
+        !IsValidationDisabled(
+            var->decorations(),
+            ast::DisabledValidation::kFunctionVarStorageClass)) {
       if (info->storage_class != ast::StorageClass::kNone) {
         diagnostics_.add_error(
             "function variable has a non-function storage class",
diff --git a/src/transform/msl.cc b/src/transform/msl.cc
index 764287d..a19b155 100644
--- a/src/transform/msl.cc
+++ b/src/transform/msl.cc
@@ -14,8 +14,16 @@
 
 #include "src/transform/msl.h"
 
+#include <unordered_map>
 #include <utility>
+#include <vector>
 
+#include "src/ast/disable_validation_decoration.h"
+#include "src/program_builder.h"
+#include "src/sem/call.h"
+#include "src/sem/function.h"
+#include "src/sem/statement.h"
+#include "src/sem/variable.h"
 #include "src/transform/canonicalize_entry_point_io.h"
 #include "src/transform/external_texture_transform.h"
 #include "src/transform/manager.h"
@@ -34,7 +42,162 @@
   if (!out.program.IsValid()) {
     return out;
   }
-  return Output{Program(std::move(out.program))};
+
+  ProgramBuilder builder;
+  CloneContext ctx(&builder, &out.program);
+  // TODO(jrprice): Consider making this a standalone transform, with target
+  // storage class(es) as transform options.
+  HandlePrivateAndWorkgroupVariables(ctx);
+  ctx.Clone();
+  return Output{Program(std::move(builder))};
+}
+
+void Msl::HandlePrivateAndWorkgroupVariables(CloneContext& ctx) const {
+  // MSL does not allow private and workgroup variables at module-scope, so we
+  // push these declarations into the entry point function and then pass them as
+  // pointer parameters to any function that references them.
+  //
+  // Since WGSL does not allow function-scope variables to have these storage
+  // classes, we annotate the new variable declarations with an attribute that
+  // bypasses that validation rule.
+  //
+  // Before:
+  // ```
+  // var<private> v : f32 = 2.0;
+  //
+  // fn foo() {
+  //   v = v + 1.0;
+  // }
+  //
+  // [[stage(compute)]]
+  // fn main() {
+  //   foo();
+  // }
+  // ```
+  //
+  // After:
+  // ```
+  // fn foo(v : ptr<private, f32>) {
+  //   *v = *v + 1.0;
+  // }
+  //
+  // [[stage(compute)]]
+  // fn main() {
+  //   var<private> v : f32 = 2.0;
+  //   let v_ptr : ptr<private, f32> = &f32;
+  //   foo(v_ptr);
+  // }
+  // ```
+
+  // Predetermine the list of function calls that need to be replaced.
+  using CallList = std::vector<const ast::CallExpression*>;
+  std::unordered_map<const ast::Function*, CallList> calls_to_replace;
+
+  std::vector<ast::Function*> functions_to_process;
+
+  // Build a list of functions that transitively reference any private or
+  // workgroup variables.
+  for (auto* func_ast : ctx.src->AST().Functions()) {
+    auto* func_sem = ctx.src->Sem().Get(func_ast);
+
+    bool needs_processing = false;
+    for (auto* var : func_sem->ReferencedModuleVariables()) {
+      if (var->StorageClass() == ast::StorageClass::kPrivate ||
+          var->StorageClass() == ast::StorageClass::kWorkgroup) {
+        needs_processing = true;
+        break;
+      }
+    }
+
+    if (needs_processing) {
+      functions_to_process.push_back(func_ast);
+
+      // Find all of the calls to this function that will need to be replaced.
+      for (auto* call : func_sem->CallSites()) {
+        auto* call_sem = ctx.src->Sem().Get(call);
+        calls_to_replace[call_sem->Stmt()->Function()].push_back(call);
+      }
+    }
+  }
+
+  for (auto* func_ast : functions_to_process) {
+    auto* func_sem = ctx.src->Sem().Get(func_ast);
+
+    // Map module-scope variables onto their function-scope replacement.
+    std::unordered_map<const sem::Variable*, Symbol> var_to_symbol;
+
+    for (auto* var : func_sem->ReferencedModuleVariables()) {
+      if (var->StorageClass() != ast::StorageClass::kPrivate &&
+          var->StorageClass() != ast::StorageClass::kWorkgroup) {
+        continue;
+      }
+
+      // This is the symbol for the pointer that replaces the module-scope var.
+      auto new_var_symbol = ctx.dst->Sym();
+
+      auto* store_type = CreateASTTypeFor(&ctx, var->Type()->UnwrapRef());
+
+      if (func_ast->IsEntryPoint()) {
+        // For an entry point, redeclare the variable at function-scope.
+        // Disable storage class validation on this variable.
+        auto* disable_validation =
+            ctx.dst->ASTNodes().Create<ast::DisableValidationDecoration>(
+                ctx.dst->ID(),
+                ast::DisabledValidation::kFunctionVarStorageClass);
+        auto* constructor = ctx.Clone(var->Declaration()->constructor());
+        auto* local_var =
+            ctx.dst->Var(ctx.dst->Sym(), store_type, var->StorageClass(),
+                         constructor, {disable_validation});
+        ctx.InsertBefore(func_ast->body()->statements(),
+                         *func_ast->body()->begin(), ctx.dst->Decl(local_var));
+
+        // Now take the address of the variable.
+        auto* ptr = ctx.dst->Const(new_var_symbol, nullptr,
+                                   ctx.dst->AddressOf(local_var));
+        ctx.InsertBefore(func_ast->body()->statements(),
+                         *func_ast->body()->begin(), ctx.dst->Decl(ptr));
+      } else {
+        // For a regular function, redeclare the variable as a pointer function
+        // parameter.
+        auto* ptr_type = ctx.dst->ty.pointer(store_type, var->StorageClass());
+        ctx.InsertBack(func_ast->params(),
+                       ctx.dst->Param(new_var_symbol, ptr_type));
+      }
+
+      // Replace all uses of the module-scope variable with the pointer
+      // replacement (dereferenced).
+      for (auto* user : var->Users()) {
+        if (user->Stmt()->Function() == func_ast) {
+          ctx.Replace(user->Declaration(), ctx.dst->Deref(new_var_symbol));
+        }
+      }
+
+      var_to_symbol[var] = new_var_symbol;
+    }
+
+    // Pass the pointers through to any functions that need them.
+    for (auto* call : calls_to_replace[func_ast]) {
+      auto* target = ctx.src->AST().Functions().Find(call->func()->symbol());
+      auto* target_sem = ctx.src->Sem().Get(target);
+
+      // Add new arguments for any referenced private and workgroup variables.
+      for (auto* target_var : target_sem->ReferencedModuleVariables()) {
+        if (target_var->StorageClass() == ast::StorageClass::kPrivate ||
+            target_var->StorageClass() == ast::StorageClass::kWorkgroup) {
+          ctx.InsertBack(call->params(),
+                         ctx.dst->Expr(var_to_symbol[target_var]));
+        }
+      }
+    }
+  }
+
+  // Now remove all module-scope private and workgroup variables.
+  for (auto* var : ctx.src->AST().GlobalVariables()) {
+    if (var->declared_storage_class() == ast::StorageClass::kPrivate ||
+        var->declared_storage_class() == ast::StorageClass::kWorkgroup) {
+      ctx.Remove(ctx.src->AST().GlobalDeclarations(), var);
+    }
+  }
 }
 
 }  // namespace transform
diff --git a/src/transform/msl.h b/src/transform/msl.h
index 3121b67..7e83032 100644
--- a/src/transform/msl.h
+++ b/src/transform/msl.h
@@ -34,6 +34,12 @@
   /// @param data optional extra transform-specific input data
   /// @returns the transformation result
   Output Run(const Program* program, const DataMap& data = {}) override;
+
+ private:
+  /// Pushes module-scope variables with private or workgroup storage classes
+  /// into the entry point function, and passes them as function parameters to
+  /// any functions that need them.
+  void HandlePrivateAndWorkgroupVariables(CloneContext& ctx) const;
 };
 
 }  // namespace transform
diff --git a/src/transform/msl_test.cc b/src/transform/msl_test.cc
index d257b2b..70f50d8 100644
--- a/src/transform/msl_test.cc
+++ b/src/transform/msl_test.cc
@@ -20,6 +20,210 @@
 namespace transform {
 namespace {
 
+using MslTest = TransformTest;
+
+TEST_F(MslTest, HandlePrivateAndWorkgroupVariables_Basic) {
+  auto* src = R"(
+var<private> p : f32;
+var<workgroup> w : f32;
+
+[[stage(compute)]]
+fn main() {
+  w = p;
+}
+)";
+
+  auto* expect = R"(
+[[stage(compute)]]
+fn main() {
+  [[internal(disable_validation__function_var_storage_class)]] var<workgroup> tint_symbol_1 : f32;
+  let tint_symbol = &(tint_symbol_1);
+  [[internal(disable_validation__function_var_storage_class)]] var<private> tint_symbol_3 : f32;
+  let tint_symbol_2 = &(tint_symbol_3);
+  *(tint_symbol) = *(tint_symbol_2);
+}
+)";
+
+  auto got = Run<Msl>(src);
+
+  EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(MslTest, HandlePrivateAndWorkgroupVariables_FunctionCalls) {
+  auto* src = R"(
+var<private> p : f32;
+var<workgroup> w : f32;
+
+fn no_uses() {
+}
+
+fn bar(a : f32, b : f32) {
+  p = a;
+  w = b;
+}
+
+fn foo(a : f32) {
+  let b : f32 = 2.0;
+  bar(a, b);
+  no_uses();
+}
+
+[[stage(compute)]]
+fn main() {
+  foo(1.0);
+}
+)";
+
+  auto* expect = R"(
+fn no_uses() {
+}
+
+fn bar(a : f32, b : f32, tint_symbol : ptr<private, f32>, tint_symbol_1 : ptr<workgroup, f32>) {
+  *(tint_symbol) = a;
+  *(tint_symbol_1) = b;
+}
+
+fn foo(a : f32, tint_symbol_2 : ptr<private, f32>, tint_symbol_3 : ptr<workgroup, f32>) {
+  let b : f32 = 2.0;
+  bar(a, b, tint_symbol_2, tint_symbol_3);
+  no_uses();
+}
+
+[[stage(compute)]]
+fn main() {
+  [[internal(disable_validation__function_var_storage_class)]] var<private> tint_symbol_5 : f32;
+  let tint_symbol_4 = &(tint_symbol_5);
+  [[internal(disable_validation__function_var_storage_class)]] var<workgroup> tint_symbol_7 : f32;
+  let tint_symbol_6 = &(tint_symbol_7);
+  foo(1.0, tint_symbol_4, tint_symbol_6);
+}
+)";
+
+  auto got = Run<Msl>(src);
+
+  EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(MslTest, HandlePrivateAndWorkgroupVariables_Constructors) {
+  auto* src = R"(
+var<private> a : f32 = 1.0;
+var<private> b : f32 = f32();
+
+[[stage(compute)]]
+fn main() {
+  let x : f32 = a + b;
+}
+)";
+
+  auto* expect = R"(
+[[stage(compute)]]
+fn main() {
+  [[internal(disable_validation__function_var_storage_class)]] var<private> tint_symbol_1 : f32 = 1.0;
+  let tint_symbol = &(tint_symbol_1);
+  [[internal(disable_validation__function_var_storage_class)]] var<private> tint_symbol_3 : f32 = f32();
+  let tint_symbol_2 = &(tint_symbol_3);
+  let x : f32 = (*(tint_symbol) + *(tint_symbol_2));
+}
+)";
+
+  auto got = Run<Msl>(src);
+
+  EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(MslTest, HandlePrivateAndWorkgroupVariables_Pointers) {
+  auto* src = R"(
+var<private> p : f32;
+var<workgroup> w : f32;
+
+[[stage(compute)]]
+fn main() {
+  let p_ptr : ptr<private, f32> = &p;
+  let w_ptr : ptr<workgroup, f32> = &w;
+  let x : f32 = *p_ptr + *w_ptr;
+  *p_ptr = x;
+}
+)";
+
+  auto* expect = R"(
+[[stage(compute)]]
+fn main() {
+  [[internal(disable_validation__function_var_storage_class)]] var<private> tint_symbol_1 : f32;
+  let tint_symbol = &(tint_symbol_1);
+  [[internal(disable_validation__function_var_storage_class)]] var<workgroup> tint_symbol_3 : f32;
+  let tint_symbol_2 = &(tint_symbol_3);
+  let p_ptr : ptr<private, f32> = &(*(tint_symbol));
+  let w_ptr : ptr<workgroup, f32> = &(*(tint_symbol_2));
+  let x : f32 = (*(p_ptr) + *(w_ptr));
+  *(p_ptr) = x;
+}
+)";
+
+  auto got = Run<Msl>(src);
+
+  EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(MslTest, HandlePrivateAndWorkgroupVariables_UnusedVariables) {
+  auto* src = R"(
+var<private> p : f32;
+var<workgroup> w : f32;
+
+[[stage(compute)]]
+fn main() {
+}
+)";
+
+  auto* expect = R"(
+[[stage(compute)]]
+fn main() {
+}
+)";
+
+  auto got = Run<Msl>(src);
+
+  EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(MslTest, HandlePrivateAndWorkgroupVariables_OtherVariables) {
+  auto* src = R"(
+[[block]]
+struct S {
+};
+
+[[group(0), binding(0)]]
+var<uniform> u : S;
+
+[[stage(compute)]]
+fn main() {
+}
+)";
+
+  auto* expect = R"(
+[[block]]
+struct S {
+};
+
+[[group(0), binding(0)]] var<uniform> u : S;
+
+[[stage(compute)]]
+fn main() {
+}
+)";
+
+  auto got = Run<Msl>(src);
+
+  EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(MslTest, HandlePrivateAndWorkgroupVariables_EmtpyModule) {
+  auto* src = "";
+
+  auto got = Run<Msl>(src);
+
+  EXPECT_EQ(src, str(got));
+}
+
 }  // namespace
 }  // namespace transform
 }  // namespace tint
diff --git a/src/writer/msl/generator_impl.cc b/src/writer/msl/generator_impl.cc
index ed64bf9..11ee9a1 100644
--- a/src/writer/msl/generator_impl.cc
+++ b/src/writer/msl/generator_impl.cc
@@ -22,6 +22,7 @@
 #include "src/ast/alias.h"
 #include "src/ast/bool_literal.h"
 #include "src/ast/call_statement.h"
+#include "src/ast/disable_validation_decoration.h"
 #include "src/ast/fallthrough_statement.h"
 #include "src/ast/float_literal.h"
 #include "src/ast/module.h"
@@ -107,9 +108,10 @@
       switch (sem->StorageClass()) {
         case ast::StorageClass::kPrivate:
         case ast::StorageClass::kWorkgroup:
-          TINT_UNIMPLEMENTED(diagnostics_)
-              << "crbug.com/tint/726: module-scope private and workgroup "
-                 "variables not yet implemented";
+          // These are pushed into the entry point by the sanitizer.
+          TINT_ICE(diagnostics_)
+              << "module-scope variables in the private/workgroup storage "
+                 "class should have been handled by the MSL sanitizer";
           break;
         default:
           break;  // Handled by another code path
@@ -2198,11 +2200,28 @@
 
   auto* decl = var->Declaration();
 
-  // TODO(dsinclair): Handle variable decorations
-  if (!decl->decorations().empty()) {
-    diagnostics_.add_error("Variable decorations are not handled yet");
-    return false;
+  for (auto* deco : decl->decorations()) {
+    if (!deco->Is<ast::InternalDecoration>()) {
+      TINT_ICE(diagnostics_) << "unexpected variable decoration";
+      return false;
+    }
   }
+
+  switch (var->StorageClass()) {
+    case ast::StorageClass::kFunction:
+    case ast::StorageClass::kNone:
+      break;
+    case ast::StorageClass::kPrivate:
+      out_ << "thread ";
+      break;
+    case ast::StorageClass::kWorkgroup:
+      out_ << "threadgroup ";
+      break;
+    default:
+      TINT_ICE(diagnostics_) << "unhandled variable storage class";
+      return false;
+  }
+
   auto* type = var->Type()->UnwrapRef();
 
   std::string name = program_->Symbols().NameFor(decl->symbol());
@@ -2225,6 +2244,7 @@
       }
     } else if (var->StorageClass() == ast::StorageClass::kPrivate ||
                var->StorageClass() == ast::StorageClass::kFunction ||
+               var->StorageClass() == ast::StorageClass::kWorkgroup ||
                var->StorageClass() == ast::StorageClass::kNone ||
                var->StorageClass() == ast::StorageClass::kOutput) {
       if (!EmitZeroValue(type)) {
diff --git a/src/writer/msl/generator_impl_type_test.cc b/src/writer/msl/generator_impl_type_test.cc
index 8054ebd..9c682a6 100644
--- a/src/writer/msl/generator_impl_type_test.cc
+++ b/src/writer/msl/generator_impl_type_test.cc
@@ -160,15 +160,14 @@
   EXPECT_EQ(gen.result(), "float2x3");
 }
 
-// TODO(dsinclair): How to annotate as workgroup?
-TEST_F(MslGeneratorImplTest, DISABLED_EmitType_Pointer) {
+TEST_F(MslGeneratorImplTest, EmitType_Pointer) {
   auto* f32 = create<sem::F32>();
   auto* p = create<sem::Pointer>(f32, ast::StorageClass::kWorkgroup);
 
   GeneratorImpl& gen = Build();
 
   ASSERT_TRUE(gen.EmitType(p, "")) << gen.error();
-  EXPECT_EQ(gen.result(), "float*");
+  EXPECT_EQ(gen.result(), "threadgroup float* ");
 }
 
 TEST_F(MslGeneratorImplTest, EmitType_Struct) {
diff --git a/src/writer/msl/generator_impl_variable_decl_statement_test.cc b/src/writer/msl/generator_impl_variable_decl_statement_test.cc
index 487a5a8..f403097 100644
--- a/src/writer/msl/generator_impl_variable_decl_statement_test.cc
+++ b/src/writer/msl/generator_impl_variable_decl_statement_test.cc
@@ -110,34 +110,44 @@
   EXPECT_EQ(gen.result(), "  float3x2 a = float3x2(0.0f);\n");
 }
 
-// TODO(crbug.com/tint/726): module-scope private and workgroup variables not
-// yet implemented
-TEST_F(MslGeneratorImplTest, DISABLED_Emit_VariableDeclStatement_Private) {
+TEST_F(MslGeneratorImplTest, Emit_VariableDeclStatement_Private) {
   Global("a", ty.f32(), ast::StorageClass::kPrivate);
 
   WrapInFunction(Expr("a"));
 
-  GeneratorImpl& gen = Build();
+  GeneratorImpl& gen = SanitizeAndBuild();
 
   gen.increment_indent();
 
   ASSERT_TRUE(gen.Generate()) << gen.error();
-  EXPECT_THAT(gen.result(), HasSubstr("  float a = 0.0f;\n"));
+  EXPECT_THAT(gen.result(), HasSubstr("thread float tint_symbol_2 = 0.0f;\n"));
 }
 
-// TODO(crbug.com/tint/726): module-scope private and workgroup variables not
-// yet implemented
-TEST_F(MslGeneratorImplTest,
-       DISABLED_Emit_VariableDeclStatement_Initializer_Private) {
-  Global("initializer", ty.f32(), ast::StorageClass::kInput);
+TEST_F(MslGeneratorImplTest, Emit_VariableDeclStatement_Initializer_Private) {
+  GlobalConst("initializer", ty.f32(), Expr(0.f));
   Global("a", ty.f32(), ast::StorageClass::kPrivate, Expr("initializer"));
 
   WrapInFunction(Expr("a"));
 
-  GeneratorImpl& gen = Build();
+  GeneratorImpl& gen = SanitizeAndBuild();
 
   ASSERT_TRUE(gen.Generate()) << gen.error();
-  EXPECT_THAT(gen.result(), HasSubstr("float a = initializer;\n"));
+  EXPECT_THAT(gen.result(),
+              HasSubstr("thread float tint_symbol_2 = initializer;\n"));
+}
+
+TEST_F(MslGeneratorImplTest, Emit_VariableDeclStatement_Workgroup) {
+  Global("a", ty.f32(), ast::StorageClass::kWorkgroup);
+
+  WrapInFunction(Expr("a"));
+
+  GeneratorImpl& gen = SanitizeAndBuild();
+
+  gen.increment_indent();
+
+  ASSERT_TRUE(gen.Generate()) << gen.error();
+  EXPECT_THAT(gen.result(),
+              HasSubstr("threadgroup float tint_symbol_2 = 0.0f;\n"));
 }
 
 TEST_F(MslGeneratorImplTest, Emit_VariableDeclStatement_Initializer_ZeroVec) {
diff --git a/test/bug/tint/749.spvasm.expected.msl b/test/bug/tint/749.spvasm.expected.msl
index 1b2d3db..5a7c08c 100644
--- a/test/bug/tint/749.spvasm.expected.msl
+++ b/test/bug/tint/749.spvasm.expected.msl
@@ -1 +1 @@
-SKIP: TINT_UNIMPLEMENTED crbug.com/tint/726: module-scope private and workgroup variables not yet implemented
+SKIP: crbug.com/tint/831
diff --git a/test/ptr_ref/load/global/i32.spvasm.expected.msl b/test/ptr_ref/load/global/i32.spvasm.expected.msl
index 1b2d3db..c59e28e 100644
--- a/test/ptr_ref/load/global/i32.spvasm.expected.msl
+++ b/test/ptr_ref/load/global/i32.spvasm.expected.msl
@@ -1 +1,11 @@
-SKIP: TINT_UNIMPLEMENTED crbug.com/tint/726: module-scope private and workgroup variables not yet implemented
+#include <metal_stdlib>
+
+using namespace metal;
+kernel void tint_symbol() {
+  thread int tint_symbol_2 = 0;
+  thread int* const tint_symbol_1 = &(tint_symbol_2);
+  int const x_9 = *(tint_symbol_1);
+  int const x_11 = (x_9 + 1);
+  return;
+}
+
diff --git a/test/ptr_ref/load/global/i32.wgsl.expected.msl b/test/ptr_ref/load/global/i32.wgsl.expected.msl
index 1b2d3db..9431488 100644
--- a/test/ptr_ref/load/global/i32.wgsl.expected.msl
+++ b/test/ptr_ref/load/global/i32.wgsl.expected.msl
@@ -1 +1,11 @@
-SKIP: TINT_UNIMPLEMENTED crbug.com/tint/726: module-scope private and workgroup variables not yet implemented
+#include <metal_stdlib>
+
+using namespace metal;
+kernel void tint_symbol() {
+  thread int tint_symbol_2 = 0;
+  thread int* const tint_symbol_1 = &(tint_symbol_2);
+  int const i = *(tint_symbol_1);
+  int const use = (i + 1);
+  return;
+}
+
diff --git a/test/ptr_ref/load/global/struct_field.spvasm.expected.msl b/test/ptr_ref/load/global/struct_field.spvasm.expected.msl
index 1b2d3db..5a7c08c 100644
--- a/test/ptr_ref/load/global/struct_field.spvasm.expected.msl
+++ b/test/ptr_ref/load/global/struct_field.spvasm.expected.msl
@@ -1 +1 @@
-SKIP: TINT_UNIMPLEMENTED crbug.com/tint/726: module-scope private and workgroup variables not yet implemented
+SKIP: crbug.com/tint/831
diff --git a/test/ptr_ref/load/global/struct_field.wgsl.expected.msl b/test/ptr_ref/load/global/struct_field.wgsl.expected.msl
index 1b2d3db..5a7c08c 100644
--- a/test/ptr_ref/load/global/struct_field.wgsl.expected.msl
+++ b/test/ptr_ref/load/global/struct_field.wgsl.expected.msl
@@ -1 +1 @@
-SKIP: TINT_UNIMPLEMENTED crbug.com/tint/726: module-scope private and workgroup variables not yet implemented
+SKIP: crbug.com/tint/831
diff --git a/test/ptr_ref/load/local/ptr_private.wgsl.expected.msl b/test/ptr_ref/load/local/ptr_private.wgsl.expected.msl
index 1b2d3db..b41e142 100644
--- a/test/ptr_ref/load/local/ptr_private.wgsl.expected.msl
+++ b/test/ptr_ref/load/local/ptr_private.wgsl.expected.msl
@@ -1 +1,11 @@
-SKIP: TINT_UNIMPLEMENTED crbug.com/tint/726: module-scope private and workgroup variables not yet implemented
+#include <metal_stdlib>
+
+using namespace metal;
+kernel void tint_symbol() {
+  thread int tint_symbol_2 = 123;
+  thread int* const tint_symbol_1 = &(tint_symbol_2);
+  thread int* const p = &(*(tint_symbol_1));
+  int const use = (*(p) + 1);
+  return;
+}
+
diff --git a/test/ptr_ref/load/local/ptr_workgroup.wgsl.expected.msl b/test/ptr_ref/load/local/ptr_workgroup.wgsl.expected.msl
index 1b2d3db..983ba40 100644
--- a/test/ptr_ref/load/local/ptr_workgroup.wgsl.expected.msl
+++ b/test/ptr_ref/load/local/ptr_workgroup.wgsl.expected.msl
@@ -1 +1,12 @@
-SKIP: TINT_UNIMPLEMENTED crbug.com/tint/726: module-scope private and workgroup variables not yet implemented
+#include <metal_stdlib>
+
+using namespace metal;
+kernel void tint_symbol() {
+  threadgroup int tint_symbol_2 = 0;
+  threadgroup int* const tint_symbol_1 = &(tint_symbol_2);
+  *(tint_symbol_1) = 123;
+  threadgroup int* const p = &(*(tint_symbol_1));
+  int const use = (*(p) + 1);
+  return;
+}
+
diff --git a/test/ptr_ref/store/global/i32.spvasm.expected.msl b/test/ptr_ref/store/global/i32.spvasm.expected.msl
index 1b2d3db..92fbc80 100644
--- a/test/ptr_ref/store/global/i32.spvasm.expected.msl
+++ b/test/ptr_ref/store/global/i32.spvasm.expected.msl
@@ -1 +1,11 @@
-SKIP: TINT_UNIMPLEMENTED crbug.com/tint/726: module-scope private and workgroup variables not yet implemented
+#include <metal_stdlib>
+
+using namespace metal;
+kernel void tint_symbol() {
+  thread int tint_symbol_2 = 0;
+  thread int* const tint_symbol_1 = &(tint_symbol_2);
+  *(tint_symbol_1) = 123;
+  *(tint_symbol_1) = ((100 + 20) + 3);
+  return;
+}
+
diff --git a/test/ptr_ref/store/global/i32.wgsl.expected.msl b/test/ptr_ref/store/global/i32.wgsl.expected.msl
index 1b2d3db..92fbc80 100644
--- a/test/ptr_ref/store/global/i32.wgsl.expected.msl
+++ b/test/ptr_ref/store/global/i32.wgsl.expected.msl
@@ -1 +1,11 @@
-SKIP: TINT_UNIMPLEMENTED crbug.com/tint/726: module-scope private and workgroup variables not yet implemented
+#include <metal_stdlib>
+
+using namespace metal;
+kernel void tint_symbol() {
+  thread int tint_symbol_2 = 0;
+  thread int* const tint_symbol_1 = &(tint_symbol_2);
+  *(tint_symbol_1) = 123;
+  *(tint_symbol_1) = ((100 + 20) + 3);
+  return;
+}
+
diff --git a/test/ptr_ref/store/global/struct_field.spvasm.expected.msl b/test/ptr_ref/store/global/struct_field.spvasm.expected.msl
index 1b2d3db..3bde281 100644
--- a/test/ptr_ref/store/global/struct_field.spvasm.expected.msl
+++ b/test/ptr_ref/store/global/struct_field.spvasm.expected.msl
@@ -1 +1 @@
-SKIP: TINT_UNIMPLEMENTED crbug.com/tint/726: module-scope private and workgroup variables not yet implemented
+SKIP: crbug.com/tint/831
\ No newline at end of file
diff --git a/test/types/module_scope_declarations.wgsl b/test/types/module_scope_declarations.wgsl
deleted file mode 100644
index 6be0c4a..0000000
--- a/test/types/module_scope_declarations.wgsl
+++ /dev/null
@@ -1,27 +0,0 @@
-struct S {
-};
-
-var<private> bool_var : bool = bool();
-let bool_let : bool = bool();
-var<private> i32_var : i32 = i32();
-let i32_let : i32 = i32();
-var<private> u32_var : u32 = u32();
-let u32_let : u32 = u32();
-var<private> f32_var : f32 = f32();
-let f32_let : f32 = f32();
-var<private> v2i32_var : vec2<i32> = vec2<i32>();
-let v2i32_let : vec2<i32> = vec2<i32>();
-var<private> v3u32_var : vec3<u32> = vec3<u32>();
-let v3u32_let : vec3<u32> = vec3<u32>();
-var<private> v4f32_var : vec4<f32> = vec4<f32>();
-let v4f32_let : vec4<f32> = vec4<f32>();
-var<private> m2x3_var : mat2x3<f32> = mat2x3<f32>();
-let m3x4_let : mat3x4<f32> = mat3x4<f32>();
-var<private> arr_var : array<f32, 4> = array<f32, 4>();
-let arr_let : array<f32, 4> = array<f32, 4>();
-var<private> struct_var : S = S();
-let struct_let : S = S();
-
-[[stage(compute)]]
-fn main() {
-}
diff --git a/test/types/module_scope_declarations.wgsl.expected.msl b/test/types/module_scope_declarations.wgsl.expected.msl
deleted file mode 100644
index 1b2d3db..0000000
--- a/test/types/module_scope_declarations.wgsl.expected.msl
+++ /dev/null
@@ -1 +0,0 @@
-SKIP: TINT_UNIMPLEMENTED crbug.com/tint/726: module-scope private and workgroup variables not yet implemented
diff --git a/test/types/module_scope_declarations.wgsl.expected.spvasm b/test/types/module_scope_declarations.wgsl.expected.spvasm
deleted file mode 100644
index 9d09d23..0000000
--- a/test/types/module_scope_declarations.wgsl.expected.spvasm
+++ /dev/null
@@ -1,82 +0,0 @@
-; SPIR-V
-; Version: 1.3
-; Generator: Google Tint Compiler; 0
-; Bound: 49
-; Schema: 0
-               OpCapability Shader
-               OpMemoryModel Logical GLSL450
-               OpEntryPoint GLCompute %main "main"
-               OpExecutionMode %main LocalSize 1 1 1
-               OpName %bool_var "bool_var"
-               OpName %bool_let "bool_let"
-               OpName %i32_var "i32_var"
-               OpName %i32_let "i32_let"
-               OpName %u32_var "u32_var"
-               OpName %u32_let "u32_let"
-               OpName %f32_var "f32_var"
-               OpName %f32_let "f32_let"
-               OpName %v2i32_var "v2i32_var"
-               OpName %v2i32_let "v2i32_let"
-               OpName %v3u32_var "v3u32_var"
-               OpName %v3u32_let "v3u32_let"
-               OpName %v4f32_var "v4f32_var"
-               OpName %v4f32_let "v4f32_let"
-               OpName %m2x3_var "m2x3_var"
-               OpName %m3x4_let "m3x4_let"
-               OpName %arr_var "arr_var"
-               OpName %arr_let "arr_let"
-               OpName %S "S"
-               OpName %struct_var "struct_var"
-               OpName %struct_let "struct_let"
-               OpName %main "main"
-               OpDecorate %_arr_float_uint_4 ArrayStride 4
-       %bool = OpTypeBool
-   %bool_let = OpConstantNull %bool
-%_ptr_Private_bool = OpTypePointer Private %bool
-   %bool_var = OpVariable %_ptr_Private_bool Private %bool_let
-        %int = OpTypeInt 32 1
-    %i32_let = OpConstantNull %int
-%_ptr_Private_int = OpTypePointer Private %int
-    %i32_var = OpVariable %_ptr_Private_int Private %i32_let
-       %uint = OpTypeInt 32 0
-    %u32_let = OpConstantNull %uint
-%_ptr_Private_uint = OpTypePointer Private %uint
-    %u32_var = OpVariable %_ptr_Private_uint Private %u32_let
-      %float = OpTypeFloat 32
-    %f32_let = OpConstantNull %float
-%_ptr_Private_float = OpTypePointer Private %float
-    %f32_var = OpVariable %_ptr_Private_float Private %f32_let
-      %v2int = OpTypeVector %int 2
-  %v2i32_let = OpConstantNull %v2int
-%_ptr_Private_v2int = OpTypePointer Private %v2int
-  %v2i32_var = OpVariable %_ptr_Private_v2int Private %v2i32_let
-     %v3uint = OpTypeVector %uint 3
-  %v3u32_let = OpConstantNull %v3uint
-%_ptr_Private_v3uint = OpTypePointer Private %v3uint
-  %v3u32_var = OpVariable %_ptr_Private_v3uint Private %v3u32_let
-    %v4float = OpTypeVector %float 4
-  %v4f32_let = OpConstantNull %v4float
-%_ptr_Private_v4float = OpTypePointer Private %v4float
-  %v4f32_var = OpVariable %_ptr_Private_v4float Private %v4f32_let
-    %v3float = OpTypeVector %float 3
-%mat2v3float = OpTypeMatrix %v3float 2
-         %31 = OpConstantNull %mat2v3float
-%_ptr_Private_mat2v3float = OpTypePointer Private %mat2v3float
-   %m2x3_var = OpVariable %_ptr_Private_mat2v3float Private %31
-%mat3v4float = OpTypeMatrix %v4float 3
-   %m3x4_let = OpConstantNull %mat3v4float
-     %uint_4 = OpConstant %uint 4
-%_arr_float_uint_4 = OpTypeArray %float %uint_4
-    %arr_let = OpConstantNull %_arr_float_uint_4
-%_ptr_Private__arr_float_uint_4 = OpTypePointer Private %_arr_float_uint_4
-    %arr_var = OpVariable %_ptr_Private__arr_float_uint_4 Private %arr_let
-          %S = OpTypeStruct
- %struct_let = OpConstantNull %S
-%_ptr_Private_S = OpTypePointer Private %S
- %struct_var = OpVariable %_ptr_Private_S Private %struct_let
-       %void = OpTypeVoid
-         %45 = OpTypeFunction %void
-       %main = OpFunction %void None %45
-         %48 = OpLabel
-               OpReturn
-               OpFunctionEnd
diff --git a/test/types/module_scope_declarations.wgsl.expected.wgsl b/test/types/module_scope_declarations.wgsl.expected.wgsl
deleted file mode 100644
index e6edc6e..0000000
--- a/test/types/module_scope_declarations.wgsl.expected.wgsl
+++ /dev/null
@@ -1,46 +0,0 @@
-struct S {
-};
-
-var<private> bool_var : bool = bool();
-
-let bool_let : bool = bool();
-
-var<private> i32_var : i32 = i32();
-
-let i32_let : i32 = i32();
-
-var<private> u32_var : u32 = u32();
-
-let u32_let : u32 = u32();
-
-var<private> f32_var : f32 = f32();
-
-let f32_let : f32 = f32();
-
-var<private> v2i32_var : vec2<i32> = vec2<i32>();
-
-let v2i32_let : vec2<i32> = vec2<i32>();
-
-var<private> v3u32_var : vec3<u32> = vec3<u32>();
-
-let v3u32_let : vec3<u32> = vec3<u32>();
-
-var<private> v4f32_var : vec4<f32> = vec4<f32>();
-
-let v4f32_let : vec4<f32> = vec4<f32>();
-
-var<private> m2x3_var : mat2x3<f32> = mat2x3<f32>();
-
-let m3x4_let : mat3x4<f32> = mat3x4<f32>();
-
-var<private> arr_var : array<f32, 4> = array<f32, 4>();
-
-let arr_let : array<f32, 4> = array<f32, 4>();
-
-var<private> struct_var : S = S();
-
-let struct_let : S = S();
-
-[[stage(compute)]]
-fn main() {
-}
diff --git a/test/types/module_scope_let.wgsl b/test/types/module_scope_let.wgsl
new file mode 100644
index 0000000..16181dd
--- /dev/null
+++ b/test/types/module_scope_let.wgsl
@@ -0,0 +1,17 @@
+struct S {
+};
+
+let bool_let : bool = bool();
+let i32_let : i32 = i32();
+let u32_let : u32 = u32();
+let f32_let : f32 = f32();
+let v2i32_let : vec2<i32> = vec2<i32>();
+let v3u32_let : vec3<u32> = vec3<u32>();
+let v4f32_let : vec4<f32> = vec4<f32>();
+let m3x4_let : mat3x4<f32> = mat3x4<f32>();
+let arr_let : array<f32, 4> = array<f32, 4>();
+let struct_let : S = S();
+
+[[stage(compute)]]
+fn main() {
+}
diff --git a/test/types/module_scope_declarations.wgsl.expected.hlsl b/test/types/module_scope_let.wgsl.expected.hlsl
similarity index 100%
rename from test/types/module_scope_declarations.wgsl.expected.hlsl
rename to test/types/module_scope_let.wgsl.expected.hlsl
diff --git a/test/types/module_scope_let.wgsl.expected.msl b/test/types/module_scope_let.wgsl.expected.msl
new file mode 100644
index 0000000..e3a2d4e
--- /dev/null
+++ b/test/types/module_scope_let.wgsl.expected.msl
@@ -0,0 +1,20 @@
+#include <metal_stdlib>
+
+using namespace metal;
+struct S {
+};
+
+constant bool bool_let = bool();
+constant int i32_let = int();
+constant uint u32_let = uint();
+constant float f32_let = float();
+constant int2 v2i32_let = int2();
+constant uint3 v3u32_let = uint3();
+constant float4 v4f32_let = float4();
+constant float3x4 m3x4_let = float3x4();
+constant float arr_let[4] = {};
+constant S struct_let = {};
+kernel void tint_symbol() {
+  return;
+}
+
diff --git a/test/types/module_scope_let.wgsl.expected.spvasm b/test/types/module_scope_let.wgsl.expected.spvasm
new file mode 100644
index 0000000..947c62f
--- /dev/null
+++ b/test/types/module_scope_let.wgsl.expected.spvasm
@@ -0,0 +1,49 @@
+; SPIR-V
+; Version: 1.3
+; Generator: Google Tint Compiler; 0
+; Bound: 26
+; Schema: 0
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+               OpName %bool_let "bool_let"
+               OpName %i32_let "i32_let"
+               OpName %u32_let "u32_let"
+               OpName %f32_let "f32_let"
+               OpName %v2i32_let "v2i32_let"
+               OpName %v3u32_let "v3u32_let"
+               OpName %v4f32_let "v4f32_let"
+               OpName %m3x4_let "m3x4_let"
+               OpName %arr_let "arr_let"
+               OpName %S "S"
+               OpName %struct_let "struct_let"
+               OpName %main "main"
+               OpDecorate %_arr_float_uint_4 ArrayStride 4
+       %bool = OpTypeBool
+   %bool_let = OpConstantNull %bool
+        %int = OpTypeInt 32 1
+    %i32_let = OpConstantNull %int
+       %uint = OpTypeInt 32 0
+    %u32_let = OpConstantNull %uint
+      %float = OpTypeFloat 32
+    %f32_let = OpConstantNull %float
+      %v2int = OpTypeVector %int 2
+  %v2i32_let = OpConstantNull %v2int
+     %v3uint = OpTypeVector %uint 3
+  %v3u32_let = OpConstantNull %v3uint
+    %v4float = OpTypeVector %float 4
+  %v4f32_let = OpConstantNull %v4float
+%mat3v4float = OpTypeMatrix %v4float 3
+   %m3x4_let = OpConstantNull %mat3v4float
+     %uint_4 = OpConstant %uint 4
+%_arr_float_uint_4 = OpTypeArray %float %uint_4
+    %arr_let = OpConstantNull %_arr_float_uint_4
+          %S = OpTypeStruct
+ %struct_let = OpConstantNull %S
+       %void = OpTypeVoid
+         %22 = OpTypeFunction %void
+       %main = OpFunction %void None %22
+         %25 = OpLabel
+               OpReturn
+               OpFunctionEnd
diff --git a/test/types/module_scope_let.wgsl.expected.wgsl b/test/types/module_scope_let.wgsl.expected.wgsl
new file mode 100644
index 0000000..eebe89e
--- /dev/null
+++ b/test/types/module_scope_let.wgsl.expected.wgsl
@@ -0,0 +1,26 @@
+struct S {
+};
+
+let bool_let : bool = bool();
+
+let i32_let : i32 = i32();
+
+let u32_let : u32 = u32();
+
+let f32_let : f32 = f32();
+
+let v2i32_let : vec2<i32> = vec2<i32>();
+
+let v3u32_let : vec3<u32> = vec3<u32>();
+
+let v4f32_let : vec4<f32> = vec4<f32>();
+
+let m3x4_let : mat3x4<f32> = mat3x4<f32>();
+
+let arr_let : array<f32, 4> = array<f32, 4>();
+
+let struct_let : S = S();
+
+[[stage(compute)]]
+fn main() {
+}
diff --git a/test/types/module_scope_var.wgsl b/test/types/module_scope_var.wgsl
new file mode 100644
index 0000000..c2442e4
--- /dev/null
+++ b/test/types/module_scope_var.wgsl
@@ -0,0 +1,28 @@
+struct S {
+};
+
+var<private> bool_var : bool;
+var<private> i32_var : i32;
+var<private> u32_var : u32;
+var<private> f32_var : f32;
+var<private> v2i32_var : vec2<i32>;
+var<private> v3u32_var : vec3<u32>;
+var<private> v4f32_var : vec4<f32>;
+var<private> m2x3_var : mat2x3<f32>;
+var<private> arr_var : array<f32, 4>;
+var<private> struct_var : S;
+
+[[stage(compute)]]
+fn main() {
+  // Reference the module-scope variables to stop them from being removed.
+  bool_var = bool();
+  i32_var = i32();
+  u32_var = u32();
+  f32_var = f32();
+  v2i32_var = vec2<i32>();
+  v3u32_var = vec3<u32>();
+  v4f32_var = vec4<f32>();
+  m2x3_var = mat2x3<f32>();
+  arr_var = array<f32, 4>();
+  struct_var = S();
+}
diff --git a/test/types/module_scope_var.wgsl.expected.hlsl b/test/types/module_scope_var.wgsl.expected.hlsl
new file mode 100644
index 0000000..b28e796
--- /dev/null
+++ b/test/types/module_scope_var.wgsl.expected.hlsl
@@ -0,0 +1,31 @@
+struct S {
+};
+
+static bool bool_var;
+static int i32_var;
+static uint u32_var;
+static float f32_var;
+static int2 v2i32_var;
+static uint3 v3u32_var;
+static float4 v4f32_var;
+static float2x3 m2x3_var;
+static float arr_var[4];
+static S struct_var;
+
+[numthreads(1, 1, 1)]
+void main() {
+  bool_var = false;
+  i32_var = 0;
+  u32_var = 0u;
+  f32_var = 0.0f;
+  v2i32_var = int2(0, 0);
+  v3u32_var = uint3(0u, 0u, 0u);
+  v4f32_var = float4(0.0f, 0.0f, 0.0f, 0.0f);
+  m2x3_var = float2x3(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f);
+  const float tint_symbol[4] = {0.0f, 0.0f, 0.0f, 0.0f};
+  arr_var = tint_symbol;
+  const S tint_symbol_1 = {};
+  struct_var = tint_symbol_1;
+  return;
+}
+
diff --git a/test/types/module_scope_var.wgsl.expected.msl b/test/types/module_scope_var.wgsl.expected.msl
new file mode 100644
index 0000000..77696be
--- /dev/null
+++ b/test/types/module_scope_var.wgsl.expected.msl
@@ -0,0 +1 @@
+SKIP: crbug.com/tint/814
diff --git a/test/types/module_scope_var.wgsl.expected.spvasm b/test/types/module_scope_var.wgsl.expected.spvasm
new file mode 100644
index 0000000..2fa75f1
--- /dev/null
+++ b/test/types/module_scope_var.wgsl.expected.spvasm
@@ -0,0 +1,80 @@
+; SPIR-V
+; Version: 1.3
+; Generator: Google Tint Compiler; 0
+; Bound: 47
+; Schema: 0
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+               OpName %bool_var "bool_var"
+               OpName %i32_var "i32_var"
+               OpName %u32_var "u32_var"
+               OpName %f32_var "f32_var"
+               OpName %v2i32_var "v2i32_var"
+               OpName %v3u32_var "v3u32_var"
+               OpName %v4f32_var "v4f32_var"
+               OpName %m2x3_var "m2x3_var"
+               OpName %arr_var "arr_var"
+               OpName %S "S"
+               OpName %struct_var "struct_var"
+               OpName %main "main"
+               OpDecorate %_arr_float_uint_4 ArrayStride 4
+       %bool = OpTypeBool
+%_ptr_Private_bool = OpTypePointer Private %bool
+          %4 = OpConstantNull %bool
+   %bool_var = OpVariable %_ptr_Private_bool Private %4
+        %int = OpTypeInt 32 1
+%_ptr_Private_int = OpTypePointer Private %int
+          %8 = OpConstantNull %int
+    %i32_var = OpVariable %_ptr_Private_int Private %8
+       %uint = OpTypeInt 32 0
+%_ptr_Private_uint = OpTypePointer Private %uint
+         %12 = OpConstantNull %uint
+    %u32_var = OpVariable %_ptr_Private_uint Private %12
+      %float = OpTypeFloat 32
+%_ptr_Private_float = OpTypePointer Private %float
+         %16 = OpConstantNull %float
+    %f32_var = OpVariable %_ptr_Private_float Private %16
+      %v2int = OpTypeVector %int 2
+%_ptr_Private_v2int = OpTypePointer Private %v2int
+         %20 = OpConstantNull %v2int
+  %v2i32_var = OpVariable %_ptr_Private_v2int Private %20
+     %v3uint = OpTypeVector %uint 3
+%_ptr_Private_v3uint = OpTypePointer Private %v3uint
+         %24 = OpConstantNull %v3uint
+  %v3u32_var = OpVariable %_ptr_Private_v3uint Private %24
+    %v4float = OpTypeVector %float 4
+%_ptr_Private_v4float = OpTypePointer Private %v4float
+         %28 = OpConstantNull %v4float
+  %v4f32_var = OpVariable %_ptr_Private_v4float Private %28
+    %v3float = OpTypeVector %float 3
+%mat2v3float = OpTypeMatrix %v3float 2
+%_ptr_Private_mat2v3float = OpTypePointer Private %mat2v3float
+         %33 = OpConstantNull %mat2v3float
+   %m2x3_var = OpVariable %_ptr_Private_mat2v3float Private %33
+     %uint_4 = OpConstant %uint 4
+%_arr_float_uint_4 = OpTypeArray %float %uint_4
+%_ptr_Private__arr_float_uint_4 = OpTypePointer Private %_arr_float_uint_4
+         %38 = OpConstantNull %_arr_float_uint_4
+    %arr_var = OpVariable %_ptr_Private__arr_float_uint_4 Private %38
+          %S = OpTypeStruct
+%_ptr_Private_S = OpTypePointer Private %S
+         %42 = OpConstantNull %S
+ %struct_var = OpVariable %_ptr_Private_S Private %42
+       %void = OpTypeVoid
+         %43 = OpTypeFunction %void
+       %main = OpFunction %void None %43
+         %46 = OpLabel
+               OpStore %bool_var %4
+               OpStore %i32_var %8
+               OpStore %u32_var %12
+               OpStore %f32_var %16
+               OpStore %v2i32_var %20
+               OpStore %v3u32_var %24
+               OpStore %v4f32_var %28
+               OpStore %m2x3_var %33
+               OpStore %arr_var %38
+               OpStore %struct_var %42
+               OpReturn
+               OpFunctionEnd
diff --git a/test/types/module_scope_var.wgsl.expected.wgsl b/test/types/module_scope_var.wgsl.expected.wgsl
new file mode 100644
index 0000000..7e53714
--- /dev/null
+++ b/test/types/module_scope_var.wgsl.expected.wgsl
@@ -0,0 +1,36 @@
+struct S {
+};
+
+var<private> bool_var : bool;
+
+var<private> i32_var : i32;
+
+var<private> u32_var : u32;
+
+var<private> f32_var : f32;
+
+var<private> v2i32_var : vec2<i32>;
+
+var<private> v3u32_var : vec3<u32>;
+
+var<private> v4f32_var : vec4<f32>;
+
+var<private> m2x3_var : mat2x3<f32>;
+
+var<private> arr_var : array<f32, 4>;
+
+var<private> struct_var : S;
+
+[[stage(compute)]]
+fn main() {
+  bool_var = bool();
+  i32_var = i32();
+  u32_var = u32();
+  f32_var = f32();
+  v2i32_var = vec2<i32>();
+  v3u32_var = vec3<u32>();
+  v4f32_var = vec4<f32>();
+  m2x3_var = mat2x3<f32>();
+  arr_var = array<f32, 4>();
+  struct_var = S();
+}
diff --git a/test/types/module_scope_var_initializers.wgsl b/test/types/module_scope_var_initializers.wgsl
new file mode 100644
index 0000000..3994d12
--- /dev/null
+++ b/test/types/module_scope_var_initializers.wgsl
@@ -0,0 +1,28 @@
+struct S {
+};
+
+var<private> bool_var : bool = bool();
+var<private> i32_var : i32 = i32();
+var<private> u32_var : u32 = u32();
+var<private> f32_var : f32 = f32();
+var<private> v2i32_var : vec2<i32> = vec2<i32>();
+var<private> v3u32_var : vec3<u32> = vec3<u32>();
+var<private> v4f32_var : vec4<f32> = vec4<f32>();
+var<private> m2x3_var : mat2x3<f32> = mat2x3<f32>();
+var<private> arr_var : array<f32, 4> = array<f32, 4>();
+var<private> struct_var : S = S();
+
+[[stage(compute)]]
+fn main() {
+  // Reference the module-scope variables to stop them from being removed.
+  bool_var = bool();
+  i32_var = i32();
+  u32_var = u32();
+  f32_var = f32();
+  v2i32_var = vec2<i32>();
+  v3u32_var = vec3<u32>();
+  v4f32_var = vec4<f32>();
+  m2x3_var = mat2x3<f32>();
+  arr_var = array<f32, 4>();
+  struct_var = S();
+}
diff --git a/test/types/module_scope_var_initializers.wgsl.expected.hlsl b/test/types/module_scope_var_initializers.wgsl.expected.hlsl
new file mode 100644
index 0000000..562a445
--- /dev/null
+++ b/test/types/module_scope_var_initializers.wgsl.expected.hlsl
@@ -0,0 +1,31 @@
+struct S {
+};
+
+static bool bool_var = false;
+static int i32_var = 0;
+static uint u32_var = 0u;
+static float f32_var = 0.0f;
+static int2 v2i32_var = int2(0, 0);
+static uint3 v3u32_var = uint3(0u, 0u, 0u);
+static float4 v4f32_var = float4(0.0f, 0.0f, 0.0f, 0.0f);
+static float2x3 m2x3_var = float2x3(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f);
+static float arr_var[4] = {0.0f, 0.0f, 0.0f, 0.0f};
+static S struct_var = {};
+
+[numthreads(1, 1, 1)]
+void main() {
+  bool_var = false;
+  i32_var = 0;
+  u32_var = 0u;
+  f32_var = 0.0f;
+  v2i32_var = int2(0, 0);
+  v3u32_var = uint3(0u, 0u, 0u);
+  v4f32_var = float4(0.0f, 0.0f, 0.0f, 0.0f);
+  m2x3_var = float2x3(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f);
+  const float tint_symbol[4] = {0.0f, 0.0f, 0.0f, 0.0f};
+  arr_var = tint_symbol;
+  const S tint_symbol_1 = {};
+  struct_var = tint_symbol_1;
+  return;
+}
+
diff --git a/test/types/module_scope_var_initializers.wgsl.expected.msl b/test/types/module_scope_var_initializers.wgsl.expected.msl
new file mode 100644
index 0000000..77696be
--- /dev/null
+++ b/test/types/module_scope_var_initializers.wgsl.expected.msl
@@ -0,0 +1 @@
+SKIP: crbug.com/tint/814
diff --git a/test/types/module_scope_var_initializers.wgsl.expected.spvasm b/test/types/module_scope_var_initializers.wgsl.expected.spvasm
new file mode 100644
index 0000000..0fb2909
--- /dev/null
+++ b/test/types/module_scope_var_initializers.wgsl.expected.spvasm
@@ -0,0 +1,80 @@
+; SPIR-V
+; Version: 1.3
+; Generator: Google Tint Compiler; 0
+; Bound: 47
+; Schema: 0
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+               OpName %bool_var "bool_var"
+               OpName %i32_var "i32_var"
+               OpName %u32_var "u32_var"
+               OpName %f32_var "f32_var"
+               OpName %v2i32_var "v2i32_var"
+               OpName %v3u32_var "v3u32_var"
+               OpName %v4f32_var "v4f32_var"
+               OpName %m2x3_var "m2x3_var"
+               OpName %arr_var "arr_var"
+               OpName %S "S"
+               OpName %struct_var "struct_var"
+               OpName %main "main"
+               OpDecorate %_arr_float_uint_4 ArrayStride 4
+       %bool = OpTypeBool
+          %2 = OpConstantNull %bool
+%_ptr_Private_bool = OpTypePointer Private %bool
+   %bool_var = OpVariable %_ptr_Private_bool Private %2
+        %int = OpTypeInt 32 1
+          %6 = OpConstantNull %int
+%_ptr_Private_int = OpTypePointer Private %int
+    %i32_var = OpVariable %_ptr_Private_int Private %6
+       %uint = OpTypeInt 32 0
+         %10 = OpConstantNull %uint
+%_ptr_Private_uint = OpTypePointer Private %uint
+    %u32_var = OpVariable %_ptr_Private_uint Private %10
+      %float = OpTypeFloat 32
+         %14 = OpConstantNull %float
+%_ptr_Private_float = OpTypePointer Private %float
+    %f32_var = OpVariable %_ptr_Private_float Private %14
+      %v2int = OpTypeVector %int 2
+         %18 = OpConstantNull %v2int
+%_ptr_Private_v2int = OpTypePointer Private %v2int
+  %v2i32_var = OpVariable %_ptr_Private_v2int Private %18
+     %v3uint = OpTypeVector %uint 3
+         %22 = OpConstantNull %v3uint
+%_ptr_Private_v3uint = OpTypePointer Private %v3uint
+  %v3u32_var = OpVariable %_ptr_Private_v3uint Private %22
+    %v4float = OpTypeVector %float 4
+         %26 = OpConstantNull %v4float
+%_ptr_Private_v4float = OpTypePointer Private %v4float
+  %v4f32_var = OpVariable %_ptr_Private_v4float Private %26
+    %v3float = OpTypeVector %float 3
+%mat2v3float = OpTypeMatrix %v3float 2
+         %31 = OpConstantNull %mat2v3float
+%_ptr_Private_mat2v3float = OpTypePointer Private %mat2v3float
+   %m2x3_var = OpVariable %_ptr_Private_mat2v3float Private %31
+     %uint_4 = OpConstant %uint 4
+%_arr_float_uint_4 = OpTypeArray %float %uint_4
+         %36 = OpConstantNull %_arr_float_uint_4
+%_ptr_Private__arr_float_uint_4 = OpTypePointer Private %_arr_float_uint_4
+    %arr_var = OpVariable %_ptr_Private__arr_float_uint_4 Private %36
+          %S = OpTypeStruct
+         %40 = OpConstantNull %S
+%_ptr_Private_S = OpTypePointer Private %S
+ %struct_var = OpVariable %_ptr_Private_S Private %40
+       %void = OpTypeVoid
+         %43 = OpTypeFunction %void
+       %main = OpFunction %void None %43
+         %46 = OpLabel
+               OpStore %bool_var %2
+               OpStore %i32_var %6
+               OpStore %u32_var %10
+               OpStore %f32_var %14
+               OpStore %v2i32_var %18
+               OpStore %v3u32_var %22
+               OpStore %v4f32_var %26
+               OpStore %m2x3_var %31
+               OpStore %arr_var %36
+               OpStore %struct_var %40
+               OpReturn
+               OpFunctionEnd
diff --git a/test/types/module_scope_var_initializers.wgsl.expected.wgsl b/test/types/module_scope_var_initializers.wgsl.expected.wgsl
new file mode 100644
index 0000000..7b16c09
--- /dev/null
+++ b/test/types/module_scope_var_initializers.wgsl.expected.wgsl
@@ -0,0 +1,36 @@
+struct S {
+};
+
+var<private> bool_var : bool = bool();
+
+var<private> i32_var : i32 = i32();
+
+var<private> u32_var : u32 = u32();
+
+var<private> f32_var : f32 = f32();
+
+var<private> v2i32_var : vec2<i32> = vec2<i32>();
+
+var<private> v3u32_var : vec3<u32> = vec3<u32>();
+
+var<private> v4f32_var : vec4<f32> = vec4<f32>();
+
+var<private> m2x3_var : mat2x3<f32> = mat2x3<f32>();
+
+var<private> arr_var : array<f32, 4> = array<f32, 4>();
+
+var<private> struct_var : S = S();
+
+[[stage(compute)]]
+fn main() {
+  bool_var = bool();
+  i32_var = i32();
+  u32_var = u32();
+  f32_var = f32();
+  v2i32_var = vec2<i32>();
+  v3u32_var = vec3<u32>();
+  v4f32_var = vec4<f32>();
+  m2x3_var = mat2x3<f32>();
+  arr_var = array<f32, 4>();
+  struct_var = S();
+}
diff --git a/test/var/private.wgsl b/test/var/private.wgsl
new file mode 100644
index 0000000..e03db87
--- /dev/null
+++ b/test/var/private.wgsl
@@ -0,0 +1,49 @@
+var<private> a : i32;
+var<private> b : i32;
+var<private> c : i32; // unused
+
+fn uses_a() {
+  a = a + 1;
+}
+
+fn uses_b() {
+  b = b * 2;
+}
+
+fn uses_a_and_b() {
+  b = a;
+}
+
+fn no_uses() {
+}
+
+fn outer() {
+  a = 0;
+  uses_a();
+  uses_a_and_b();
+  uses_b();
+  no_uses();
+}
+
+[[stage(compute)]]
+fn main1() {
+  a = 42;
+  uses_a();
+}
+
+[[stage(compute)]]
+fn main2() {
+  b = 7;
+  uses_b();
+}
+
+[[stage(compute)]]
+fn main3() {
+  outer();
+  no_uses();
+}
+
+[[stage(compute)]]
+fn main4() {
+  no_uses();
+}
diff --git a/test/var/private.wgsl.expected.hlsl b/test/var/private.wgsl.expected.hlsl
new file mode 100644
index 0000000..f30875e
--- /dev/null
+++ b/test/var/private.wgsl.expected.hlsl
@@ -0,0 +1,54 @@
+static int a;
+
+static int b;
+
+void uses_a() {
+  a = (a + 1);
+}
+
+void uses_b() {
+  b = (b * 2);
+}
+
+void uses_a_and_b() {
+  b = a;
+}
+
+void no_uses() {
+}
+
+void outer() {
+  a = 0;
+  uses_a();
+  uses_a_and_b();
+  uses_b();
+  no_uses();
+}
+
+[numthreads(1, 1, 1)]
+void main1() {
+  a = 42;
+  uses_a();
+  return;
+}
+
+[numthreads(1, 1, 1)]
+void main2() {
+  b = 7;
+  uses_b();
+  return;
+}
+
+[numthreads(1, 1, 1)]
+void main3() {
+  outer();
+  no_uses();
+  return;
+}
+
+[numthreads(1, 1, 1)]
+void main4() {
+  no_uses();
+  return;
+}
+
diff --git a/test/var/private.wgsl.expected.msl b/test/var/private.wgsl.expected.msl
new file mode 100644
index 0000000..f17afeb
--- /dev/null
+++ b/test/var/private.wgsl.expected.msl
@@ -0,0 +1,57 @@
+#include <metal_stdlib>
+
+using namespace metal;
+void uses_a(thread int* const tint_symbol) {
+  *(tint_symbol) = (*(tint_symbol) + 1);
+}
+
+void uses_b(thread int* const tint_symbol_1) {
+  *(tint_symbol_1) = (*(tint_symbol_1) * 2);
+}
+
+void uses_a_and_b(thread int* const tint_symbol_2, thread int* const tint_symbol_3) {
+  *(tint_symbol_2) = *(tint_symbol_3);
+}
+
+void no_uses() {
+}
+
+void outer(thread int* const tint_symbol_4, thread int* const tint_symbol_5) {
+  *(tint_symbol_4) = 0;
+  uses_a(tint_symbol_4);
+  uses_a_and_b(tint_symbol_5, tint_symbol_4);
+  uses_b(tint_symbol_5);
+  no_uses();
+}
+
+kernel void main1() {
+  thread int tint_symbol_7 = 0;
+  thread int* const tint_symbol_6 = &(tint_symbol_7);
+  *(tint_symbol_6) = 42;
+  uses_a(tint_symbol_6);
+  return;
+}
+
+kernel void main2() {
+  thread int tint_symbol_9 = 0;
+  thread int* const tint_symbol_8 = &(tint_symbol_9);
+  *(tint_symbol_8) = 7;
+  uses_b(tint_symbol_8);
+  return;
+}
+
+kernel void main3() {
+  thread int tint_symbol_11 = 0;
+  thread int* const tint_symbol_10 = &(tint_symbol_11);
+  thread int tint_symbol_13 = 0;
+  thread int* const tint_symbol_12 = &(tint_symbol_13);
+  outer(tint_symbol_10, tint_symbol_12);
+  no_uses();
+  return;
+}
+
+kernel void main4() {
+  no_uses();
+  return;
+}
+
diff --git a/test/var/private.wgsl.expected.spvasm b/test/var/private.wgsl.expected.spvasm
new file mode 100644
index 0000000..daa3cfb
--- /dev/null
+++ b/test/var/private.wgsl.expected.spvasm
@@ -0,0 +1,96 @@
+; SPIR-V
+; Version: 1.3
+; Generator: Google Tint Compiler; 0
+; Bound: 46
+; Schema: 0
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main1 "main1"
+               OpEntryPoint GLCompute %main2 "main2"
+               OpEntryPoint GLCompute %main3 "main3"
+               OpEntryPoint GLCompute %main4 "main4"
+               OpExecutionMode %main1 LocalSize 1 1 1
+               OpExecutionMode %main2 LocalSize 1 1 1
+               OpExecutionMode %main3 LocalSize 1 1 1
+               OpExecutionMode %main4 LocalSize 1 1 1
+               OpName %a "a"
+               OpName %b "b"
+               OpName %c "c"
+               OpName %uses_a "uses_a"
+               OpName %uses_b "uses_b"
+               OpName %uses_a_and_b "uses_a_and_b"
+               OpName %no_uses "no_uses"
+               OpName %outer "outer"
+               OpName %main1 "main1"
+               OpName %main2 "main2"
+               OpName %main3 "main3"
+               OpName %main4 "main4"
+        %int = OpTypeInt 32 1
+%_ptr_Private_int = OpTypePointer Private %int
+          %4 = OpConstantNull %int
+          %a = OpVariable %_ptr_Private_int Private %4
+          %b = OpVariable %_ptr_Private_int Private %4
+          %c = OpVariable %_ptr_Private_int Private %4
+       %void = OpTypeVoid
+          %7 = OpTypeFunction %void
+      %int_1 = OpConstant %int 1
+      %int_2 = OpConstant %int 2
+      %int_0 = OpConstant %int 0
+     %int_42 = OpConstant %int 42
+      %int_7 = OpConstant %int 7
+     %uses_a = OpFunction %void None %7
+         %10 = OpLabel
+         %11 = OpLoad %int %a
+         %13 = OpIAdd %int %11 %int_1
+               OpStore %a %13
+               OpReturn
+               OpFunctionEnd
+     %uses_b = OpFunction %void None %7
+         %15 = OpLabel
+         %16 = OpLoad %int %b
+         %18 = OpIMul %int %16 %int_2
+               OpStore %b %18
+               OpReturn
+               OpFunctionEnd
+%uses_a_and_b = OpFunction %void None %7
+         %20 = OpLabel
+         %21 = OpLoad %int %a
+               OpStore %b %21
+               OpReturn
+               OpFunctionEnd
+    %no_uses = OpFunction %void None %7
+         %23 = OpLabel
+               OpReturn
+               OpFunctionEnd
+      %outer = OpFunction %void None %7
+         %25 = OpLabel
+               OpStore %a %int_0
+         %27 = OpFunctionCall %void %uses_a
+         %28 = OpFunctionCall %void %uses_a_and_b
+         %29 = OpFunctionCall %void %uses_b
+         %30 = OpFunctionCall %void %no_uses
+               OpReturn
+               OpFunctionEnd
+      %main1 = OpFunction %void None %7
+         %32 = OpLabel
+               OpStore %a %int_42
+         %34 = OpFunctionCall %void %uses_a
+               OpReturn
+               OpFunctionEnd
+      %main2 = OpFunction %void None %7
+         %36 = OpLabel
+               OpStore %b %int_7
+         %38 = OpFunctionCall %void %uses_b
+               OpReturn
+               OpFunctionEnd
+      %main3 = OpFunction %void None %7
+         %40 = OpLabel
+         %41 = OpFunctionCall %void %outer
+         %42 = OpFunctionCall %void %no_uses
+               OpReturn
+               OpFunctionEnd
+      %main4 = OpFunction %void None %7
+         %44 = OpLabel
+         %45 = OpFunctionCall %void %no_uses
+               OpReturn
+               OpFunctionEnd
diff --git a/test/var/private.wgsl.expected.wgsl b/test/var/private.wgsl.expected.wgsl
new file mode 100644
index 0000000..ef20cc2
--- /dev/null
+++ b/test/var/private.wgsl.expected.wgsl
@@ -0,0 +1,51 @@
+var<private> a : i32;
+
+var<private> b : i32;
+
+var<private> c : i32;
+
+fn uses_a() {
+  a = (a + 1);
+}
+
+fn uses_b() {
+  b = (b * 2);
+}
+
+fn uses_a_and_b() {
+  b = a;
+}
+
+fn no_uses() {
+}
+
+fn outer() {
+  a = 0;
+  uses_a();
+  uses_a_and_b();
+  uses_b();
+  no_uses();
+}
+
+[[stage(compute)]]
+fn main1() {
+  a = 42;
+  uses_a();
+}
+
+[[stage(compute)]]
+fn main2() {
+  b = 7;
+  uses_b();
+}
+
+[[stage(compute)]]
+fn main3() {
+  outer();
+  no_uses();
+}
+
+[[stage(compute)]]
+fn main4() {
+  no_uses();
+}
diff --git a/test/var/workgroup.wgsl b/test/var/workgroup.wgsl
new file mode 100644
index 0000000..1d0ba40
--- /dev/null
+++ b/test/var/workgroup.wgsl
@@ -0,0 +1,49 @@
+var<workgroup> a : i32;
+var<workgroup> b : i32;
+var<workgroup> c : i32; // unused
+
+fn uses_a() {
+  a = a + 1;
+}
+
+fn uses_b() {
+  b = b * 2;
+}
+
+fn uses_a_and_b() {
+  b = a;
+}
+
+fn no_uses() {
+}
+
+fn outer() {
+  a = 0;
+  uses_a();
+  uses_a_and_b();
+  uses_b();
+  no_uses();
+}
+
+[[stage(compute)]]
+fn main1() {
+  a = 42;
+  uses_a();
+}
+
+[[stage(compute)]]
+fn main2() {
+  b = 7;
+  uses_b();
+}
+
+[[stage(compute)]]
+fn main3() {
+  outer();
+  no_uses();
+}
+
+[[stage(compute)]]
+fn main4() {
+  no_uses();
+}
diff --git a/test/var/workgroup.wgsl.expected.hlsl b/test/var/workgroup.wgsl.expected.hlsl
new file mode 100644
index 0000000..83c32ca
--- /dev/null
+++ b/test/var/workgroup.wgsl.expected.hlsl
@@ -0,0 +1,54 @@
+groupshared int a;
+
+groupshared int b;
+
+void uses_a() {
+  a = (a + 1);
+}
+
+void uses_b() {
+  b = (b * 2);
+}
+
+void uses_a_and_b() {
+  b = a;
+}
+
+void no_uses() {
+}
+
+void outer() {
+  a = 0;
+  uses_a();
+  uses_a_and_b();
+  uses_b();
+  no_uses();
+}
+
+[numthreads(1, 1, 1)]
+void main1() {
+  a = 42;
+  uses_a();
+  return;
+}
+
+[numthreads(1, 1, 1)]
+void main2() {
+  b = 7;
+  uses_b();
+  return;
+}
+
+[numthreads(1, 1, 1)]
+void main3() {
+  outer();
+  no_uses();
+  return;
+}
+
+[numthreads(1, 1, 1)]
+void main4() {
+  no_uses();
+  return;
+}
+
diff --git a/test/var/workgroup.wgsl.expected.msl b/test/var/workgroup.wgsl.expected.msl
new file mode 100644
index 0000000..3c53df8
--- /dev/null
+++ b/test/var/workgroup.wgsl.expected.msl
@@ -0,0 +1,57 @@
+#include <metal_stdlib>
+
+using namespace metal;
+void uses_a(threadgroup int* const tint_symbol) {
+  *(tint_symbol) = (*(tint_symbol) + 1);
+}
+
+void uses_b(threadgroup int* const tint_symbol_1) {
+  *(tint_symbol_1) = (*(tint_symbol_1) * 2);
+}
+
+void uses_a_and_b(threadgroup int* const tint_symbol_2, threadgroup int* const tint_symbol_3) {
+  *(tint_symbol_2) = *(tint_symbol_3);
+}
+
+void no_uses() {
+}
+
+void outer(threadgroup int* const tint_symbol_4, threadgroup int* const tint_symbol_5) {
+  *(tint_symbol_4) = 0;
+  uses_a(tint_symbol_4);
+  uses_a_and_b(tint_symbol_5, tint_symbol_4);
+  uses_b(tint_symbol_5);
+  no_uses();
+}
+
+kernel void main1() {
+  threadgroup int tint_symbol_7 = 0;
+  threadgroup int* const tint_symbol_6 = &(tint_symbol_7);
+  *(tint_symbol_6) = 42;
+  uses_a(tint_symbol_6);
+  return;
+}
+
+kernel void main2() {
+  threadgroup int tint_symbol_9 = 0;
+  threadgroup int* const tint_symbol_8 = &(tint_symbol_9);
+  *(tint_symbol_8) = 7;
+  uses_b(tint_symbol_8);
+  return;
+}
+
+kernel void main3() {
+  threadgroup int tint_symbol_11 = 0;
+  threadgroup int* const tint_symbol_10 = &(tint_symbol_11);
+  threadgroup int tint_symbol_13 = 0;
+  threadgroup int* const tint_symbol_12 = &(tint_symbol_13);
+  outer(tint_symbol_10, tint_symbol_12);
+  no_uses();
+  return;
+}
+
+kernel void main4() {
+  no_uses();
+  return;
+}
+
diff --git a/test/var/workgroup.wgsl.expected.spvasm b/test/var/workgroup.wgsl.expected.spvasm
new file mode 100644
index 0000000..66c1f0c
--- /dev/null
+++ b/test/var/workgroup.wgsl.expected.spvasm
@@ -0,0 +1,95 @@
+; SPIR-V
+; Version: 1.3
+; Generator: Google Tint Compiler; 0
+; Bound: 45
+; Schema: 0
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main1 "main1"
+               OpEntryPoint GLCompute %main2 "main2"
+               OpEntryPoint GLCompute %main3 "main3"
+               OpEntryPoint GLCompute %main4 "main4"
+               OpExecutionMode %main1 LocalSize 1 1 1
+               OpExecutionMode %main2 LocalSize 1 1 1
+               OpExecutionMode %main3 LocalSize 1 1 1
+               OpExecutionMode %main4 LocalSize 1 1 1
+               OpName %a "a"
+               OpName %b "b"
+               OpName %c "c"
+               OpName %uses_a "uses_a"
+               OpName %uses_b "uses_b"
+               OpName %uses_a_and_b "uses_a_and_b"
+               OpName %no_uses "no_uses"
+               OpName %outer "outer"
+               OpName %main1 "main1"
+               OpName %main2 "main2"
+               OpName %main3 "main3"
+               OpName %main4 "main4"
+        %int = OpTypeInt 32 1
+%_ptr_Workgroup_int = OpTypePointer Workgroup %int
+          %a = OpVariable %_ptr_Workgroup_int Workgroup
+          %b = OpVariable %_ptr_Workgroup_int Workgroup
+          %c = OpVariable %_ptr_Workgroup_int Workgroup
+       %void = OpTypeVoid
+          %6 = OpTypeFunction %void
+      %int_1 = OpConstant %int 1
+      %int_2 = OpConstant %int 2
+      %int_0 = OpConstant %int 0
+     %int_42 = OpConstant %int 42
+      %int_7 = OpConstant %int 7
+     %uses_a = OpFunction %void None %6
+          %9 = OpLabel
+         %10 = OpLoad %int %a
+         %12 = OpIAdd %int %10 %int_1
+               OpStore %a %12
+               OpReturn
+               OpFunctionEnd
+     %uses_b = OpFunction %void None %6
+         %14 = OpLabel
+         %15 = OpLoad %int %b
+         %17 = OpIMul %int %15 %int_2
+               OpStore %b %17
+               OpReturn
+               OpFunctionEnd
+%uses_a_and_b = OpFunction %void None %6
+         %19 = OpLabel
+         %20 = OpLoad %int %a
+               OpStore %b %20
+               OpReturn
+               OpFunctionEnd
+    %no_uses = OpFunction %void None %6
+         %22 = OpLabel
+               OpReturn
+               OpFunctionEnd
+      %outer = OpFunction %void None %6
+         %24 = OpLabel
+               OpStore %a %int_0
+         %26 = OpFunctionCall %void %uses_a
+         %27 = OpFunctionCall %void %uses_a_and_b
+         %28 = OpFunctionCall %void %uses_b
+         %29 = OpFunctionCall %void %no_uses
+               OpReturn
+               OpFunctionEnd
+      %main1 = OpFunction %void None %6
+         %31 = OpLabel
+               OpStore %a %int_42
+         %33 = OpFunctionCall %void %uses_a
+               OpReturn
+               OpFunctionEnd
+      %main2 = OpFunction %void None %6
+         %35 = OpLabel
+               OpStore %b %int_7
+         %37 = OpFunctionCall %void %uses_b
+               OpReturn
+               OpFunctionEnd
+      %main3 = OpFunction %void None %6
+         %39 = OpLabel
+         %40 = OpFunctionCall %void %outer
+         %41 = OpFunctionCall %void %no_uses
+               OpReturn
+               OpFunctionEnd
+      %main4 = OpFunction %void None %6
+         %43 = OpLabel
+         %44 = OpFunctionCall %void %no_uses
+               OpReturn
+               OpFunctionEnd
diff --git a/test/var/workgroup.wgsl.expected.wgsl b/test/var/workgroup.wgsl.expected.wgsl
new file mode 100644
index 0000000..1861fb8
--- /dev/null
+++ b/test/var/workgroup.wgsl.expected.wgsl
@@ -0,0 +1,51 @@
+var<workgroup> a : i32;
+
+var<workgroup> b : i32;
+
+var<workgroup> c : i32;
+
+fn uses_a() {
+  a = (a + 1);
+}
+
+fn uses_b() {
+  b = (b * 2);
+}
+
+fn uses_a_and_b() {
+  b = a;
+}
+
+fn no_uses() {
+}
+
+fn outer() {
+  a = 0;
+  uses_a();
+  uses_a_and_b();
+  uses_b();
+  no_uses();
+}
+
+[[stage(compute)]]
+fn main1() {
+  a = 42;
+  uses_a();
+}
+
+[[stage(compute)]]
+fn main2() {
+  b = 7;
+  uses_b();
+}
+
+[[stage(compute)]]
+fn main3() {
+  outer();
+  no_uses();
+}
+
+[[stage(compute)]]
+fn main4() {
+  no_uses();
+}