Add semantic::Variable, use it.

Pull the mutable semantic field from ast::Variable and into a new semantic::Variable node.
Have the TypeDeterminer create these semantic::Variable nodes.

Bug: tint:390
Change-Id: Ia13f5e7b065941ed66ea5a86c6ccb288071feff3
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/40063
Commit-Queue: Ben Clayton <bclayton@google.com>
Reviewed-by: David Neto <dneto@google.com>
diff --git a/BUILD.gn b/BUILD.gn
index 8e15afc..12578bb 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -385,6 +385,7 @@
     "src/semantic/sem_function.cc",
     "src/semantic/sem_info.cc",
     "src/semantic/sem_node.cc",
+    "src/semantic/sem_variable.cc",
     "src/semantic/type_mappings.h",
     "src/source.cc",
     "src/source.h",
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 56a1efb..f79ae1a 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -199,6 +199,7 @@
   semantic/sem_function.cc
   semantic/sem_info.cc
   semantic/sem_node.cc
+  semantic/sem_variable.cc
   semantic/type_mappings.h
   source.cc
   source.h
diff --git a/src/ast/variable.cc b/src/ast/variable.cc
index cc0e676..40056a4 100644
--- a/src/ast/variable.cc
+++ b/src/ast/variable.cc
@@ -19,6 +19,7 @@
 #include "src/ast/constant_id_decoration.h"
 #include "src/clone_context.h"
 #include "src/program_builder.h"
+#include "src/semantic/variable.h"
 
 TINT_INSTANTIATE_CLASS_ID(tint::ast::Variable);
 
@@ -38,7 +39,7 @@
       is_const_(is_const),
       constructor_(constructor),
       decorations_(std::move(decorations)),
-      storage_class_(sc) {}
+      declared_storage_class_(sc) {}
 
 Variable::Variable(Variable&&) = default;
 
@@ -91,10 +92,10 @@
 }
 
 Variable* Variable::Clone(CloneContext* ctx) const {
-  return ctx->dst->create<Variable>(ctx->Clone(source()), ctx->Clone(symbol_),
-                                    storage_class(), ctx->Clone(type()),
-                                    is_const_, ctx->Clone(constructor()),
-                                    ctx->Clone(decorations_));
+  return ctx->dst->create<Variable>(
+      ctx->Clone(source()), ctx->Clone(symbol_), declared_storage_class(),
+      ctx->Clone(type()), is_const_, ctx->Clone(constructor()),
+      ctx->Clone(decorations_));
 }
 
 bool Variable::IsValid() const {
@@ -110,13 +111,15 @@
   return true;
 }
 
-void Variable::info_to_str(const semantic::Info&,
+void Variable::info_to_str(const semantic::Info& sem,
                            std::ostream& out,
                            size_t indent) const {
+  auto* var_sem = sem.Get(this);
   make_indent(out, indent);
   out << symbol_.to_str() << std::endl;
   make_indent(out, indent);
-  out << storage_class_ << std::endl;
+  out << (var_sem ? var_sem->StorageClass() : declared_storage_class())
+      << std::endl;
   make_indent(out, indent);
   out << type_->type_name() << std::endl;
 }
diff --git a/src/ast/variable.h b/src/ast/variable.h
index fb337b1..2d18b14 100644
--- a/src/ast/variable.h
+++ b/src/ast/variable.h
@@ -84,7 +84,7 @@
   /// Create a variable
   /// @param source the variable source
   /// @param sym the variable symbol
-  /// @param sc the variable storage class
+  /// @param sc the declared storage class
   /// @param type the value type
   /// @param is_const true if the variable is const
   /// @param constructor the constructor expression
@@ -107,12 +107,10 @@
   /// @returns the variable's type.
   type::Type* type() const { return type_; }
 
-  /// Sets the storage class
-  /// @param sc the storage class
-  void set_storage_class(StorageClass sc) { storage_class_ = sc; }
-  /// @returns the storage class
-  StorageClass storage_class() const { return storage_class_; }
-
+  /// @returns the declared storage class
+  StorageClass declared_storage_class() const {
+    return declared_storage_class_;
+  }
   /// @returns the constructor expression or nullptr if none set
   Expression* constructor() const { return constructor_; }
   /// @returns true if the variable has an constructor
@@ -126,7 +124,7 @@
 
   /// @returns true if the decorations include a LocationDecoration
   bool HasLocationDecoration() const;
-  /// @returns true if the deocrations include a BuiltinDecoration
+  /// @returns true if the decorations include a BuiltinDecoration
   bool HasBuiltinDecoration() const;
   /// @returns true if the decorations include a ConstantIdDecoration
   bool HasConstantIdDecoration() const;
@@ -182,8 +180,7 @@
   bool const is_const_;
   Expression* const constructor_;
   VariableDecorationList const decorations_;
-
-  StorageClass storage_class_ = StorageClass::kNone;  // Semantic info
+  StorageClass const declared_storage_class_;
 };
 
 /// A list of variables
diff --git a/src/ast/variable_test.cc b/src/ast/variable_test.cc
index f700381..9a9f6be 100644
--- a/src/ast/variable_test.cc
+++ b/src/ast/variable_test.cc
@@ -30,7 +30,7 @@
   auto* v = Var("my_var", StorageClass::kFunction, ty.i32());
 
   EXPECT_EQ(v->symbol(), Symbol(1));
-  EXPECT_EQ(v->storage_class(), StorageClass::kFunction);
+  EXPECT_EQ(v->declared_storage_class(), StorageClass::kFunction);
   EXPECT_EQ(v->type(), ty.i32());
   EXPECT_EQ(v->source().range.begin.line, 0u);
   EXPECT_EQ(v->source().range.begin.column, 0u);
@@ -44,7 +44,7 @@
       "i", StorageClass::kPrivate, ty.f32(), nullptr, VariableDecorationList{});
 
   EXPECT_EQ(v->symbol(), Symbol(1));
-  EXPECT_EQ(v->storage_class(), StorageClass::kPrivate);
+  EXPECT_EQ(v->declared_storage_class(), StorageClass::kPrivate);
   EXPECT_EQ(v->type(), ty.f32());
   EXPECT_EQ(v->source().range.begin.line, 27u);
   EXPECT_EQ(v->source().range.begin.column, 4u);
@@ -59,7 +59,7 @@
       VariableDecorationList{});
 
   EXPECT_EQ(v->symbol(), Symbol(1));
-  EXPECT_EQ(v->storage_class(), StorageClass::kWorkgroup);
+  EXPECT_EQ(v->declared_storage_class(), StorageClass::kWorkgroup);
   EXPECT_EQ(v->type(), ty.i32());
   EXPECT_EQ(v->source().range.begin.line, 27u);
   EXPECT_EQ(v->source().range.begin.column, 4u);
diff --git a/src/inspector/inspector.cc b/src/inspector/inspector.cc
index ccfe471..ca4bb97 100644
--- a/src/inspector/inspector.cc
+++ b/src/inspector/inspector.cc
@@ -30,6 +30,7 @@
 #include "src/ast/variable.h"
 #include "src/program.h"
 #include "src/semantic/function.h"
+#include "src/semantic/variable.h"
 #include "src/type/access_control_type.h"
 #include "src/type/array_type.h"
 #include "src/type/f32_type.h"
@@ -66,14 +67,16 @@
              entry_point.workgroup_size_z) = func->workgroup_size();
 
     for (auto* var : program_->Sem().Get(func)->ReferencedModuleVariables()) {
-      auto name = program_->Symbols().NameFor(var->symbol());
-      if (var->HasBuiltinDecoration()) {
+      auto* decl = var->Declaration();
+
+      auto name = program_->Symbols().NameFor(decl->symbol());
+      if (decl->HasBuiltinDecoration()) {
         continue;
       }
 
       StageVariable stage_variable;
       stage_variable.name = name;
-      auto* location_decoration = var->GetLocationDecoration();
+      auto* location_decoration = decl->GetLocationDecoration();
       if (location_decoration) {
         stage_variable.has_location_decoration = true;
         stage_variable.location_decoration = location_decoration->value();
@@ -81,9 +84,9 @@
         stage_variable.has_location_decoration = false;
       }
 
-      if (var->storage_class() == ast::StorageClass::kInput) {
+      if (var->StorageClass() == ast::StorageClass::kInput) {
         entry_point.input_variables.push_back(stage_variable);
-      } else if (var->storage_class() == ast::StorageClass::kOutput) {
+      } else if (var->StorageClass() == ast::StorageClass::kOutput) {
         entry_point.output_variables.push_back(stage_variable);
       }
     }
@@ -188,14 +191,14 @@
 
   auto* func_sem = program_->Sem().Get(func);
   for (auto& ruv : func_sem->ReferencedUniformVariables()) {
-    ResourceBinding entry;
-    ast::Variable* var = nullptr;
-    semantic::Function::BindingInfo binding_info;
-    std::tie(var, binding_info) = ruv;
-    if (!var->type()->Is<type::AccessControl>()) {
+    auto* var = ruv.first;
+    auto* decl = var->Declaration();
+    auto binding_info = ruv.second;
+
+    if (!decl->type()->Is<type::AccessControl>()) {
       continue;
     }
-    auto* unwrapped_type = var->type()->UnwrapIfNeeded();
+    auto* unwrapped_type = decl->type()->UnwrapIfNeeded();
 
     auto* str = unwrapped_type->As<type::Struct>();
     if (str == nullptr) {
@@ -206,10 +209,11 @@
       continue;
     }
 
+    ResourceBinding entry;
     entry.bind_group = binding_info.group->value();
     entry.binding = binding_info.binding->value();
     entry.min_buffer_binding_size =
-        var->type()->MinBufferBindingSize(type::MemoryLayout::kUniformBuffer);
+        decl->type()->MinBufferBindingSize(type::MemoryLayout::kUniformBuffer);
 
     result.push_back(entry);
   }
@@ -239,11 +243,9 @@
 
   auto* func_sem = program_->Sem().Get(func);
   for (auto& rs : func_sem->ReferencedSamplerVariables()) {
-    ResourceBinding entry;
-    ast::Variable* var = nullptr;
-    semantic::Function::BindingInfo binding_info;
-    std::tie(var, binding_info) = rs;
+    auto binding_info = rs.second;
 
+    ResourceBinding entry;
     entry.bind_group = binding_info.group->value();
     entry.binding = binding_info.binding->value();
 
@@ -264,11 +266,9 @@
 
   auto* func_sem = program_->Sem().Get(func);
   for (auto& rcs : func_sem->ReferencedComparisonSamplerVariables()) {
-    ResourceBinding entry;
-    ast::Variable* var = nullptr;
-    semantic::Function::BindingInfo binding_info;
-    std::tie(var, binding_info) = rcs;
+    auto binding_info = rcs.second;
 
+    ResourceBinding entry;
     entry.bind_group = binding_info.group->value();
     entry.binding = binding_info.binding->value();
 
@@ -314,12 +314,11 @@
   auto* func_sem = program_->Sem().Get(func);
   std::vector<ResourceBinding> result;
   for (auto& rsv : func_sem->ReferencedStoragebufferVariables()) {
-    ResourceBinding entry;
-    ast::Variable* var = nullptr;
-    semantic::Function::BindingInfo binding_info;
-    std::tie(var, binding_info) = rsv;
+    auto* var = rsv.first;
+    auto* decl = var->Declaration();
+    auto binding_info = rsv.second;
 
-    auto* ac_type = var->type()->As<type::AccessControl>();
+    auto* ac_type = decl->type()->As<type::AccessControl>();
     if (ac_type == nullptr) {
       continue;
     }
@@ -328,14 +327,15 @@
       continue;
     }
 
-    if (!var->type()->UnwrapIfNeeded()->Is<type::Struct>()) {
+    if (!decl->type()->UnwrapIfNeeded()->Is<type::Struct>()) {
       continue;
     }
 
+    ResourceBinding entry;
     entry.bind_group = binding_info.group->value();
     entry.binding = binding_info.binding->value();
     entry.min_buffer_binding_size =
-        var->type()->MinBufferBindingSize(type::MemoryLayout::kStorageBuffer);
+        decl->type()->MinBufferBindingSize(type::MemoryLayout::kStorageBuffer);
 
     result.push_back(entry);
   }
@@ -357,15 +357,15 @@
       multisampled_only ? func_sem->ReferencedMultisampledTextureVariables()
                         : func_sem->ReferencedSampledTextureVariables();
   for (auto& ref : referenced_variables) {
-    ResourceBinding entry;
-    ast::Variable* var = nullptr;
-    semantic::Function::BindingInfo binding_info;
-    std::tie(var, binding_info) = ref;
+    auto* var = ref.first;
+    auto* decl = var->Declaration();
+    auto binding_info = ref.second;
 
+    ResourceBinding entry;
     entry.bind_group = binding_info.group->value();
     entry.binding = binding_info.binding->value();
 
-    auto* texture_type = var->type()->UnwrapIfNeeded()->As<type::Texture>();
+    auto* texture_type = decl->type()->UnwrapIfNeeded()->As<type::Texture>();
     switch (texture_type->dim()) {
       case type::TextureDimension::k1d:
         entry.dim = ResourceBinding::TextureDimension::k1d;
diff --git a/src/reader/wgsl/parser_impl_global_variable_decl_test.cc b/src/reader/wgsl/parser_impl_global_variable_decl_test.cc
index 9c83e42..625abb5 100644
--- a/src/reader/wgsl/parser_impl_global_variable_decl_test.cc
+++ b/src/reader/wgsl/parser_impl_global_variable_decl_test.cc
@@ -38,7 +38,7 @@
 
   EXPECT_EQ(e->symbol(), p->builder().Symbols().Get("a"));
   EXPECT_TRUE(e->type()->Is<type::F32>());
-  EXPECT_EQ(e->storage_class(), ast::StorageClass::kOutput);
+  EXPECT_EQ(e->declared_storage_class(), ast::StorageClass::kOutput);
 
   EXPECT_EQ(e->source().range.begin.line, 1u);
   EXPECT_EQ(e->source().range.begin.column, 10u);
@@ -61,7 +61,7 @@
 
   EXPECT_EQ(e->symbol(), p->builder().Symbols().Get("a"));
   EXPECT_TRUE(e->type()->Is<type::F32>());
-  EXPECT_EQ(e->storage_class(), ast::StorageClass::kOutput);
+  EXPECT_EQ(e->declared_storage_class(), ast::StorageClass::kOutput);
 
   EXPECT_EQ(e->source().range.begin.line, 1u);
   EXPECT_EQ(e->source().range.begin.column, 10u);
@@ -87,7 +87,7 @@
   EXPECT_EQ(e->symbol(), p->builder().Symbols().Get("a"));
   ASSERT_NE(e->type(), nullptr);
   EXPECT_TRUE(e->type()->Is<type::F32>());
-  EXPECT_EQ(e->storage_class(), ast::StorageClass::kOutput);
+  EXPECT_EQ(e->declared_storage_class(), ast::StorageClass::kOutput);
 
   EXPECT_EQ(e->source().range.begin.line, 1u);
   EXPECT_EQ(e->source().range.begin.column, 35u);
@@ -117,7 +117,7 @@
   EXPECT_EQ(e->symbol(), p->builder().Symbols().Get("a"));
   ASSERT_NE(e->type(), nullptr);
   EXPECT_TRUE(e->type()->Is<type::F32>());
-  EXPECT_EQ(e->storage_class(), ast::StorageClass::kOutput);
+  EXPECT_EQ(e->declared_storage_class(), ast::StorageClass::kOutput);
 
   EXPECT_EQ(e->source().range.begin.line, 1u);
   EXPECT_EQ(e->source().range.begin.column, 38u);
diff --git a/src/semantic/function.h b/src/semantic/function.h
index 0c75113..70708d9 100644
--- a/src/semantic/function.h
+++ b/src/semantic/function.h
@@ -28,7 +28,6 @@
 namespace ast {
 class BindingDecoration;
 class GroupDecoration;
-class Variable;
 class LocationDecoration;
 class BuiltinDecoration;
 }  // namespace ast
@@ -38,6 +37,8 @@
 
 namespace semantic {
 
+class Variable;
+
 /// Function holds the semantic information for function nodes.
 class Function : public Castable<Function, Node> {
  public:
@@ -54,8 +55,8 @@
   /// @param local_referenced_module_vars the locally referenced module
   /// variables
   /// @param ancestor_entry_points the ancestor entry points
-  explicit Function(std::vector<ast::Variable*> referenced_module_vars,
-                    std::vector<ast::Variable*> local_referenced_module_vars,
+  explicit Function(std::vector<const Variable*> referenced_module_vars,
+                    std::vector<const Variable*> local_referenced_module_vars,
                     std::vector<Symbol> ancestor_entry_points);
 
   /// Destructor
@@ -64,11 +65,11 @@
   /// Note: If this function calls other functions, the return will also include
   /// all of the referenced variables from the callees.
   /// @returns the referenced module variables
-  const std::vector<ast::Variable*>& ReferencedModuleVariables() const {
+  const std::vector<const Variable*>& ReferencedModuleVariables() const {
     return referenced_module_vars_;
   }
   /// @returns the locally referenced module variables
-  const std::vector<ast::Variable*>& LocalReferencedModuleVariables() const {
+  const std::vector<const Variable*>& LocalReferencedModuleVariables() const {
     return local_referenced_module_vars_;
   }
   /// @returns the ancestor entry points
@@ -77,53 +78,53 @@
   }
   /// Retrieves any referenced location variables
   /// @returns the <variable, decoration> pair.
-  const std::vector<std::pair<ast::Variable*, ast::LocationDecoration*>>
+  const std::vector<std::pair<const Variable*, ast::LocationDecoration*>>
   ReferencedLocationVariables() const;
 
   /// Retrieves any referenced builtin variables
   /// @returns the <variable, decoration> pair.
-  const std::vector<std::pair<ast::Variable*, ast::BuiltinDecoration*>>
+  const std::vector<std::pair<const Variable*, ast::BuiltinDecoration*>>
   ReferencedBuiltinVariables() const;
 
   /// Retrieves any referenced uniform variables. Note, the variables must be
   /// decorated with both binding and group decorations.
   /// @returns the referenced uniforms
-  const std::vector<std::pair<ast::Variable*, BindingInfo>>
+  const std::vector<std::pair<const Variable*, BindingInfo>>
   ReferencedUniformVariables() const;
 
   /// Retrieves any referenced storagebuffer variables. Note, the variables
   /// must be decorated with both binding and group decorations.
   /// @returns the referenced storagebuffers
-  const std::vector<std::pair<ast::Variable*, BindingInfo>>
+  const std::vector<std::pair<const Variable*, BindingInfo>>
   ReferencedStoragebufferVariables() const;
 
   /// Retrieves any referenced regular Sampler variables. Note, the
   /// variables must be decorated with both binding and group decorations.
   /// @returns the referenced storagebuffers
-  const std::vector<std::pair<ast::Variable*, BindingInfo>>
+  const std::vector<std::pair<const Variable*, BindingInfo>>
   ReferencedSamplerVariables() const;
 
   /// Retrieves any referenced comparison Sampler variables. Note, the
   /// variables must be decorated with both binding and group decorations.
   /// @returns the referenced storagebuffers
-  const std::vector<std::pair<ast::Variable*, BindingInfo>>
+  const std::vector<std::pair<const Variable*, BindingInfo>>
   ReferencedComparisonSamplerVariables() const;
 
   /// Retrieves any referenced sampled textures variables. Note, the
   /// variables must be decorated with both binding and group decorations.
   /// @returns the referenced sampled textures
-  const std::vector<std::pair<ast::Variable*, BindingInfo>>
+  const std::vector<std::pair<const Variable*, BindingInfo>>
   ReferencedSampledTextureVariables() const;
 
   /// Retrieves any referenced multisampled textures variables. Note, the
   /// variables must be decorated with both binding and group decorations.
   /// @returns the referenced sampled textures
-  const std::vector<std::pair<ast::Variable*, BindingInfo>>
+  const std::vector<std::pair<const Variable*, BindingInfo>>
   ReferencedMultisampledTextureVariables() const;
 
   /// Retrieves any locally referenced builtin variables
   /// @returns the <variable, decoration> pairs.
-  const std::vector<std::pair<ast::Variable*, ast::BuiltinDecoration*>>
+  const std::vector<std::pair<const Variable*, ast::BuiltinDecoration*>>
   LocalReferencedBuiltinVariables() const;
 
   /// Checks if the given entry point is an ancestor
@@ -132,13 +133,13 @@
   bool HasAncestorEntryPoint(Symbol sym) const;
 
  private:
-  const std::vector<std::pair<ast::Variable*, BindingInfo>>
+  const std::vector<std::pair<const Variable*, BindingInfo>>
   ReferencedSamplerVariablesImpl(type::SamplerKind kind) const;
-  const std::vector<std::pair<ast::Variable*, BindingInfo>>
+  const std::vector<std::pair<const Variable*, BindingInfo>>
   ReferencedSampledTextureVariablesImpl(bool multisampled) const;
 
-  std::vector<ast::Variable*> const referenced_module_vars_;
-  std::vector<ast::Variable*> const local_referenced_module_vars_;
+  std::vector<const Variable*> const referenced_module_vars_;
+  std::vector<const Variable*> const local_referenced_module_vars_;
   std::vector<Symbol> const ancestor_entry_points_;
 };
 
diff --git a/src/semantic/sem_function.cc b/src/semantic/sem_function.cc
index 5613697..26768ec 100644
--- a/src/semantic/sem_function.cc
+++ b/src/semantic/sem_function.cc
@@ -20,6 +20,7 @@
 #include "src/ast/location_decoration.h"
 #include "src/ast/variable.h"
 #include "src/ast/variable_decoration.h"
+#include "src/semantic/variable.h"
 #include "src/type/multisampled_texture_type.h"
 #include "src/type/sampled_texture_type.h"
 #include "src/type/texture_type.h"
@@ -29,8 +30,8 @@
 namespace tint {
 namespace semantic {
 
-Function::Function(std::vector<ast::Variable*> referenced_module_vars,
-                   std::vector<ast::Variable*> local_referenced_module_vars,
+Function::Function(std::vector<const Variable*> referenced_module_vars,
+                   std::vector<const Variable*> local_referenced_module_vars,
                    std::vector<Symbol> ancestor_entry_points)
     : referenced_module_vars_(std::move(referenced_module_vars)),
       local_referenced_module_vars_(std::move(local_referenced_module_vars)),
@@ -38,12 +39,12 @@
 
 Function::~Function() = default;
 
-const std::vector<std::pair<ast::Variable*, ast::LocationDecoration*>>
+const std::vector<std::pair<const Variable*, ast::LocationDecoration*>>
 Function::ReferencedLocationVariables() const {
-  std::vector<std::pair<ast::Variable*, ast::LocationDecoration*>> ret;
+  std::vector<std::pair<const Variable*, ast::LocationDecoration*>> ret;
 
   for (auto* var : ReferencedModuleVariables()) {
-    for (auto* deco : var->decorations()) {
+    for (auto* deco : var->Declaration()->decorations()) {
       if (auto* location = deco->As<ast::LocationDecoration>()) {
         ret.push_back({var, location});
         break;
@@ -53,18 +54,18 @@
   return ret;
 }
 
-const std::vector<std::pair<ast::Variable*, Function::BindingInfo>>
+const std::vector<std::pair<const Variable*, Function::BindingInfo>>
 Function::ReferencedUniformVariables() const {
-  std::vector<std::pair<ast::Variable*, Function::BindingInfo>> ret;
+  std::vector<std::pair<const Variable*, Function::BindingInfo>> ret;
 
   for (auto* var : ReferencedModuleVariables()) {
-    if (var->storage_class() != ast::StorageClass::kUniform) {
+    if (var->StorageClass() != ast::StorageClass::kUniform) {
       continue;
     }
 
     ast::BindingDecoration* binding = nullptr;
     ast::GroupDecoration* group = nullptr;
-    for (auto* deco : var->decorations()) {
+    for (auto* deco : var->Declaration()->decorations()) {
       if (auto* b = deco->As<ast::BindingDecoration>()) {
         binding = b;
       } else if (auto* g = deco->As<ast::GroupDecoration>()) {
@@ -80,18 +81,18 @@
   return ret;
 }
 
-const std::vector<std::pair<ast::Variable*, Function::BindingInfo>>
+const std::vector<std::pair<const Variable*, Function::BindingInfo>>
 Function::ReferencedStoragebufferVariables() const {
-  std::vector<std::pair<ast::Variable*, Function::BindingInfo>> ret;
+  std::vector<std::pair<const Variable*, Function::BindingInfo>> ret;
 
   for (auto* var : ReferencedModuleVariables()) {
-    if (var->storage_class() != ast::StorageClass::kStorage) {
+    if (var->StorageClass() != ast::StorageClass::kStorage) {
       continue;
     }
 
     ast::BindingDecoration* binding = nullptr;
     ast::GroupDecoration* group = nullptr;
-    for (auto* deco : var->decorations()) {
+    for (auto* deco : var->Declaration()->decorations()) {
       if (auto* b = deco->As<ast::BindingDecoration>()) {
         binding = b;
       } else if (auto* s = deco->As<ast::GroupDecoration>()) {
@@ -107,12 +108,12 @@
   return ret;
 }
 
-const std::vector<std::pair<ast::Variable*, ast::BuiltinDecoration*>>
+const std::vector<std::pair<const Variable*, ast::BuiltinDecoration*>>
 Function::ReferencedBuiltinVariables() const {
-  std::vector<std::pair<ast::Variable*, ast::BuiltinDecoration*>> ret;
+  std::vector<std::pair<const Variable*, ast::BuiltinDecoration*>> ret;
 
   for (auto* var : ReferencedModuleVariables()) {
-    for (auto* deco : var->decorations()) {
+    for (auto* deco : var->Declaration()->decorations()) {
       if (auto* builtin = deco->As<ast::BuiltinDecoration>()) {
         ret.push_back({var, builtin});
         break;
@@ -122,32 +123,32 @@
   return ret;
 }
 
-const std::vector<std::pair<ast::Variable*, Function::BindingInfo>>
+const std::vector<std::pair<const Variable*, Function::BindingInfo>>
 Function::ReferencedSamplerVariables() const {
   return ReferencedSamplerVariablesImpl(type::SamplerKind::kSampler);
 }
 
-const std::vector<std::pair<ast::Variable*, Function::BindingInfo>>
+const std::vector<std::pair<const Variable*, Function::BindingInfo>>
 Function::ReferencedComparisonSamplerVariables() const {
   return ReferencedSamplerVariablesImpl(type::SamplerKind::kComparisonSampler);
 }
 
-const std::vector<std::pair<ast::Variable*, Function::BindingInfo>>
+const std::vector<std::pair<const Variable*, Function::BindingInfo>>
 Function::ReferencedSampledTextureVariables() const {
   return ReferencedSampledTextureVariablesImpl(false);
 }
 
-const std::vector<std::pair<ast::Variable*, Function::BindingInfo>>
+const std::vector<std::pair<const Variable*, Function::BindingInfo>>
 Function::ReferencedMultisampledTextureVariables() const {
   return ReferencedSampledTextureVariablesImpl(true);
 }
 
-const std::vector<std::pair<ast::Variable*, ast::BuiltinDecoration*>>
+const std::vector<std::pair<const Variable*, ast::BuiltinDecoration*>>
 Function::LocalReferencedBuiltinVariables() const {
-  std::vector<std::pair<ast::Variable*, ast::BuiltinDecoration*>> ret;
+  std::vector<std::pair<const Variable*, ast::BuiltinDecoration*>> ret;
 
   for (auto* var : LocalReferencedModuleVariables()) {
-    for (auto* deco : var->decorations()) {
+    for (auto* deco : var->Declaration()->decorations()) {
       if (auto* builtin = deco->As<ast::BuiltinDecoration>()) {
         ret.push_back({var, builtin});
         break;
@@ -166,12 +167,12 @@
   return false;
 }
 
-const std::vector<std::pair<ast::Variable*, Function::BindingInfo>>
+const std::vector<std::pair<const Variable*, Function::BindingInfo>>
 Function::ReferencedSamplerVariablesImpl(type::SamplerKind kind) const {
-  std::vector<std::pair<ast::Variable*, Function::BindingInfo>> ret;
+  std::vector<std::pair<const Variable*, Function::BindingInfo>> ret;
 
   for (auto* var : ReferencedModuleVariables()) {
-    auto* unwrapped_type = var->type()->UnwrapIfNeeded();
+    auto* unwrapped_type = var->Declaration()->type()->UnwrapIfNeeded();
     auto* sampler = unwrapped_type->As<type::Sampler>();
     if (sampler == nullptr || sampler->kind() != kind) {
       continue;
@@ -179,7 +180,7 @@
 
     ast::BindingDecoration* binding = nullptr;
     ast::GroupDecoration* group = nullptr;
-    for (auto* deco : var->decorations()) {
+    for (auto* deco : var->Declaration()->decorations()) {
       if (auto* b = deco->As<ast::BindingDecoration>()) {
         binding = b;
       }
@@ -196,12 +197,12 @@
   return ret;
 }
 
-const std::vector<std::pair<ast::Variable*, Function::BindingInfo>>
+const std::vector<std::pair<const Variable*, Function::BindingInfo>>
 Function::ReferencedSampledTextureVariablesImpl(bool multisampled) const {
-  std::vector<std::pair<ast::Variable*, Function::BindingInfo>> ret;
+  std::vector<std::pair<const Variable*, Function::BindingInfo>> ret;
 
   for (auto* var : ReferencedModuleVariables()) {
-    auto* unwrapped_type = var->type()->UnwrapIfNeeded();
+    auto* unwrapped_type = var->Declaration()->type()->UnwrapIfNeeded();
     auto* texture = unwrapped_type->As<type::Texture>();
     if (texture == nullptr) {
       continue;
@@ -216,7 +217,7 @@
 
     ast::BindingDecoration* binding = nullptr;
     ast::GroupDecoration* group = nullptr;
-    for (auto* deco : var->decorations()) {
+    for (auto* deco : var->Declaration()->decorations()) {
       if (auto* b = deco->As<ast::BindingDecoration>()) {
         binding = b;
       } else if (auto* s = deco->As<ast::GroupDecoration>()) {
diff --git a/src/semantic/sem_variable.cc b/src/semantic/sem_variable.cc
new file mode 100644
index 0000000..33e84a1
--- /dev/null
+++ b/src/semantic/sem_variable.cc
@@ -0,0 +1,28 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/semantic/variable.h"
+
+TINT_INSTANTIATE_CLASS_ID(tint::semantic::Variable);
+
+namespace tint {
+namespace semantic {
+
+Variable::Variable(ast::Variable* declaration, ast::StorageClass storage_class)
+    : declaration_(declaration), storage_class_(storage_class) {}
+
+Variable::~Variable() = default;
+
+}  // namespace semantic
+}  // namespace tint
diff --git a/src/semantic/type_mappings.h b/src/semantic/type_mappings.h
index ac099a8..58b91c5 100644
--- a/src/semantic/type_mappings.h
+++ b/src/semantic/type_mappings.h
@@ -24,6 +24,7 @@
 
 class Expression;
 class Function;
+class Variable;
 
 }  // namespace ast
 
@@ -31,6 +32,7 @@
 
 class Expression;
 class Function;
+class Variable;
 
 /// TypeMappings is a struct that holds dummy `operator()` methods that's used
 /// by SemanticNodeTypeFor to map AST node types to their corresponding semantic
@@ -41,6 +43,7 @@
   //! @cond Doxygen_Suppress
   semantic::Expression* operator()(ast::Expression*);
   semantic::Function* operator()(ast::Function*);
+  semantic::Variable* operator()(ast::Variable*);
   //! @endcond
 };
 
diff --git a/src/semantic/variable.h b/src/semantic/variable.h
new file mode 100644
index 0000000..6f15dc7
--- /dev/null
+++ b/src/semantic/variable.h
@@ -0,0 +1,64 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0(the "License");
+
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_SEMANTIC_VARIABLE_H_
+#define SRC_SEMANTIC_VARIABLE_H_
+
+#include <utility>
+#include <vector>
+
+#include "src/ast/storage_class.h"
+#include "src/semantic/node.h"
+#include "src/type/sampler_type.h"
+
+namespace tint {
+
+// Forward declarations
+namespace ast {
+class Variable;
+}  // namespace ast
+namespace type {
+class Type;
+}  // namespace type
+
+namespace semantic {
+
+/// Variable holds the semantic information for variables.
+class Variable : public Castable<Variable, Node> {
+ public:
+  /// Constructor
+  /// @param declaration the AST declaration node
+  /// @param storage_class the variable storage class
+  explicit Variable(ast::Variable* declaration,
+                    ast::StorageClass storage_class);
+
+  /// Destructor
+  ~Variable() override;
+
+  /// @returns the AST declaration node
+  ast::Variable* Declaration() const { return declaration_; }
+
+  /// @returns the storage class for the variable
+  ast::StorageClass StorageClass() const { return storage_class_; }
+
+ private:
+  ast::Variable* const declaration_;
+  ast::StorageClass const storage_class_;
+};
+
+}  // namespace semantic
+}  // namespace tint
+
+#endif  // SRC_SEMANTIC_VARIABLE_H_
diff --git a/src/transform/emit_vertex_point_size.cc b/src/transform/emit_vertex_point_size.cc
index 319d144..748021c 100644
--- a/src/transform/emit_vertex_point_size.cc
+++ b/src/transform/emit_vertex_point_size.cc
@@ -58,7 +58,6 @@
       false,                                  // is_const
       nullptr,                                // constructor
       ast::VariableDecorationList{
-          // decorations
           out.create<ast::BuiltinDecoration>(Source{},
                                              ast::Builtin::kPointSize),
       });
diff --git a/src/transform/first_index_offset.cc b/src/transform/first_index_offset.cc
index fec1068..4ed52ab 100644
--- a/src/transform/first_index_offset.cc
+++ b/src/transform/first_index_offset.cc
@@ -70,7 +70,7 @@
   return ctx->dst->create<ast::Variable>(
       ctx->Clone(in->source()),                // source
       ctx->dst->Symbols().Register(new_name),  // symbol
-      in->storage_class(),                     // storage_class
+      in->declared_storage_class(),            // declared_storage_class
       ctx->Clone(in->type()),                  // type
       in->is_const(),                          // is_const
       ctx->Clone(in->constructor()),           // constructor
@@ -226,7 +226,7 @@
       ast::VariableDecorationList{
           dst->create<ast::BindingDecoration>(Source{}, binding_),
           dst->create<ast::GroupDecoration>(Source{}, group_),
-      });  // decorations
+      });
 
   dst->AST().AddGlobalVariable(idx_var);
 
diff --git a/src/transform/vertex_pulling.cc b/src/transform/vertex_pulling.cc
index 39585be..f052bd4 100644
--- a/src/transform/vertex_pulling.cc
+++ b/src/transform/vertex_pulling.cc
@@ -35,6 +35,7 @@
 #include "src/clone_context.h"
 #include "src/program.h"
 #include "src/program_builder.h"
+#include "src/semantic/variable.h"
 #include "src/type/array_type.h"
 #include "src/type/f32_type.h"
 #include "src/type/i32_type.h"
@@ -150,7 +151,8 @@
 
   // Look for an existing vertex index builtin
   for (auto* v : ctx.src->AST().GlobalVariables()) {
-    if (v->storage_class() != ast::StorageClass::kInput) {
+    auto* sem = ctx.src->Sem().Get(v);
+    if (sem->StorageClass() != ast::StorageClass::kInput) {
       continue;
     }
 
@@ -196,7 +198,8 @@
 
   // Look for an existing instance index builtin
   for (auto* v : ctx.src->AST().GlobalVariables()) {
-    if (v->storage_class() != ast::StorageClass::kInput) {
+    auto* sem = ctx.src->Sem().Get(v);
+    if (sem->StorageClass() != ast::StorageClass::kInput) {
       continue;
     }
 
@@ -229,7 +232,8 @@
 
 void VertexPulling::State::ConvertVertexInputVariablesToPrivate() {
   for (auto* v : ctx.src->AST().GlobalVariables()) {
-    if (v->storage_class() != ast::StorageClass::kInput) {
+    auto* sem = ctx.src->Sem().Get(v);
+    if (sem->StorageClass() != ast::StorageClass::kInput) {
       continue;
     }
 
diff --git a/src/type_determiner.cc b/src/type_determiner.cc
index 6172384..e75bdc8 100644
--- a/src/type_determiner.cc
+++ b/src/type_determiner.cc
@@ -45,6 +45,7 @@
 #include "src/program_builder.h"
 #include "src/semantic/expression.h"
 #include "src/semantic/function.h"
+#include "src/semantic/variable.h"
 #include "src/type/array_type.h"
 #include "src/type/bool_type.h"
 #include "src/type/depth_texture_type.h"
@@ -88,13 +89,13 @@
   error_ += msg;
 }
 
-void TypeDeterminer::set_referenced_from_function_if_needed(ast::Variable* var,
+void TypeDeterminer::set_referenced_from_function_if_needed(VariableInfo* var,
                                                             bool local) {
   if (current_function_ == nullptr) {
     return;
   }
-  if (var->storage_class() == ast::StorageClass::kNone ||
-      var->storage_class() == ast::StorageClass::kFunction) {
+  if (var->storage_class == ast::StorageClass::kNone ||
+      var->storage_class == ast::StorageClass::kFunction) {
     return;
   }
 
@@ -105,7 +106,18 @@
 }
 
 bool TypeDeterminer::Determine() {
+  bool result = DetermineInternal();
+
+  // Even if resolving failed, create all the semantic nodes for information we
+  // did generate.
+  CreateSemanticNodes();
+
+  return result;
+}
+
+bool TypeDeterminer::DetermineInternal() {
   std::vector<type::StorageTexture*> storage_textures;
+
   for (auto& it : builder_->Types().types()) {
     if (auto* storage =
             it.second->UnwrapIfNeeded()->As<type::StorageTexture>()) {
@@ -122,7 +134,7 @@
   }
 
   for (auto* var : builder_->AST().GlobalVariables()) {
-    variable_stack_.set_global(var->symbol(), var);
+    variable_stack_.set_global(var->symbol(), CreateVariableInfo(var));
 
     if (var->has_constructor()) {
       if (!DetermineResultType(var->constructor())) {
@@ -135,8 +147,8 @@
     return false;
   }
 
-  // Walk over the caller to callee information and update functions with which
-  // entry points call those functions.
+  // Walk over the caller to callee information and update functions with
+  // which entry points call those functions.
   for (auto* func : builder_->AST().Functions()) {
     if (!func->IsEntryPoint()) {
       continue;
@@ -146,8 +158,6 @@
     }
   }
 
-  CreateSemanticFunctions();
-
   return true;
 }
 
@@ -170,15 +180,13 @@
 }
 
 bool TypeDeterminer::DetermineFunction(ast::Function* func) {
-  auto* info = function_infos_.Create<FunctionInfo>(func);
-  symbol_to_function_[func->symbol()] = info;
-  function_to_info_.emplace(func, info);
-
-  current_function_ = info;
+  current_function_ = function_infos_.Create<FunctionInfo>(func);
+  symbol_to_function_[func->symbol()] = current_function_;
+  function_to_info_.emplace(func, current_function_);
 
   variable_stack_.push_scope();
   for (auto* param : func->params()) {
-    variable_stack_.set(param->symbol(), param);
+    variable_stack_.set(param->symbol(), CreateVariableInfo(param));
   }
 
   if (!DetermineStatements(func->body())) {
@@ -211,22 +219,26 @@
   }
 
   auto* var = var_decl->variable();
+
+  auto* info = CreateVariableInfo(var);
+  variable_to_info_.emplace(var, info);
+
   // Nothing to do for const
   if (var->is_const()) {
     return true;
   }
 
-  if (var->storage_class() == ast::StorageClass::kFunction) {
+  if (info->storage_class == ast::StorageClass::kFunction) {
     return true;
   }
 
-  if (var->storage_class() != ast::StorageClass::kNone) {
+  if (info->storage_class != ast::StorageClass::kNone) {
     set_error(stmt->source(),
               "function variable has a non-function storage class");
     return false;
   }
 
-  var->set_storage_class(ast::StorageClass::kFunction);
+  info->storage_class = ast::StorageClass::kFunction;
   return true;
 }
 
@@ -291,7 +303,8 @@
     return true;
   }
   if (auto* v = stmt->As<ast::VariableDeclStatement>()) {
-    variable_stack_.set(v->variable()->symbol(), v->variable());
+    variable_stack_.set(v->variable()->symbol(),
+                        variable_to_info_.at(v->variable()));
     return DetermineResultType(v->variable()->constructor());
   }
 
@@ -816,17 +829,17 @@
 
 bool TypeDeterminer::DetermineIdentifier(ast::IdentifierExpression* expr) {
   auto symbol = expr->symbol();
-  ast::Variable* var;
+  VariableInfo* var;
   if (variable_stack_.get(symbol, &var)) {
     // A constant is the type, but a variable is always a pointer so synthesize
     // the pointer around the variable type.
-    if (var->is_const()) {
-      SetType(expr, var->type());
-    } else if (var->type()->Is<type::Pointer>()) {
-      SetType(expr, var->type());
+    if (var->declaration->is_const()) {
+      SetType(expr, var->declaration->type());
+    } else if (var->declaration->type()->Is<type::Pointer>()) {
+      SetType(expr, var->declaration->type());
     } else {
-      SetType(expr, builder_->create<type::Pointer>(var->type(),
-                                                    var->storage_class()));
+      SetType(expr, builder_->create<type::Pointer>(var->declaration->type(),
+                                                    var->storage_class));
     }
 
     set_referenced_from_function_if_needed(var, true);
@@ -1146,6 +1159,13 @@
   return true;
 }
 
+TypeDeterminer::VariableInfo* TypeDeterminer::CreateVariableInfo(
+    ast::Variable* var) {
+  auto* info = variable_infos_.Create(var);
+  variable_to_info_.emplace(var, info);
+  return info;
+}
+
 bool TypeDeterminer::DetermineStorageTextureSubtype(type::StorageTexture* tex) {
   if (tex->type() != nullptr) {
     return true;
@@ -1211,22 +1231,40 @@
                              builder_->create<semantic::Expression>(type));
 }
 
-void TypeDeterminer::CreateSemanticFunctions() const {
+void TypeDeterminer::CreateSemanticNodes() const {
+  auto& sem = builder_->Sem();
+
+  for (auto it : variable_to_info_) {
+    auto* var = it.first;
+    auto* info = it.second;
+    sem.Add(var,
+            builder_->create<semantic::Variable>(var, info->storage_class));
+  }
+
+  auto remap_vars = [&sem](const std::vector<VariableInfo*>& in) {
+    std::vector<const semantic::Variable*> out;
+    out.reserve(in.size());
+    for (auto* info : in) {
+      out.emplace_back(sem.Get(info->declaration));
+    }
+    return out;
+  };
+
   for (auto it : function_to_info_) {
     auto* func = it.first;
     auto* info = it.second;
-    if (builder_->Sem().Get(func)) {
-      // ast::Function already has a semantic::Function node.
-      // This is likely via explicit call to DetermineXXX() in test.
-      continue;
-    }
-    builder_->Sem().Add(func, builder_->create<semantic::Function>(
-                                  info->referenced_module_vars,
-                                  info->local_referenced_module_vars,
-                                  info->ancestor_entry_points));
+    sem.Add(func, builder_->create<semantic::Function>(
+                      remap_vars(info->referenced_module_vars),
+                      remap_vars(info->local_referenced_module_vars),
+                      info->ancestor_entry_points));
   }
 }
 
+TypeDeterminer::VariableInfo::VariableInfo(ast::Variable* decl)
+    : declaration(decl), storage_class(decl->declared_storage_class()) {}
+
+TypeDeterminer::VariableInfo::~VariableInfo() = default;
+
 TypeDeterminer::FunctionInfo::FunctionInfo(ast::Function* decl)
     : declaration(decl) {}
 
diff --git a/src/type_determiner.h b/src/type_determiner.h
index c943a53..1402dda 100644
--- a/src/type_determiner.h
+++ b/src/type_determiner.h
@@ -66,23 +66,6 @@
   /// @returns true if the type determiner was successful
   bool Determine();
 
-  /// Creates the semantic::Function nodes and adds them to the semantic::Info
-  /// of the ProgramBuilder.
-  void CreateSemanticFunctions() const;
-
-  /// Retrieves information for the requested import.
-  /// @param src the source of the import
-  /// @param path the import path
-  /// @param name the method name to get information on
-  /// @param params the parameters to the method call
-  /// @param id out parameter for the external call ID. Must not be a nullptr.
-  /// @returns the return type of `name` in `path` or nullptr on error.
-  type::Type* GetImportData(const Source& src,
-                            const std::string& path,
-                            const std::string& name,
-                            const ast::ExpressionList& params,
-                            uint32_t* id);
-
   /// Sets the intrinsic data information for the identifier if needed
   /// @param ident the identifier expression
   /// @returns true if an intrinsic was set
@@ -99,6 +82,7 @@
         set.emplace(val);
       }
     }
+    size_t size() const { return vector.size(); }
     ConstIterator begin() const { return vector.begin(); }
     ConstIterator end() const { return vector.end(); }
     operator const std::vector<T> &() const { return vector; }
@@ -110,16 +94,31 @@
 
   /// Structure holding semantic information about a function.
   /// Used to build the semantic::Function nodes at the end of resolving.
+  struct VariableInfo {
+    explicit VariableInfo(ast::Variable* decl);
+    ~VariableInfo();
+
+    ast::Variable* const declaration;
+    ast::StorageClass storage_class;
+  };
+
+  /// Structure holding semantic information about a function.
+  /// Used to build the semantic::Function nodes at the end of resolving.
   struct FunctionInfo {
     explicit FunctionInfo(ast::Function* decl);
     ~FunctionInfo();
 
     ast::Function* const declaration;
-    UniqueVector<ast::Variable*> referenced_module_vars;
-    UniqueVector<ast::Variable*> local_referenced_module_vars;
+    UniqueVector<VariableInfo*> referenced_module_vars;
+    UniqueVector<VariableInfo*> local_referenced_module_vars;
     UniqueVector<Symbol> ancestor_entry_points;
   };
 
+  /// Determines type information for the program, without creating final the
+  /// semantic nodes.
+  /// @returns true if the determination was successful
+  bool DetermineInternal();
+
   /// Determines type information for functions
   /// @param funcs the functions to check
   /// @returns true if the determination was successful
@@ -154,8 +153,25 @@
   /// @returns false on error
   bool DetermineStorageTextureSubtype(type::StorageTexture* tex);
 
+  /// Creates the nodes and adds them to the semantic::Info mappings of the
+  /// ProgramBuilder.
+  void CreateSemanticNodes() const;
+
+  /// Retrieves information for the requested import.
+  /// @param src the source of the import
+  /// @param path the import path
+  /// @param name the method name to get information on
+  /// @param params the parameters to the method call
+  /// @param id out parameter for the external call ID. Must not be a nullptr.
+  /// @returns the return type of `name` in `path` or nullptr on error.
+  type::Type* GetImportData(const Source& src,
+                            const std::string& path,
+                            const std::string& name,
+                            const ast::ExpressionList& params,
+                            uint32_t* id);
+
   void set_error(const Source& src, const std::string& msg);
-  void set_referenced_from_function_if_needed(ast::Variable* var, bool local);
+  void set_referenced_from_function_if_needed(VariableInfo* var, bool local);
   void set_entry_points(const Symbol& fn_sym, Symbol ep_sym);
 
   bool DetermineArrayAccessor(ast::ArrayAccessorExpression* expr);
@@ -169,6 +185,8 @@
   bool DetermineMemberAccessor(ast::MemberAccessorExpression* expr);
   bool DetermineUnaryOp(ast::UnaryOpExpression* expr);
 
+  VariableInfo* CreateVariableInfo(ast::Variable*);
+
   /// @returns the resolved type of the ast::Expression `expr`
   /// @param expr the expression
   type::Type* TypeOf(ast::Expression* expr) const {
@@ -183,10 +201,12 @@
 
   ProgramBuilder* builder_;
   std::string error_;
-  ScopeStack<ast::Variable*> variable_stack_;
+  ScopeStack<VariableInfo*> variable_stack_;
   std::unordered_map<Symbol, FunctionInfo*> symbol_to_function_;
   std::unordered_map<ast::Function*, FunctionInfo*> function_to_info_;
+  std::unordered_map<ast::Variable*, VariableInfo*> variable_to_info_;
   FunctionInfo* current_function_ = nullptr;
+  BlockAllocator<VariableInfo> variable_infos_;
   BlockAllocator<FunctionInfo> function_infos_;
 
   // Map from caller functions to callee functions.
diff --git a/src/type_determiner_test.cc b/src/type_determiner_test.cc
index b97f009..96def37 100644
--- a/src/type_determiner_test.cc
+++ b/src/type_determiner_test.cc
@@ -53,6 +53,7 @@
 #include "src/program_builder.h"
 #include "src/semantic/expression.h"
 #include "src/semantic/function.h"
+#include "src/semantic/variable.h"
 #include "src/type/alias_type.h"
 #include "src/type/array_type.h"
 #include "src/type/bool_type.h"
@@ -689,11 +690,11 @@
 
   const auto& vars = func_sem->ReferencedModuleVariables();
   ASSERT_EQ(vars.size(), 5u);
-  EXPECT_EQ(vars[0], out_var);
-  EXPECT_EQ(vars[1], in_var);
-  EXPECT_EQ(vars[2], wg_var);
-  EXPECT_EQ(vars[3], sb_var);
-  EXPECT_EQ(vars[4], priv_var);
+  EXPECT_EQ(vars[0]->Declaration(), out_var);
+  EXPECT_EQ(vars[1]->Declaration(), in_var);
+  EXPECT_EQ(vars[2]->Declaration(), wg_var);
+  EXPECT_EQ(vars[3]->Declaration(), sb_var);
+  EXPECT_EQ(vars[4]->Declaration(), priv_var);
 }
 
 TEST_F(TypeDeterminerTest, Function_RegisterInputOutputVariables_SubFunction) {
@@ -726,11 +727,11 @@
 
   const auto& vars = func2_sem->ReferencedModuleVariables();
   ASSERT_EQ(vars.size(), 5u);
-  EXPECT_EQ(vars[0], out_var);
-  EXPECT_EQ(vars[1], in_var);
-  EXPECT_EQ(vars[2], wg_var);
-  EXPECT_EQ(vars[3], sb_var);
-  EXPECT_EQ(vars[4], priv_var);
+  EXPECT_EQ(vars[0]->Declaration(), out_var);
+  EXPECT_EQ(vars[1]->Declaration(), in_var);
+  EXPECT_EQ(vars[2]->Declaration(), wg_var);
+  EXPECT_EQ(vars[3]->Declaration(), sb_var);
+  EXPECT_EQ(vars[4]->Declaration(), priv_var);
 }
 
 TEST_F(TypeDeterminerTest, Function_NotRegisterFunctionVariable) {
@@ -1490,7 +1491,7 @@
 
   EXPECT_TRUE(td()->Determine()) << td()->error();
 
-  EXPECT_EQ(var->storage_class(), ast::StorageClass::kFunction);
+  EXPECT_EQ(Sem().Get(var)->StorageClass(), ast::StorageClass::kFunction);
 }
 
 TEST_F(TypeDeterminerTest, StorageClass_DoesNotSetOnConst) {
@@ -1501,7 +1502,7 @@
 
   EXPECT_TRUE(td()->Determine()) << td()->error();
 
-  EXPECT_EQ(var->storage_class(), ast::StorageClass::kNone);
+  EXPECT_EQ(Sem().Get(var)->StorageClass(), ast::StorageClass::kNone);
 }
 
 TEST_F(TypeDeterminerTest, StorageClass_NonFunctionClassError) {
diff --git a/src/validator/validator_impl.cc b/src/validator/validator_impl.cc
index 0b0b163..265ad84 100644
--- a/src/validator/validator_impl.cc
+++ b/src/validator/validator_impl.cc
@@ -31,6 +31,7 @@
 #include "src/ast/uint_literal.h"
 #include "src/ast/variable_decl_statement.h"
 #include "src/semantic/expression.h"
+#include "src/semantic/variable.h"
 #include "src/type/alias_type.h"
 #include "src/type/array_type.h"
 #include "src/type/bool_type.h"
@@ -336,19 +337,26 @@
 bool ValidatorImpl::ValidateGlobalVariables(
     const ast::VariableList& global_vars) {
   for (auto* var : global_vars) {
+    auto* sem = program_->Sem().Get(var);
+    if (!sem) {
+      add_error(var->source(), "no semantic information for variable '" +
+                                   program_->Symbols().NameFor(var->symbol()) +
+                                   "'");
+      return false;
+    }
+
     if (variable_stack_.has(var->symbol())) {
       add_error(var->source(), "v-0011",
                 "redeclared global identifier '" +
                     program_->Symbols().NameFor(var->symbol()) + "'");
       return false;
     }
-    if (!var->is_const() && var->storage_class() == ast::StorageClass::kNone) {
+    if (!var->is_const() && sem->StorageClass() == ast::StorageClass::kNone) {
       add_error(var->source(), "v-0022",
                 "global variables must have a storage class");
       return false;
     }
-    if (var->is_const() &&
-        !(var->storage_class() == ast::StorageClass::kNone)) {
+    if (var->is_const() && !(sem->StorageClass() == ast::StorageClass::kNone)) {
       add_error(var->source(), "v-global01",
                 "global constants shouldn't have a storage class");
       return false;
diff --git a/src/writer/hlsl/generator_impl.cc b/src/writer/hlsl/generator_impl.cc
index 62e52e5..6e4eff6 100644
--- a/src/writer/hlsl/generator_impl.cc
+++ b/src/writer/hlsl/generator_impl.cc
@@ -47,6 +47,7 @@
 #include "src/program_builder.h"
 #include "src/semantic/expression.h"
 #include "src/semantic/function.h"
+#include "src/semantic/variable.h"
 #include "src/type/access_control_type.h"
 #include "src/type/alias_type.h"
 #include "src/type/array_type.h"
@@ -194,7 +195,8 @@
 }
 
 void GeneratorImpl::register_global(ast::Variable* global) {
-  global_variables_.set(global->symbol(), global);
+  auto* sem = builder_.Sem().Get(global);
+  global_variables_.set(global->symbol(), sem);
 }
 
 std::string GeneratorImpl::generate_name(const std::string& prefix) {
@@ -1134,10 +1136,11 @@
   return false;
 }
 
-bool GeneratorImpl::global_is_in_struct(ast::Variable* var) const {
-  if (var->HasLocationDecoration() || var->HasBuiltinDecoration()) {
-    return var->storage_class() == ast::StorageClass::kInput ||
-           var->storage_class() == ast::StorageClass::kOutput;
+bool GeneratorImpl::global_is_in_struct(const semantic::Variable* var) const {
+  if (var->Declaration()->HasLocationDecoration() ||
+      var->Declaration()->HasBuiltinDecoration()) {
+    return var->StorageClass() == ast::StorageClass::kInput ||
+           var->StorageClass() == ast::StorageClass::kOutput;
   }
   return false;
 }
@@ -1146,10 +1149,10 @@
                                    std::ostream& out,
                                    ast::IdentifierExpression* expr) {
   auto* ident = expr->As<ast::IdentifierExpression>();
-  ast::Variable* var = nullptr;
+  const semantic::Variable* var = nullptr;
   if (global_variables_.get(ident->symbol(), &var)) {
     if (global_is_in_struct(var)) {
-      auto var_type = var->storage_class() == ast::StorageClass::kInput
+      auto var_type = var->StorageClass() == ast::StorageClass::kInput
                           ? VarType::kIn
                           : VarType::kOut;
       auto name = current_ep_var_name(var_type);
@@ -1230,14 +1233,14 @@
     const semantic::Function* func) {
   for (auto data : func->ReferencedLocationVariables()) {
     auto* var = data.first;
-    if (var->storage_class() == ast::StorageClass::kInput) {
+    if (var->StorageClass() == ast::StorageClass::kInput) {
       return true;
     }
   }
 
   for (auto data : func->ReferencedBuiltinVariables()) {
     auto* var = data.first;
-    if (var->storage_class() == ast::StorageClass::kInput) {
+    if (var->StorageClass() == ast::StorageClass::kInput) {
       return true;
     }
   }
@@ -1248,14 +1251,14 @@
     const semantic::Function* func) {
   for (auto data : func->ReferencedLocationVariables()) {
     auto* var = data.first;
-    if (var->storage_class() == ast::StorageClass::kOutput) {
+    if (var->StorageClass() == ast::StorageClass::kOutput) {
       return true;
     }
   }
 
   for (auto data : func->ReferencedBuiltinVariables()) {
     auto* var = data.first;
-    if (var->storage_class() == ast::StorageClass::kOutput) {
+    if (var->StorageClass() == ast::StorageClass::kOutput) {
       return true;
     }
   }
@@ -1266,16 +1269,16 @@
     const semantic::Function* func) {
   for (auto data : func->ReferencedLocationVariables()) {
     auto* var = data.first;
-    if (var->storage_class() == ast::StorageClass::kOutput ||
-        var->storage_class() == ast::StorageClass::kInput) {
+    if (var->StorageClass() == ast::StorageClass::kOutput ||
+        var->StorageClass() == ast::StorageClass::kInput) {
       return true;
     }
   }
 
   for (auto data : func->ReferencedBuiltinVariables()) {
     auto* var = data.first;
-    if (var->storage_class() == ast::StorageClass::kOutput ||
-        var->storage_class() == ast::StorageClass::kInput) {
+    if (var->StorageClass() == ast::StorageClass::kOutput ||
+        var->StorageClass() == ast::StorageClass::kInput) {
       return true;
     }
   }
@@ -1407,51 +1410,54 @@
 
   for (auto data : func_sem->ReferencedLocationVariables()) {
     auto* var = data.first;
+    auto* decl = var->Declaration();
     auto* deco = data.second;
 
-    if (var->storage_class() == ast::StorageClass::kInput) {
-      in_variables.push_back({var, deco});
-    } else if (var->storage_class() == ast::StorageClass::kOutput) {
-      outvariables.push_back({var, deco});
+    if (var->StorageClass() == ast::StorageClass::kInput) {
+      in_variables.push_back({decl, deco});
+    } else if (var->StorageClass() == ast::StorageClass::kOutput) {
+      outvariables.push_back({decl, deco});
     }
   }
 
   for (auto data : func_sem->ReferencedBuiltinVariables()) {
     auto* var = data.first;
+    auto* decl = var->Declaration();
     auto* deco = data.second;
 
-    if (var->storage_class() == ast::StorageClass::kInput) {
-      in_variables.push_back({var, deco});
-    } else if (var->storage_class() == ast::StorageClass::kOutput) {
-      outvariables.push_back({var, deco});
+    if (var->StorageClass() == ast::StorageClass::kInput) {
+      in_variables.push_back({decl, deco});
+    } else if (var->StorageClass() == ast::StorageClass::kOutput) {
+      outvariables.push_back({decl, deco});
     }
   }
 
   bool emitted_uniform = false;
   for (auto data : func_sem->ReferencedUniformVariables()) {
     auto* var = data.first;
+    auto* decl = var->Declaration();
     // TODO(dsinclair): We're using the binding to make up the buffer number but
     // we should instead be using a provided mapping that uses both buffer and
     // set. https://bugs.chromium.org/p/tint/issues/detail?id=104
     auto* binding = data.second.binding;
     if (binding == nullptr) {
       error_ = "unable to find binding information for uniform: " +
-               builder_.Symbols().NameFor(var->symbol());
+               builder_.Symbols().NameFor(decl->symbol());
       return false;
     }
     // auto* set = data.second.set;
 
     // If the global has already been emitted we skip it, it's been emitted by
     // a previous entry point.
-    if (emitted_globals.count(var->symbol()) != 0) {
+    if (emitted_globals.count(decl->symbol()) != 0) {
       continue;
     }
-    emitted_globals.insert(var->symbol());
+    emitted_globals.insert(decl->symbol());
 
-    auto* type = var->type()->UnwrapIfNeeded();
+    auto* type = decl->type()->UnwrapIfNeeded();
     if (auto* strct = type->As<type::Struct>()) {
       out << "ConstantBuffer<" << builder_.Symbols().NameFor(strct->symbol())
-          << "> " << builder_.Symbols().NameFor(var->symbol())
+          << "> " << builder_.Symbols().NameFor(decl->symbol())
           << " : register(b" << binding->value() << ");" << std::endl;
     } else {
       // TODO(dsinclair): There is outstanding spec work to require all uniform
@@ -1460,7 +1466,7 @@
       // is not a block.
       // Relevant: https://github.com/gpuweb/gpuweb/issues/1004
       //           https://github.com/gpuweb/gpuweb/issues/1008
-      auto name = "cbuffer_" + builder_.Symbols().NameFor(var->symbol());
+      auto name = "cbuffer_" + builder_.Symbols().NameFor(decl->symbol());
       out << "cbuffer " << name << " : register(b" << binding->value() << ") {"
           << std::endl;
 
@@ -1469,7 +1475,7 @@
       if (!EmitType(out, type, "")) {
         return false;
       }
-      out << " " << builder_.Symbols().NameFor(var->symbol()) << ";"
+      out << " " << builder_.Symbols().NameFor(decl->symbol()) << ";"
           << std::endl;
       decrement_indent();
       out << "};" << std::endl;
@@ -1484,16 +1490,17 @@
   bool emitted_storagebuffer = false;
   for (auto data : func_sem->ReferencedStoragebufferVariables()) {
     auto* var = data.first;
+    auto* decl = var->Declaration();
     auto* binding = data.second.binding;
 
     // If the global has already been emitted we skip it, it's been emitted by
     // a previous entry point.
-    if (emitted_globals.count(var->symbol()) != 0) {
+    if (emitted_globals.count(decl->symbol()) != 0) {
       continue;
     }
-    emitted_globals.insert(var->symbol());
+    emitted_globals.insert(decl->symbol());
 
-    auto* ac = var->type()->As<type::AccessControl>();
+    auto* ac = decl->type()->As<type::AccessControl>();
     if (ac == nullptr) {
       error_ = "access control type required for storage buffer";
       return false;
@@ -1502,7 +1509,7 @@
     if (ac->IsReadWrite()) {
       out << "RW";
     }
-    out << "ByteAddressBuffer " << builder_.Symbols().NameFor(var->symbol())
+    out << "ByteAddressBuffer " << builder_.Symbols().NameFor(decl->symbol())
         << " : register(u" << binding->value() << ");" << std::endl;
     emitted_storagebuffer = true;
   }
@@ -2069,11 +2076,11 @@
 
   // Check if this is a storage buffer variable
   if (auto* ident = expr->structure()->As<ast::IdentifierExpression>()) {
-    ast::Variable* var = nullptr;
+    const semantic::Variable* var = nullptr;
     if (!global_variables_.get(ident->symbol(), &var)) {
       return false;
     }
-    return var->storage_class() == ast::StorageClass::kStorage;
+    return var->StorageClass() == ast::StorageClass::kStorage;
   } else if (auto* member = structure->As<ast::MemberAccessorExpression>()) {
     return is_storage_buffer_access(member);
   } else if (auto* array = structure->As<ast::ArrayAccessorExpression>()) {
diff --git a/src/writer/hlsl/generator_impl.h b/src/writer/hlsl/generator_impl.h
index 24089ae..0c175fc 100644
--- a/src/writer/hlsl/generator_impl.h
+++ b/src/writer/hlsl/generator_impl.h
@@ -338,7 +338,7 @@
   /// Checks if the global variable is in an input or output struct
   /// @param var the variable to check
   /// @returns true if the global is in an input or output struct
-  bool global_is_in_struct(ast::Variable* var) const;
+  bool global_is_in_struct(const semantic::Variable* var) const;
   /// Creates a text string representing the index into a storage buffer
   /// @param pre the pre stream
   /// @param expr the expression to use as the index
@@ -400,7 +400,7 @@
   Symbol current_ep_sym_;
   bool generating_entry_point_ = false;
   uint32_t loop_emission_counter_ = 0;
-  ScopeStack<ast::Variable*> global_variables_;
+  ScopeStack<const semantic::Variable*> global_variables_;
   std::unordered_map<Symbol, EntryPointData> ep_sym_to_in_data_;
   std::unordered_map<Symbol, EntryPointData> ep_sym_to_out_data_;
 
diff --git a/src/writer/msl/generator_impl.cc b/src/writer/msl/generator_impl.cc
index 0f9d249..be67f4d 100644
--- a/src/writer/msl/generator_impl.cc
+++ b/src/writer/msl/generator_impl.cc
@@ -52,6 +52,7 @@
 #include "src/program.h"
 #include "src/semantic/expression.h"
 #include "src/semantic/function.h"
+#include "src/semantic/variable.h"
 #include "src/type/access_control_type.h"
 #include "src/type/alias_type.h"
 #include "src/type/array_type.h"
@@ -120,7 +121,8 @@
   out_ << "#include <metal_stdlib>" << std::endl << std::endl;
 
   for (auto* global : program_->AST().GlobalVariables()) {
-    global_variables_.set(global->symbol(), global);
+    auto* sem = program_->Sem().Get(global);
+    global_variables_.set(global->symbol(), sem);
   }
 
   for (auto* const ty : program_->AST().ConstructedTypes()) {
@@ -508,14 +510,14 @@
   auto* func_sem = program_->Sem().Get(func);
   for (const auto& data : func_sem->ReferencedBuiltinVariables()) {
     auto* var = data.first;
-    if (var->storage_class() != ast::StorageClass::kInput) {
+    if (var->StorageClass() != ast::StorageClass::kInput) {
       continue;
     }
     if (!first) {
       out_ << ", ";
     }
     first = false;
-    out_ << program_->Symbols().NameFor(var->symbol());
+    out_ << program_->Symbols().NameFor(var->Declaration()->symbol());
   }
 
   for (const auto& data : func_sem->ReferencedUniformVariables()) {
@@ -524,7 +526,7 @@
       out_ << ", ";
     }
     first = false;
-    out_ << program_->Symbols().NameFor(var->symbol());
+    out_ << program_->Symbols().NameFor(var->Declaration()->symbol());
   }
 
   for (const auto& data : func_sem->ReferencedStoragebufferVariables()) {
@@ -533,7 +535,7 @@
       out_ << ", ";
     }
     first = false;
-    out_ << program_->Symbols().NameFor(var->symbol());
+    out_ << program_->Symbols().NameFor(var->Declaration()->symbol());
   }
 
   const auto& params = expr->params();
@@ -1033,10 +1035,10 @@
     auto* var = data.first;
     auto* deco = data.second;
 
-    if (var->storage_class() == ast::StorageClass::kInput) {
-      in_locations.push_back({var, deco->value()});
-    } else if (var->storage_class() == ast::StorageClass::kOutput) {
-      out_variables.push_back({var, deco});
+    if (var->StorageClass() == ast::StorageClass::kInput) {
+      in_locations.push_back({var->Declaration(), deco->value()});
+    } else if (var->StorageClass() == ast::StorageClass::kOutput) {
+      out_variables.push_back({var->Declaration(), deco});
     }
   }
 
@@ -1044,8 +1046,8 @@
     auto* var = data.first;
     auto* deco = data.second;
 
-    if (var->storage_class() == ast::StorageClass::kOutput) {
-      out_variables.push_back({var, deco});
+    if (var->StorageClass() == ast::StorageClass::kOutput) {
+      out_variables.push_back({var->Declaration(), deco});
     }
   }
 
@@ -1191,7 +1193,7 @@
   auto* func_sem = program_->Sem().Get(func);
   for (auto data : func_sem->ReferencedLocationVariables()) {
     auto* var = data.first;
-    if (var->storage_class() == ast::StorageClass::kInput) {
+    if (var->StorageClass() == ast::StorageClass::kInput) {
       return true;
     }
   }
@@ -1203,14 +1205,14 @@
 
   for (auto data : func_sem->ReferencedLocationVariables()) {
     auto* var = data.first;
-    if (var->storage_class() == ast::StorageClass::kOutput) {
+    if (var->StorageClass() == ast::StorageClass::kOutput) {
       return true;
     }
   }
 
   for (auto data : func_sem->ReferencedBuiltinVariables()) {
     auto* var = data.first;
-    if (var->storage_class() == ast::StorageClass::kOutput) {
+    if (var->StorageClass() == ast::StorageClass::kOutput) {
       return true;
     }
   }
@@ -1308,7 +1310,7 @@
 
   for (const auto& data : func_sem->ReferencedBuiltinVariables()) {
     auto* var = data.first;
-    if (var->storage_class() != ast::StorageClass::kInput) {
+    if (var->StorageClass() != ast::StorageClass::kInput) {
       continue;
     }
     if (!first) {
@@ -1317,10 +1319,10 @@
     first = false;
 
     out_ << "thread ";
-    if (!EmitType(var->type(), "")) {
+    if (!EmitType(var->Declaration()->type(), "")) {
       return false;
     }
-    out_ << "& " << program_->Symbols().NameFor(var->symbol());
+    out_ << "& " << program_->Symbols().NameFor(var->Declaration()->symbol());
   }
 
   for (const auto& data : func_sem->ReferencedUniformVariables()) {
@@ -1332,10 +1334,10 @@
 
     out_ << "constant ";
     // TODO(dsinclair): Can arrays be uniform? If so, fix this ...
-    if (!EmitType(var->type(), "")) {
+    if (!EmitType(var->Declaration()->type(), "")) {
       return false;
     }
-    out_ << "& " << program_->Symbols().NameFor(var->symbol());
+    out_ << "& " << program_->Symbols().NameFor(var->Declaration()->symbol());
   }
 
   for (const auto& data : func_sem->ReferencedStoragebufferVariables()) {
@@ -1345,7 +1347,7 @@
     }
     first = false;
 
-    auto* ac = var->type()->As<type::AccessControl>();
+    auto* ac = var->Declaration()->type()->As<type::AccessControl>();
     if (ac == nullptr) {
       error_ = "invalid type for storage buffer, expected access control";
       return false;
@@ -1358,7 +1360,7 @@
     if (!EmitType(ac->type(), "")) {
       return false;
     }
-    out_ << "& " << program_->Symbols().NameFor(var->symbol());
+    out_ << "& " << program_->Symbols().NameFor(var->Declaration()->symbol());
   }
 
   for (auto* v : func->params()) {
@@ -1447,7 +1449,7 @@
 
   for (auto data : func_sem->ReferencedBuiltinVariables()) {
     auto* var = data.first;
-    if (var->storage_class() != ast::StorageClass::kInput) {
+    if (var->StorageClass() != ast::StorageClass::kInput) {
       continue;
     }
 
@@ -1458,7 +1460,7 @@
 
     auto* builtin = data.second;
 
-    if (!EmitType(var->type(), "")) {
+    if (!EmitType(var->Declaration()->type(), "")) {
       return false;
     }
 
@@ -1467,8 +1469,8 @@
       error_ = "unknown builtin";
       return false;
     }
-    out_ << " " << program_->Symbols().NameFor(var->symbol()) << " [[" << attr
-         << "]]";
+    out_ << " " << program_->Symbols().NameFor(var->Declaration()->symbol())
+         << " [[" << attr << "]]";
   }
 
   for (auto data : func_sem->ReferencedUniformVariables()) {
@@ -1484,7 +1486,7 @@
     auto* binding = data.second.binding;
     if (binding == nullptr) {
       error_ = "unable to find binding information for uniform: " +
-               program_->Symbols().NameFor(var->symbol());
+               program_->Symbols().NameFor(var->Declaration()->symbol());
       return false;
     }
     // auto* set = data.second.set;
@@ -1492,11 +1494,11 @@
     out_ << "constant ";
     // TODO(dsinclair): Can you have a uniform array? If so, this needs to be
     // updated to handle arrays property.
-    if (!EmitType(var->type(), "")) {
+    if (!EmitType(var->Declaration()->type(), "")) {
       return false;
     }
-    out_ << "& " << program_->Symbols().NameFor(var->symbol()) << " [[buffer("
-         << binding->value() << ")]]";
+    out_ << "& " << program_->Symbols().NameFor(var->Declaration()->symbol())
+         << " [[buffer(" << binding->value() << ")]]";
   }
 
   for (auto data : func_sem->ReferencedStoragebufferVariables()) {
@@ -1512,7 +1514,7 @@
     auto* binding = data.second.binding;
     // auto* set = data.second.set;
 
-    auto* ac = var->type()->As<type::AccessControl>();
+    auto* ac = var->Declaration()->type()->As<type::AccessControl>();
     if (ac == nullptr) {
       error_ = "invalid type for storage buffer, expected access control";
       return false;
@@ -1525,8 +1527,8 @@
     if (!EmitType(ac->type(), "")) {
       return false;
     }
-    out_ << "& " << program_->Symbols().NameFor(var->symbol()) << " [[buffer("
-         << binding->value() << ")]]";
+    out_ << "& " << program_->Symbols().NameFor(var->Declaration()->symbol())
+         << " [[buffer(" << binding->value() << ")]]";
   }
 
   out_ << ") {" << std::endl;
@@ -1563,23 +1565,23 @@
   return true;
 }
 
-bool GeneratorImpl::global_is_in_struct(ast::Variable* var) const {
+bool GeneratorImpl::global_is_in_struct(const semantic::Variable* var) const {
   bool in_or_out_struct_has_location =
-      var != nullptr && var->HasLocationDecoration() &&
-      (var->storage_class() == ast::StorageClass::kInput ||
-       var->storage_class() == ast::StorageClass::kOutput);
+      var != nullptr && var->Declaration()->HasLocationDecoration() &&
+      (var->StorageClass() == ast::StorageClass::kInput ||
+       var->StorageClass() == ast::StorageClass::kOutput);
   bool in_struct_has_builtin =
-      var != nullptr && var->HasBuiltinDecoration() &&
-      var->storage_class() == ast::StorageClass::kOutput;
+      var != nullptr && var->Declaration()->HasBuiltinDecoration() &&
+      var->StorageClass() == ast::StorageClass::kOutput;
   return in_or_out_struct_has_location || in_struct_has_builtin;
 }
 
 bool GeneratorImpl::EmitIdentifier(ast::IdentifierExpression* expr) {
   auto* ident = expr->As<ast::IdentifierExpression>();
-  ast::Variable* var = nullptr;
+  const semantic::Variable* var = nullptr;
   if (global_variables_.get(ident->symbol(), &var)) {
     if (global_is_in_struct(var)) {
-      auto var_type = var->storage_class() == ast::StorageClass::kInput
+      auto var_type = var->StorageClass() == ast::StorageClass::kInput
                           ? VarType::kIn
                           : VarType::kOut;
       auto name = current_ep_var_name(var_type);
@@ -1623,7 +1625,7 @@
     // will be turned into assignments.
     for (auto* s : *(stmt->body())) {
       if (auto* decl = s->As<ast::VariableDeclStatement>()) {
-        if (!EmitVariable(decl->variable(), true)) {
+        if (!EmitVariable(program_->Sem().Get(decl->variable()), true)) {
           return false;
         }
       }
@@ -1839,7 +1841,8 @@
     return EmitSwitch(s);
   }
   if (auto* v = stmt->As<ast::VariableDeclStatement>()) {
-    return EmitVariable(v->variable(), false);
+    auto* var = program_->Sem().Get(v->variable());
+    return EmitVariable(var, false);
   }
 
   error_ = "unknown statement type: " + program_->str(stmt);
@@ -2078,35 +2081,38 @@
   return true;
 }
 
-bool GeneratorImpl::EmitVariable(ast::Variable* var, bool skip_constructor) {
+bool GeneratorImpl::EmitVariable(const semantic::Variable* var,
+                                 bool skip_constructor) {
   make_indent();
 
+  auto* decl = var->Declaration();
+
   // TODO(dsinclair): Handle variable decorations
-  if (!var->decorations().empty()) {
+  if (!decl->decorations().empty()) {
     error_ = "Variable decorations are not handled yet";
     return false;
   }
-  if (var->is_const()) {
+  if (decl->is_const()) {
     out_ << "const ";
   }
-  if (!EmitType(var->type(), program_->Symbols().NameFor(var->symbol()))) {
+  if (!EmitType(decl->type(), program_->Symbols().NameFor(decl->symbol()))) {
     return false;
   }
-  if (!var->type()->Is<type::Array>()) {
-    out_ << " " << program_->Symbols().NameFor(var->symbol());
+  if (!decl->type()->Is<type::Array>()) {
+    out_ << " " << program_->Symbols().NameFor(decl->symbol());
   }
 
   if (!skip_constructor) {
     out_ << " = ";
-    if (var->constructor() != nullptr) {
-      if (!EmitExpression(var->constructor())) {
+    if (decl->constructor() != nullptr) {
+      if (!EmitExpression(decl->constructor())) {
         return false;
       }
-    } else if (var->storage_class() == ast::StorageClass::kPrivate ||
-               var->storage_class() == ast::StorageClass::kFunction ||
-               var->storage_class() == ast::StorageClass::kNone ||
-               var->storage_class() == ast::StorageClass::kOutput) {
-      if (!EmitZeroValue(var->type())) {
+    } else if (var->StorageClass() == ast::StorageClass::kPrivate ||
+               var->StorageClass() == ast::StorageClass::kFunction ||
+               var->StorageClass() == ast::StorageClass::kNone ||
+               var->StorageClass() == ast::StorageClass::kOutput) {
+      if (!EmitZeroValue(decl->type())) {
         return false;
       }
     }
diff --git a/src/writer/msl/generator_impl.h b/src/writer/msl/generator_impl.h
index d67b54d..0aafa62 100644
--- a/src/writer/msl/generator_impl.h
+++ b/src/writer/msl/generator_impl.h
@@ -221,7 +221,7 @@
   /// @param var the variable to generate
   /// @param skip_constructor set true if the constructor should be skipped
   /// @returns true if the variable was emitted
-  bool EmitVariable(ast::Variable* var, bool skip_constructor);
+  bool EmitVariable(const semantic::Variable* var, bool skip_constructor);
   /// Handles generating a program scope constant variable
   /// @param var the variable to emit
   /// @returns true if the variable was emitted
@@ -256,7 +256,7 @@
   /// Checks if the global variable is in an input or output struct
   /// @param var the variable to check
   /// @returns true if the global is in an input or output struct
-  bool global_is_in_struct(ast::Variable* var) const;
+  bool global_is_in_struct(const semantic::Variable* var) const;
 
   /// Converts a builtin to an attribute name
   /// @param builtin the builtin to convert
@@ -283,7 +283,7 @@
   }
 
   Namer namer_;
-  ScopeStack<ast::Variable*> global_variables_;
+  ScopeStack<const semantic::Variable*> global_variables_;
   Symbol current_ep_sym_;
   bool generating_entry_point_ = false;
   const Program* program_ = nullptr;
diff --git a/src/writer/spirv/builder.cc b/src/writer/spirv/builder.cc
index 645c6b4..79c71e8 100644
--- a/src/writer/spirv/builder.cc
+++ b/src/writer/spirv/builder.cc
@@ -61,6 +61,7 @@
 #include "src/program.h"
 #include "src/semantic/expression.h"
 #include "src/semantic/function.h"
+#include "src/semantic/variable.h"
 #include "src/type/access_control_type.h"
 #include "src/type/alias_type.h"
 #include "src/type/array_type.h"
@@ -462,15 +463,15 @@
   for (const auto* var : func_sem->ReferencedModuleVariables()) {
     // For SPIR-V 1.3 we only output Input/output variables. If we update to
     // SPIR-V 1.4 or later this should be all variables.
-    if (var->storage_class() != ast::StorageClass::kInput &&
-        var->storage_class() != ast::StorageClass::kOutput) {
+    if (var->StorageClass() != ast::StorageClass::kInput &&
+        var->StorageClass() != ast::StorageClass::kOutput) {
       continue;
     }
 
     uint32_t var_id;
-    if (!scope_stack_.get(var->symbol(), &var_id)) {
+    if (!scope_stack_.get(var->Declaration()->symbol(), &var_id)) {
       error_ = "unable to find ID for global variable: " +
-               builder_.Symbols().NameFor(var->symbol());
+               builder_.Symbols().NameFor(var->Declaration()->symbol());
       return false;
     }
 
@@ -700,6 +701,8 @@
 }
 
 bool Builder::GenerateGlobalVariable(ast::Variable* var) {
+  auto* sem = builder_.Sem().Get(var);
+
   uint32_t init_id = 0;
   if (var->has_constructor()) {
     if (!var->constructor()->Is<ast::ConstructorExpression>()) {
@@ -731,9 +734,9 @@
   auto result = result_op();
   auto var_id = result.to_i();
 
-  auto sc = var->storage_class() == ast::StorageClass::kNone
+  auto sc = sem->StorageClass() == ast::StorageClass::kNone
                 ? ast::StorageClass::kPrivate
-                : var->storage_class();
+                : sem->StorageClass();
 
   type::Pointer pt(var->type(), sc);
   auto type_id = GenerateTypeIfNeeded(&pt);
@@ -799,9 +802,9 @@
         return 0;
       }
       ops.push_back(Operand::Int(init_id));
-    } else if (var->storage_class() == ast::StorageClass::kPrivate ||
-               var->storage_class() == ast::StorageClass::kNone ||
-               var->storage_class() == ast::StorageClass::kOutput) {
+    } else if (sem->StorageClass() == ast::StorageClass::kPrivate ||
+               sem->StorageClass() == ast::StorageClass::kNone ||
+               sem->StorageClass() == ast::StorageClass::kOutput) {
       ast::NullLiteral nl(Source{}, type);
       init_id = GenerateLiteralIfNeeded(var, &nl);
       if (init_id == 0) {
diff --git a/src/writer/wgsl/generator_impl.cc b/src/writer/wgsl/generator_impl.cc
index 7410675..3025ffd 100644
--- a/src/writer/wgsl/generator_impl.cc
+++ b/src/writer/wgsl/generator_impl.cc
@@ -59,6 +59,7 @@
 #include "src/ast/workgroup_decoration.h"
 #include "src/program.h"
 #include "src/semantic/function.h"
+#include "src/semantic/variable.h"
 #include "src/type/access_control_type.h"
 #include "src/type/alias_type.h"
 #include "src/type/array_type.h"
@@ -148,7 +149,7 @@
 
   bool found_func_variable = false;
   for (auto* var : program_->Sem().Get(func)->ReferencedModuleVariables()) {
-    if (!EmitVariable(var)) {
+    if (!EmitVariable(var->Declaration())) {
       return false;
     }
     found_func_variable = true;
@@ -583,9 +584,11 @@
 }
 
 bool GeneratorImpl::EmitVariable(ast::Variable* var) {
+  auto* sem = program_->Sem().Get(var);
+
   make_indent();
 
-  if (!var->decorations().empty() && !EmitVariableDecorations(var)) {
+  if (!var->decorations().empty() && !EmitVariableDecorations(sem)) {
     return false;
   }
 
@@ -593,9 +596,9 @@
     out_ << "const";
   } else {
     out_ << "var";
-    if (var->storage_class() != ast::StorageClass::kNone &&
-        var->storage_class() != ast::StorageClass::kFunction) {
-      out_ << "<" << var->storage_class() << ">";
+    if (sem->StorageClass() != ast::StorageClass::kNone &&
+        sem->StorageClass() != ast::StorageClass::kFunction) {
+      out_ << "<" << sem->StorageClass() << ">";
     }
   }
 
@@ -615,10 +618,12 @@
   return true;
 }
 
-bool GeneratorImpl::EmitVariableDecorations(ast::Variable* var) {
+bool GeneratorImpl::EmitVariableDecorations(const semantic::Variable* var) {
+  auto* decl = var->Declaration();
+
   out_ << "[[";
   bool first = true;
-  for (auto* deco : var->decorations()) {
+  for (auto* deco : decl->decorations()) {
     if (!first) {
       out_ << ", ";
     }
diff --git a/src/writer/wgsl/generator_impl.h b/src/writer/wgsl/generator_impl.h
index fa4603b..34ad398 100644
--- a/src/writer/wgsl/generator_impl.h
+++ b/src/writer/wgsl/generator_impl.h
@@ -202,7 +202,7 @@
   /// Handles generating variable decorations
   /// @param var the decorated variable
   /// @returns true if the variable decoration was emitted
-  bool EmitVariableDecorations(ast::Variable* var);
+  bool EmitVariableDecorations(const semantic::Variable* var);
 
  private:
   Program const* const program_;