sem: Split sem::Variable into global, local and parameter

Each of these may contain information specific to their kind.

Change-Id: Ic8ac808088132b7bc2e43da6ce46a06571e0fed5
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/59200
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Ryan Harrison <rharrison@chromium.org>
Reviewed-by: James Price <jrprice@google.com>
diff --git a/src/ast/identifier_expression.h b/src/ast/identifier_expression.h
index da32342..3deff16 100644
--- a/src/ast/identifier_expression.h
+++ b/src/ast/identifier_expression.h
@@ -16,7 +16,6 @@
 #define SRC_AST_IDENTIFIER_EXPRESSION_H_
 
 #include "src/ast/expression.h"
-#include "src/sem/intrinsic.h"
 
 namespace tint {
 namespace ast {
diff --git a/src/inspector/inspector.cc b/src/inspector/inspector.cc
index 1003e0f..0e64db7 100644
--- a/src/inspector/inspector.cc
+++ b/src/inspector/inspector.cc
@@ -174,7 +174,8 @@
 
       auto name = program_->Symbols().NameFor(decl->symbol());
 
-      if (var->IsPipelineConstant()) {
+      auto* global = var->As<sem::GlobalVariable>();
+      if (global && global->IsPipelineConstant()) {
         OverridableConstant overridable_constant;
         overridable_constant.name = name;
         entry_point.overridable_constants.push_back(overridable_constant);
@@ -203,8 +204,8 @@
 std::map<uint32_t, Scalar> Inspector::GetConstantIDs() {
   std::map<uint32_t, Scalar> result;
   for (auto* var : program_->AST().GlobalVariables()) {
-    auto* sem_var = program_->Sem().Get(var);
-    if (!sem_var->IsPipelineConstant()) {
+    auto* global = program_->Sem().Get<sem::GlobalVariable>(var);
+    if (!global || !global->IsPipelineConstant()) {
       continue;
     }
 
@@ -212,7 +213,7 @@
     // WGSL, so the resolver should catch it. Thus here the inspector just
     // assumes all definitions of the constant id are the same, so only needs
     // to find the first reference to constant id.
-    uint32_t constant_id = sem_var->ConstantId();
+    uint32_t constant_id = global->ConstantId();
     if (result.find(constant_id) != result.end()) {
       continue;
     }
@@ -274,10 +275,10 @@
 std::map<std::string, uint32_t> Inspector::GetConstantNameToIdMap() {
   std::map<std::string, uint32_t> result;
   for (auto* var : program_->AST().GlobalVariables()) {
-    auto* sem_var = program_->Sem().Get(var);
-    if (sem_var->IsPipelineConstant()) {
+    auto* global = program_->Sem().Get<sem::GlobalVariable>(var);
+    if (global && global->IsPipelineConstant()) {
       auto name = program_->Symbols().NameFor(var->symbol());
-      result[name] = sem_var->ConstantId();
+      result[name] = global->ConstantId();
     }
   }
   return result;
diff --git a/src/inspector/inspector_test.cc b/src/inspector/inspector_test.cc
index 7d7740a..40fc591 100644
--- a/src/inspector/inspector_test.cc
+++ b/src/inspector/inspector_test.cc
@@ -928,16 +928,19 @@
   EXPECT_EQ(result["v300"], 300u);
 
   ASSERT_TRUE(result.count("a"));
-  ASSERT_TRUE(program_->Sem().Get(a));
-  EXPECT_EQ(result["a"], program_->Sem().Get(a)->ConstantId());
+  ASSERT_TRUE(program_->Sem().Get<sem::GlobalVariable>(a));
+  EXPECT_EQ(result["a"],
+            program_->Sem().Get<sem::GlobalVariable>(a)->ConstantId());
 
   ASSERT_TRUE(result.count("b"));
-  ASSERT_TRUE(program_->Sem().Get(b));
-  EXPECT_EQ(result["b"], program_->Sem().Get(b)->ConstantId());
+  ASSERT_TRUE(program_->Sem().Get<sem::GlobalVariable>(b));
+  EXPECT_EQ(result["b"],
+            program_->Sem().Get<sem::GlobalVariable>(b)->ConstantId());
 
   ASSERT_TRUE(result.count("c"));
-  ASSERT_TRUE(program_->Sem().Get(c));
-  EXPECT_EQ(result["c"], program_->Sem().Get(c)->ConstantId());
+  ASSERT_TRUE(program_->Sem().Get<sem::GlobalVariable>(c));
+  EXPECT_EQ(result["c"],
+            program_->Sem().Get<sem::GlobalVariable>(c)->ConstantId());
 }
 
 TEST_F(InspectorGetStorageSizeTest, Empty) {
diff --git a/src/intrinsic_table.cc b/src/intrinsic_table.cc
index 233aff4..4f1fa28 100644
--- a/src/intrinsic_table.cc
+++ b/src/intrinsic_table.cc
@@ -701,6 +701,55 @@
 
 #include "intrinsic_table.inl"
 
+/// IntrinsicPrototype describes a fully matched intrinsic function, which is
+/// used as a lookup for building unique sem::Intrinsic instances.
+struct IntrinsicPrototype {
+  /// Parameter describes a single parameter
+  struct Parameter {
+    /// Parameter type
+    sem::Type* const type;
+    /// Parameter usage
+    ParameterUsage const usage = ParameterUsage::kNone;
+  };
+
+  /// Hasher provides a hash function for the IntrinsicPrototype
+  struct Hasher {
+    /// @param i the IntrinsicPrototype to create a hash for
+    /// @return the hash value
+    inline std::size_t operator()(const IntrinsicPrototype& i) const {
+      size_t hash = utils::Hash(i.parameters.size());
+      for (auto& p : i.parameters) {
+        utils::HashCombine(&hash, p.type, p.usage);
+      }
+      return utils::Hash(hash, i.type, i.return_type, i.supported_stages,
+                         i.is_deprecated);
+    }
+  };
+
+  sem::IntrinsicType type = sem::IntrinsicType::kNone;
+  std::vector<Parameter> parameters;
+  sem::Type const* return_type = nullptr;
+  PipelineStageSet supported_stages;
+  bool is_deprecated = false;
+};
+
+/// Equality operator for IntrinsicPrototype
+bool operator==(const IntrinsicPrototype& a, const IntrinsicPrototype& b) {
+  if (a.type != b.type || a.supported_stages != b.supported_stages ||
+      a.return_type != b.return_type || a.is_deprecated != b.is_deprecated ||
+      a.parameters.size() != b.parameters.size()) {
+    return false;
+  }
+  for (size_t i = 0; i < a.parameters.size(); i++) {
+    auto& pa = a.parameters[i];
+    auto& pb = b.parameters[i];
+    if (pa.type != pb.type || pa.usage != pb.usage) {
+      return false;
+    }
+  }
+  return true;
+}
+
 /// Impl is the private implementation of the IntrinsicTable interface.
 class Impl : public IntrinsicTable {
  public:
@@ -726,7 +775,10 @@
 
   ProgramBuilder& builder;
   Matchers matchers;
-  std::unordered_map<sem::Intrinsic, sem::Intrinsic*> intrinsics;
+  std::unordered_map<IntrinsicPrototype,
+                     sem::Intrinsic*,
+                     IntrinsicPrototype::Hasher>
+      intrinsics;
 };
 
 /// @return a string representing a call to an intrinsic with the given argument
@@ -833,7 +885,7 @@
 
   ClosedState closed(builder);
 
-  sem::ParameterList parameters;
+  std::vector<IntrinsicPrototype::Parameter> parameters;
 
   auto num_params = std::min(num_parameters, num_arguments);
   for (uint32_t p = 0; p < num_params; p++) {
@@ -841,8 +893,8 @@
     auto* indices = parameter.matcher_indices;
     auto* type = Match(closed, overload, indices).Type(args[p]->UnwrapRef());
     if (type) {
-      parameters.emplace_back(
-          sem::Parameter{const_cast<sem::Type*>(type), parameter.usage});
+      parameters.emplace_back(IntrinsicPrototype::Parameter{
+          const_cast<sem::Type*>(type), parameter.usage});
       match_score += kScorePerMatchedParam;
     } else {
       overload_matched = false;
@@ -899,13 +951,25 @@
     return_type = builder.create<sem::Void>();
   }
 
-  sem::Intrinsic intrinsic(intrinsic_type, const_cast<sem::Type*>(return_type),
-                           std::move(parameters), overload.supported_stages,
-                           overload.is_deprecated);
+  IntrinsicPrototype intrinsic;
+  intrinsic.type = intrinsic_type;
+  intrinsic.return_type = return_type;
+  intrinsic.parameters = std::move(parameters);
+  intrinsic.supported_stages = overload.supported_stages;
+  intrinsic.is_deprecated = overload.is_deprecated;
 
   // De-duplicate intrinsics that are identical.
   return utils::GetOrCreate(intrinsics, intrinsic, [&] {
-    return builder.create<sem::Intrinsic>(intrinsic);
+    sem::ParameterList params;
+    params.reserve(intrinsic.parameters.size());
+    for (auto& p : intrinsic.parameters) {
+      params.emplace_back(builder.create<sem::Parameter>(
+          nullptr, p.type, ast::StorageClass::kNone, ast::Access::kUndefined,
+          p.usage));
+    }
+    return builder.create<sem::Intrinsic>(
+        intrinsic.type, const_cast<sem::Type*>(intrinsic.return_type),
+        std::move(params), intrinsic.supported_stages, intrinsic.is_deprecated);
   });
 }
 
diff --git a/src/intrinsic_table_test.cc b/src/intrinsic_table_test.cc
index 2635312..374c56b 100644
--- a/src/intrinsic_table_test.cc
+++ b/src/intrinsic_table_test.cc
@@ -45,7 +45,8 @@
   ASSERT_EQ(Diagnostics().str(), "");
   EXPECT_THAT(result->Type(), IntrinsicType::kCos);
   EXPECT_THAT(result->ReturnType(), f32);
-  EXPECT_THAT(result->Parameters(), ElementsAre(Parameter{f32}));
+  ASSERT_THAT(result->Parameters().size(), 1);
+  EXPECT_EQ(result->Parameters()[0]->Type(), f32);
 }
 
 TEST_F(IntrinsicTableTest, MismatchF32) {
@@ -65,7 +66,8 @@
   ASSERT_EQ(Diagnostics().str(), "");
   EXPECT_THAT(result->Type(), IntrinsicType::kUnpack2x16float);
   EXPECT_THAT(result->ReturnType(), vec2_f32);
-  EXPECT_THAT(result->Parameters(), ElementsAre(Parameter{u32}));
+  ASSERT_EQ(result->Parameters().size(), 1u);
+  EXPECT_EQ(result->Parameters()[0]->Type(), u32);
 }
 
 TEST_F(IntrinsicTableTest, MismatchU32) {
@@ -87,10 +89,13 @@
   ASSERT_EQ(Diagnostics().str(), "");
   EXPECT_THAT(result->Type(), IntrinsicType::kTextureLoad);
   EXPECT_THAT(result->ReturnType(), vec4_f32);
-  EXPECT_THAT(result->Parameters(),
-              ElementsAre(Parameter{tex, ParameterUsage::kTexture},
-                          Parameter{i32, ParameterUsage::kCoords},
-                          Parameter{i32, ParameterUsage::kLevel}));
+  ASSERT_EQ(result->Parameters().size(), 3u);
+  EXPECT_EQ(result->Parameters()[0]->Type(), tex);
+  EXPECT_EQ(result->Parameters()[0]->Usage(), ParameterUsage::kTexture);
+  EXPECT_EQ(result->Parameters()[1]->Type(), i32);
+  EXPECT_EQ(result->Parameters()[1]->Usage(), ParameterUsage::kCoords);
+  EXPECT_EQ(result->Parameters()[2]->Type(), i32);
+  EXPECT_EQ(result->Parameters()[2]->Usage(), ParameterUsage::kLevel);
 }
 
 TEST_F(IntrinsicTableTest, MismatchI32) {
@@ -109,7 +114,8 @@
   ASSERT_EQ(Diagnostics().str(), "");
   EXPECT_THAT(result->Type(), IntrinsicType::kCountOneBits);
   EXPECT_THAT(result->ReturnType(), i32);
-  EXPECT_THAT(result->Parameters(), ElementsAre(Parameter{i32}));
+  ASSERT_EQ(result->Parameters().size(), 1u);
+  EXPECT_EQ(result->Parameters()[0]->Type(), i32);
 }
 
 TEST_F(IntrinsicTableTest, MatchIU32AsU32) {
@@ -119,7 +125,8 @@
   ASSERT_EQ(Diagnostics().str(), "");
   EXPECT_THAT(result->Type(), IntrinsicType::kCountOneBits);
   EXPECT_THAT(result->ReturnType(), u32);
-  EXPECT_THAT(result->Parameters(), ElementsAre(Parameter{u32}));
+  ASSERT_EQ(result->Parameters().size(), 1u);
+  EXPECT_EQ(result->Parameters()[0]->Type(), u32);
 }
 
 TEST_F(IntrinsicTableTest, MismatchIU32) {
@@ -137,8 +144,10 @@
   ASSERT_EQ(Diagnostics().str(), "");
   EXPECT_THAT(result->Type(), IntrinsicType::kClamp);
   EXPECT_THAT(result->ReturnType(), i32);
-  EXPECT_THAT(result->Parameters(),
-              ElementsAre(Parameter{i32}, Parameter{i32}, Parameter{i32}));
+  ASSERT_EQ(result->Parameters().size(), 3u);
+  EXPECT_EQ(result->Parameters()[0]->Type(), i32);
+  EXPECT_EQ(result->Parameters()[1]->Type(), i32);
+  EXPECT_EQ(result->Parameters()[2]->Type(), i32);
 }
 
 TEST_F(IntrinsicTableTest, MatchFIU32AsU32) {
@@ -149,8 +158,10 @@
   ASSERT_EQ(Diagnostics().str(), "");
   EXPECT_THAT(result->Type(), IntrinsicType::kClamp);
   EXPECT_THAT(result->ReturnType(), u32);
-  EXPECT_THAT(result->Parameters(),
-              ElementsAre(Parameter{u32}, Parameter{u32}, Parameter{u32}));
+  ASSERT_EQ(result->Parameters().size(), 3u);
+  EXPECT_EQ(result->Parameters()[0]->Type(), u32);
+  EXPECT_EQ(result->Parameters()[1]->Type(), u32);
+  EXPECT_EQ(result->Parameters()[2]->Type(), u32);
 }
 
 TEST_F(IntrinsicTableTest, MatchFIU32AsF32) {
@@ -161,8 +172,10 @@
   ASSERT_EQ(Diagnostics().str(), "");
   EXPECT_THAT(result->Type(), IntrinsicType::kClamp);
   EXPECT_THAT(result->ReturnType(), f32);
-  EXPECT_THAT(result->Parameters(),
-              ElementsAre(Parameter{f32}, Parameter{f32}, Parameter{f32}));
+  ASSERT_EQ(result->Parameters().size(), 3u);
+  EXPECT_EQ(result->Parameters()[0]->Type(), f32);
+  EXPECT_EQ(result->Parameters()[1]->Type(), f32);
+  EXPECT_EQ(result->Parameters()[2]->Type(), f32);
 }
 
 TEST_F(IntrinsicTableTest, MismatchFIU32) {
@@ -182,8 +195,10 @@
   ASSERT_EQ(Diagnostics().str(), "");
   EXPECT_THAT(result->Type(), IntrinsicType::kSelect);
   EXPECT_THAT(result->ReturnType(), f32);
-  EXPECT_THAT(result->Parameters(),
-              ElementsAre(Parameter{f32}, Parameter{f32}, Parameter{bool_}));
+  ASSERT_EQ(result->Parameters().size(), 3u);
+  EXPECT_EQ(result->Parameters()[0]->Type(), f32);
+  EXPECT_EQ(result->Parameters()[1]->Type(), f32);
+  EXPECT_EQ(result->Parameters()[2]->Type(), bool_);
 }
 
 TEST_F(IntrinsicTableTest, MismatchBool) {
@@ -203,8 +218,9 @@
   ASSERT_EQ(Diagnostics().str(), "");
   EXPECT_THAT(result->Type(), IntrinsicType::kModf);
   EXPECT_THAT(result->ReturnType(), f32);
-  EXPECT_THAT(result->Parameters(),
-              ElementsAre(Parameter{f32}, Parameter{ptr}));
+  ASSERT_EQ(result->Parameters().size(), 2u);
+  EXPECT_EQ(result->Parameters()[0]->Type(), f32);
+  EXPECT_EQ(result->Parameters()[1]->Type(), ptr);
 }
 
 TEST_F(IntrinsicTableTest, MismatchPointer) {
@@ -225,7 +241,7 @@
   EXPECT_THAT(result->Type(), IntrinsicType::kArrayLength);
   EXPECT_TRUE(result->ReturnType()->Is<sem::U32>());
   ASSERT_EQ(result->Parameters().size(), 1u);
-  auto* param_type = result->Parameters()[0].type;
+  auto* param_type = result->Parameters()[0]->Type();
   ASSERT_TRUE(param_type->Is<sem::Pointer>());
   EXPECT_TRUE(param_type->As<sem::Pointer>()->StoreType()->Is<sem::Array>());
 }
@@ -249,10 +265,13 @@
   ASSERT_EQ(Diagnostics().str(), "");
   EXPECT_THAT(result->Type(), IntrinsicType::kTextureSample);
   EXPECT_THAT(result->ReturnType(), vec4_f32);
-  EXPECT_THAT(result->Parameters(),
-              ElementsAre(Parameter{tex, ParameterUsage::kTexture},
-                          Parameter{sampler, ParameterUsage::kSampler},
-                          Parameter{vec2_f32, ParameterUsage::kCoords}));
+  ASSERT_EQ(result->Parameters().size(), 3u);
+  EXPECT_EQ(result->Parameters()[0]->Type(), tex);
+  EXPECT_EQ(result->Parameters()[0]->Usage(), ParameterUsage::kTexture);
+  EXPECT_EQ(result->Parameters()[1]->Type(), sampler);
+  EXPECT_EQ(result->Parameters()[1]->Usage(), ParameterUsage::kSampler);
+  EXPECT_EQ(result->Parameters()[2]->Type(), vec2_f32);
+  EXPECT_EQ(result->Parameters()[2]->Usage(), ParameterUsage::kCoords);
 }
 
 TEST_F(IntrinsicTableTest, MismatchSampler) {
@@ -277,10 +296,13 @@
   ASSERT_EQ(Diagnostics().str(), "");
   EXPECT_THAT(result->Type(), IntrinsicType::kTextureLoad);
   EXPECT_THAT(result->ReturnType(), vec4_f32);
-  EXPECT_THAT(result->Parameters(),
-              ElementsAre(Parameter{tex, ParameterUsage::kTexture},
-                          Parameter{vec2_i32, ParameterUsage::kCoords},
-                          Parameter{i32, ParameterUsage::kLevel}));
+  ASSERT_EQ(result->Parameters().size(), 3u);
+  EXPECT_EQ(result->Parameters()[0]->Type(), tex);
+  EXPECT_EQ(result->Parameters()[0]->Usage(), ParameterUsage::kTexture);
+  EXPECT_EQ(result->Parameters()[1]->Type(), vec2_i32);
+  EXPECT_EQ(result->Parameters()[1]->Usage(), ParameterUsage::kCoords);
+  EXPECT_EQ(result->Parameters()[2]->Type(), i32);
+  EXPECT_EQ(result->Parameters()[2]->Usage(), ParameterUsage::kLevel);
 }
 
 TEST_F(IntrinsicTableTest, MatchMultisampledTexture) {
@@ -295,10 +317,13 @@
   ASSERT_EQ(Diagnostics().str(), "");
   EXPECT_THAT(result->Type(), IntrinsicType::kTextureLoad);
   EXPECT_THAT(result->ReturnType(), vec4_f32);
-  EXPECT_THAT(result->Parameters(),
-              ElementsAre(Parameter{tex, ParameterUsage::kTexture},
-                          Parameter{vec2_i32, ParameterUsage::kCoords},
-                          Parameter{i32, ParameterUsage::kSampleIndex}));
+  ASSERT_EQ(result->Parameters().size(), 3u);
+  EXPECT_EQ(result->Parameters()[0]->Type(), tex);
+  EXPECT_EQ(result->Parameters()[0]->Usage(), ParameterUsage::kTexture);
+  EXPECT_EQ(result->Parameters()[1]->Type(), vec2_i32);
+  EXPECT_EQ(result->Parameters()[1]->Usage(), ParameterUsage::kCoords);
+  EXPECT_EQ(result->Parameters()[2]->Type(), i32);
+  EXPECT_EQ(result->Parameters()[2]->Usage(), ParameterUsage::kSampleIndex);
 }
 
 TEST_F(IntrinsicTableTest, MatchDepthTexture) {
@@ -312,10 +337,13 @@
   ASSERT_EQ(Diagnostics().str(), "");
   EXPECT_THAT(result->Type(), IntrinsicType::kTextureLoad);
   EXPECT_THAT(result->ReturnType(), f32);
-  EXPECT_THAT(result->Parameters(),
-              ElementsAre(Parameter{tex, ParameterUsage::kTexture},
-                          Parameter{vec2_i32, ParameterUsage::kCoords},
-                          Parameter{i32, ParameterUsage::kLevel}));
+  ASSERT_EQ(result->Parameters().size(), 3u);
+  EXPECT_EQ(result->Parameters()[0]->Type(), tex);
+  EXPECT_EQ(result->Parameters()[0]->Usage(), ParameterUsage::kTexture);
+  EXPECT_EQ(result->Parameters()[1]->Type(), vec2_i32);
+  EXPECT_EQ(result->Parameters()[1]->Usage(), ParameterUsage::kCoords);
+  EXPECT_EQ(result->Parameters()[2]->Type(), i32);
+  EXPECT_EQ(result->Parameters()[2]->Usage(), ParameterUsage::kLevel);
 }
 
 TEST_F(IntrinsicTableTest, MatchExternalTexture) {
@@ -330,9 +358,11 @@
   ASSERT_EQ(Diagnostics().str(), "");
   EXPECT_THAT(result->Type(), IntrinsicType::kTextureLoad);
   EXPECT_THAT(result->ReturnType(), vec4_f32);
-  EXPECT_THAT(result->Parameters(),
-              ElementsAre(Parameter{tex, ParameterUsage::kTexture},
-                          Parameter{vec2_i32, ParameterUsage::kCoords}));
+  ASSERT_EQ(result->Parameters().size(), 2u);
+  EXPECT_EQ(result->Parameters()[0]->Type(), tex);
+  EXPECT_EQ(result->Parameters()[0]->Usage(), ParameterUsage::kTexture);
+  EXPECT_EQ(result->Parameters()[1]->Type(), vec2_i32);
+  EXPECT_EQ(result->Parameters()[1]->Usage(), ParameterUsage::kCoords);
 }
 
 TEST_F(IntrinsicTableTest, MatchROStorageTexture) {
@@ -352,9 +382,11 @@
   ASSERT_EQ(Diagnostics().str(), "");
   EXPECT_THAT(result->Type(), IntrinsicType::kTextureLoad);
   EXPECT_THAT(result->ReturnType(), vec4_f32);
-  EXPECT_THAT(result->Parameters(),
-              ElementsAre(Parameter{tex, ParameterUsage::kTexture},
-                          Parameter{vec2_i32, ParameterUsage::kCoords}));
+  ASSERT_EQ(result->Parameters().size(), 2u);
+  EXPECT_EQ(result->Parameters()[0]->Type(), tex);
+  EXPECT_EQ(result->Parameters()[0]->Usage(), ParameterUsage::kTexture);
+  EXPECT_EQ(result->Parameters()[1]->Type(), vec2_i32);
+  EXPECT_EQ(result->Parameters()[1]->Usage(), ParameterUsage::kCoords);
 }
 
 TEST_F(IntrinsicTableTest, MatchWOStorageTexture) {
@@ -374,10 +406,13 @@
   ASSERT_EQ(Diagnostics().str(), "");
   EXPECT_THAT(result->Type(), IntrinsicType::kTextureStore);
   EXPECT_TRUE(result->ReturnType()->Is<sem::Void>());
-  EXPECT_THAT(result->Parameters(),
-              ElementsAre(Parameter{tex, ParameterUsage::kTexture},
-                          Parameter{vec2_i32, ParameterUsage::kCoords},
-                          Parameter{vec4_f32, ParameterUsage::kValue}));
+  ASSERT_EQ(result->Parameters().size(), 3u);
+  EXPECT_EQ(result->Parameters()[0]->Type(), tex);
+  EXPECT_EQ(result->Parameters()[0]->Usage(), ParameterUsage::kTexture);
+  EXPECT_EQ(result->Parameters()[1]->Type(), vec2_i32);
+  EXPECT_EQ(result->Parameters()[1]->Usage(), ParameterUsage::kCoords);
+  EXPECT_EQ(result->Parameters()[2]->Type(), vec4_f32);
+  EXPECT_EQ(result->Parameters()[2]->Usage(), ParameterUsage::kValue);
 }
 
 TEST_F(IntrinsicTableTest, MismatchTexture) {
@@ -401,7 +436,8 @@
   ASSERT_EQ(Diagnostics().str(), "");
   EXPECT_THAT(result->Type(), IntrinsicType::kCos);
   EXPECT_THAT(result->ReturnType(), f32);
-  EXPECT_THAT(result->Parameters(), ElementsAre(Parameter{f32}));
+  ASSERT_EQ(result->Parameters().size(), 1u);
+  EXPECT_EQ(result->Parameters()[0]->Type(), f32);
 }
 
 TEST_F(IntrinsicTableTest, MatchOpenType) {
@@ -412,8 +448,9 @@
   ASSERT_EQ(Diagnostics().str(), "");
   EXPECT_THAT(result->Type(), IntrinsicType::kClamp);
   EXPECT_THAT(result->ReturnType(), f32);
-  EXPECT_THAT(result->Parameters(),
-              ElementsAre(Parameter{f32}, Parameter{f32}, Parameter{f32}));
+  EXPECT_EQ(result->Parameters()[0]->Type(), f32);
+  EXPECT_EQ(result->Parameters()[1]->Type(), f32);
+  EXPECT_EQ(result->Parameters()[2]->Type(), f32);
 }
 
 TEST_F(IntrinsicTableTest, MismatchOpenType) {
@@ -434,9 +471,10 @@
   ASSERT_EQ(Diagnostics().str(), "");
   EXPECT_THAT(result->Type(), IntrinsicType::kClamp);
   EXPECT_THAT(result->ReturnType(), vec2_f32);
-  EXPECT_THAT(result->Parameters(),
-              ElementsAre(Parameter{vec2_f32}, Parameter{vec2_f32},
-                          Parameter{vec2_f32}));
+  ASSERT_EQ(result->Parameters().size(), 3u);
+  EXPECT_EQ(result->Parameters()[0]->Type(), vec2_f32);
+  EXPECT_EQ(result->Parameters()[1]->Type(), vec2_f32);
+  EXPECT_EQ(result->Parameters()[2]->Type(), vec2_f32);
 }
 
 TEST_F(IntrinsicTableTest, MismatchOpenSizeVector) {
@@ -459,7 +497,8 @@
   ASSERT_EQ(Diagnostics().str(), "");
   EXPECT_THAT(result->Type(), IntrinsicType::kDeterminant);
   EXPECT_THAT(result->ReturnType(), f32);
-  EXPECT_THAT(result->Parameters(), ElementsAre(Parameter{mat3_f32}));
+  ASSERT_EQ(result->Parameters().size(), 1u);
+  EXPECT_EQ(result->Parameters()[0]->Type(), mat3_f32);
 }
 
 TEST_F(IntrinsicTableTest, MismatchOpenSizeMatrix) {
diff --git a/src/program_builder.h b/src/program_builder.h
index d4a25cf..a7d8f90 100644
--- a/src/program_builder.h
+++ b/src/program_builder.h
@@ -50,6 +50,7 @@
 #include "src/ast/pointer.h"
 #include "src/ast/return_statement.h"
 #include "src/ast/sampled_texture.h"
+#include "src/ast/sampler.h"
 #include "src/ast/scalar_constructor_expression.h"
 #include "src/ast/sint_literal.h"
 #include "src/ast/stage_decoration.h"
diff --git a/src/reader/spirv/function.cc b/src/reader/spirv/function.cc
index d673c60..819b9a3 100644
--- a/src/reader/spirv/function.cc
+++ b/src/reader/spirv/function.cc
@@ -34,6 +34,7 @@
 #include "src/ast/unary_op_expression.h"
 #include "src/ast/variable_decl_statement.h"
 #include "src/sem/depth_texture_type.h"
+#include "src/sem/intrinsic_type.h"
 #include "src/sem/sampled_texture_type.h"
 
 // Terms:
@@ -4959,9 +4960,7 @@
 TypedExpression FunctionEmitter::MakeIntrinsicCall(
     const spvtools::opt::Instruction& inst) {
   const auto intrinsic = GetIntrinsic(inst.opcode());
-  std::ostringstream ss;
-  ss << intrinsic;
-  auto name = ss.str();
+  auto* name = sem::str(intrinsic);
   auto* ident = create<ast::IdentifierExpression>(
       Source{}, builder_.Symbols().Register(name));
 
diff --git a/src/resolver/intrinsic_test.cc b/src/resolver/intrinsic_test.cc
index 27b15df..d94ed2d 100644
--- a/src/resolver/intrinsic_test.cc
+++ b/src/resolver/intrinsic_test.cc
@@ -1706,11 +1706,11 @@
   std::stringstream out;
   out << function << "(";
   bool first = true;
-  for (auto& param : params) {
+  for (auto* param : params) {
     if (!first) {
       out << ", ";
     }
-    out << sem::str(param.usage);
+    out << sem::str(param->Usage());
     first = false;
   }
   out << ")";
diff --git a/src/resolver/pipeline_overridable_constant_test.cc b/src/resolver/pipeline_overridable_constant_test.cc
index 9fdfd8f..5d0571f 100644
--- a/src/resolver/pipeline_overridable_constant_test.cc
+++ b/src/resolver/pipeline_overridable_constant_test.cc
@@ -30,7 +30,7 @@
 
   EXPECT_TRUE(r()->Resolve()) << r()->error();
 
-  auto* sem_a = Sem().Get(a);
+  auto* sem_a = Sem().Get<sem::GlobalVariable>(a);
   ASSERT_NE(sem_a, nullptr);
   EXPECT_EQ(sem_a->Declaration(), a);
   EXPECT_FALSE(sem_a->IsPipelineConstant());
@@ -41,7 +41,7 @@
 
   EXPECT_TRUE(r()->Resolve()) << r()->error();
 
-  auto* sem_a = Sem().Get(a);
+  auto* sem_a = Sem().Get<sem::GlobalVariable>(a);
   ASSERT_NE(sem_a, nullptr);
   EXPECT_EQ(sem_a->Declaration(), a);
   EXPECT_TRUE(sem_a->IsPipelineConstant());
@@ -53,7 +53,7 @@
 
   EXPECT_TRUE(r()->Resolve()) << r()->error();
 
-  auto* sem_a = Sem().Get(a);
+  auto* sem_a = Sem().Get<sem::GlobalVariable>(a);
   ASSERT_NE(sem_a, nullptr);
   EXPECT_EQ(sem_a->Declaration(), a);
   EXPECT_TRUE(sem_a->IsPipelineConstant());
@@ -79,7 +79,7 @@
 
   std::vector<uint16_t> constant_ids;
   for (auto* var : variables) {
-    auto* sem = Sem().Get(var);
+    auto* sem = Sem().Get<sem::GlobalVariable>(var);
     ASSERT_NE(sem, nullptr);
     constant_ids.push_back(static_cast<uint16_t>(sem->ConstantId()));
   }
diff --git a/src/resolver/resolver.cc b/src/resolver/resolver.cc
index b41b1bc..842b348 100644
--- a/src/resolver/resolver.cc
+++ b/src/resolver/resolver.cc
@@ -3693,11 +3693,24 @@
         next_constant_id = constant_id + 1;
       }
 
-      sem_var = builder_->create<sem::Variable>(var, info->type, constant_id);
-    } else {
       sem_var =
-          builder_->create<sem::Variable>(var, info->type, info->storage_class,
-                                          info->access, info->binding_point);
+          builder_->create<sem::GlobalVariable>(var, info->type, constant_id);
+    } else {
+      switch (info->kind) {
+        case VariableKind::kGlobal:
+          sem_var = builder_->create<sem::GlobalVariable>(
+              var, info->type, info->storage_class, info->access,
+              info->binding_point);
+          break;
+        case VariableKind::kLocal:
+          sem_var = builder_->create<sem::LocalVariable>(
+              var, info->type, info->storage_class, info->access);
+          break;
+        case VariableKind::kParameter:
+          sem_var = builder_->create<sem::Parameter>(
+              var, info->type, info->storage_class, info->access);
+          break;
+      }
     }
 
     std::vector<const sem::VariableUser*> users;
@@ -3739,9 +3752,15 @@
     auto* func = it.first;
     auto* info = it.second;
 
+    sem::ParameterList parameters;
+    parameters.reserve(info->parameters.size());
+    for (auto* p : info->parameters) {
+      parameters.emplace_back(sem.Get<sem::Parameter>(p->declaration));
+    }
+
     auto* sem_func = builder_->create<sem::Function>(
         info->declaration, const_cast<sem::Type*>(info->return_type),
-        remap_vars(info->parameters), remap_vars(info->referenced_module_vars),
+        parameters, remap_vars(info->referenced_module_vars),
         remap_vars(info->local_referenced_module_vars), info->return_statements,
         info->callsites, ancestor_entry_points[func->symbol()],
         info->workgroup_size);
diff --git a/src/resolver/resolver_test.cc b/src/resolver/resolver_test.cc
index d231774..f176dc1 100644
--- a/src/resolver/resolver_test.cc
+++ b/src/resolver/resolver_test.cc
@@ -2018,8 +2018,10 @@
 
   EXPECT_TRUE(r()->Resolve()) << r()->error();
 
-  EXPECT_EQ(Sem().Get(s1)->BindingPoint(), (sem::BindingPoint{1u, 2u}));
-  EXPECT_EQ(Sem().Get(s2)->BindingPoint(), (sem::BindingPoint{3u, 4u}));
+  EXPECT_EQ(Sem().Get<sem::GlobalVariable>(s1)->BindingPoint(),
+            (sem::BindingPoint{1u, 2u}));
+  EXPECT_EQ(Sem().Get<sem::GlobalVariable>(s2)->BindingPoint(),
+            (sem::BindingPoint{3u, 4u}));
 }
 
 TEST_F(ResolverTest, Function_EntryPoints_StageDecoration) {
diff --git a/src/sem/call_target.cc b/src/sem/call_target.cc
index 5306bf8..b8bf12c 100644
--- a/src/sem/call_target.cc
+++ b/src/sem/call_target.cc
@@ -32,18 +32,12 @@
 
 int IndexOf(const ParameterList& parameters, ParameterUsage usage) {
   for (size_t i = 0; i < parameters.size(); i++) {
-    if (parameters[i].usage == usage) {
+    if (parameters[i]->Usage() == usage) {
       return static_cast<int>(i);
     }
   }
   return -1;
 }
 
-std::ostream& operator<<(std::ostream& out, Parameter parameter) {
-  out << "[type: " << parameter.type->FriendlyName(SymbolTable{ProgramID{}})
-      << ", usage: " << str(parameter.usage) << "]";
-  return out;
-}
-
 }  // namespace sem
 }  // namespace tint
diff --git a/src/sem/call_target.h b/src/sem/call_target.h
index e994c80..33d5029 100644
--- a/src/sem/call_target.h
+++ b/src/sem/call_target.h
@@ -18,39 +18,15 @@
 #include <vector>
 
 #include "src/sem/node.h"
-#include "src/sem/parameter_usage.h"
 #include "src/sem/sampler_type.h"
+#include "src/sem/variable.h"
 #include "src/utils/hash.h"
 
 namespace tint {
-
 namespace sem {
 // Forward declarations
 class Type;
 
-/// Parameter describes a single parameter of a call target
-struct Parameter {
-  /// Parameter type
-  sem::Type* const type;
-  /// Parameter usage
-  ParameterUsage const usage = ParameterUsage::kNone;
-};
-
-std::ostream& operator<<(std::ostream& out, Parameter parameter);
-
-/// Equality operator for Parameters
-static inline bool operator==(const Parameter& a, const Parameter& b) {
-  return a.type == b.type && a.usage == b.usage;
-}
-
-/// Inequality operator for Parameters
-static inline bool operator!=(const Parameter& a, const Parameter& b) {
-  return !(a == b);
-}
-
-/// ParameterList is a list of Parameter
-using ParameterList = std::vector<Parameter>;
-
 /// @param parameters the list of parameters
 /// @param usage the parameter usage to find
 /// @returns the index of the parameter with the given usage, or -1 if no
@@ -85,19 +61,4 @@
 }  // namespace sem
 }  // namespace tint
 
-namespace std {
-
-/// Custom std::hash specialization for tint::sem::Parameter
-template <>
-class hash<tint::sem::Parameter> {
- public:
-  /// @param p the tint::sem::Parameter to create a hash for
-  /// @return the hash value
-  inline std::size_t operator()(const tint::sem::Parameter& p) const {
-    return tint::utils::Hash(p.type, p.usage);
-  }
-};
-
-}  // namespace std
-
 #endif  // SRC_SEM_CALL_TARGET_H_
diff --git a/src/sem/function.cc b/src/sem/function.cc
index fcf192a..21fabf9 100644
--- a/src/sem/function.cc
+++ b/src/sem/function.cc
@@ -27,31 +27,17 @@
 namespace tint {
 namespace sem {
 
-namespace {
-
-ParameterList GetParameters(const std::vector<const Variable*>& params) {
-  ParameterList parameters;
-  parameters.reserve(params.size());
-  for (auto* param : params) {
-    parameters.emplace_back(Parameter{param->Type(), ParameterUsage::kNone});
-  }
-  return parameters;
-}
-
-}  // namespace
-
 Function::Function(ast::Function* declaration,
                    Type* return_type,
-                   std::vector<const Variable*> parameters,
+                   ParameterList parameters,
                    std::vector<const Variable*> referenced_module_vars,
                    std::vector<const Variable*> local_referenced_module_vars,
                    std::vector<const ast::ReturnStatement*> return_statements,
                    std::vector<const ast::CallExpression*> callsites,
                    std::vector<Symbol> ancestor_entry_points,
                    std::array<WorkgroupDimension, 3> workgroup_size)
-    : Base(return_type, GetParameters(parameters)),
+    : Base(return_type, std::move(parameters)),
       declaration_(declaration),
-      parameters_(std::move(parameters)),
       referenced_module_vars_(std::move(referenced_module_vars)),
       local_referenced_module_vars_(std::move(local_referenced_module_vars)),
       return_statements_(std::move(return_statements)),
diff --git a/src/sem/function.h b/src/sem/function.h
index d48047b..6ab1d39 100644
--- a/src/sem/function.h
+++ b/src/sem/function.h
@@ -68,7 +68,7 @@
   /// @param workgroup_size the workgroup size
   Function(ast::Function* declaration,
            Type* return_type,
-           std::vector<const Variable*> parameters,
+           ParameterList parameters,
            std::vector<const Variable*> referenced_module_vars,
            std::vector<const Variable*> local_referenced_module_vars,
            std::vector<const ast::ReturnStatement*> return_statements,
@@ -82,9 +82,6 @@
   /// @returns the ast::Function declaration
   ast::Function* Declaration() const { return declaration_; }
 
-  /// @return the parameters to the function
-  const std::vector<const Variable*> Parameters() const { return parameters_; }
-
   /// 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
@@ -178,7 +175,6 @@
       bool multisampled) const;
 
   ast::Function* const declaration_;
-  std::vector<const Variable*> const parameters_;
   std::vector<const Variable*> const referenced_module_vars_;
   std::vector<const Variable*> const local_referenced_module_vars_;
   std::vector<const ast::ReturnStatement*> const return_statements_;
diff --git a/src/sem/intrinsic.cc b/src/sem/intrinsic.cc
index 4be75e8..b7d57a9 100644
--- a/src/sem/intrinsic.cc
+++ b/src/sem/intrinsic.cc
@@ -19,11 +19,6 @@
 namespace tint {
 namespace sem {
 
-std::ostream& operator<<(std::ostream& out, IntrinsicType i) {
-  out << str(i);
-  return out;
-}
-
 const char* Intrinsic::str() const {
   return sem::str(type_);
 }
@@ -103,7 +98,7 @@
 
 Intrinsic::Intrinsic(IntrinsicType type,
                      sem::Type* return_type,
-                     const ParameterList& parameters,
+                     ParameterList parameters,
                      PipelineStageSet supported_stages,
                      bool is_deprecated)
     : Base(return_type, parameters),
@@ -111,8 +106,6 @@
       supported_stages_(supported_stages),
       is_deprecated_(is_deprecated) {}
 
-Intrinsic::Intrinsic(const Intrinsic&) = default;
-
 Intrinsic::~Intrinsic() = default;
 
 bool Intrinsic::IsCoarseDerivative() const {
@@ -155,25 +148,5 @@
   return IsAtomicIntrinsic(type_);
 }
 
-bool operator==(const Intrinsic& a, const Intrinsic& b) {
-  static_assert(sizeof(Intrinsic(IntrinsicType::kNone, nullptr, ParameterList{},
-                                 PipelineStageSet{}, false)) > 0,
-                "don't forget to update the comparison below if you change the "
-                "constructor of Intrinsic!");
-
-  if (a.Type() != b.Type() || a.SupportedStages() != b.SupportedStages() ||
-      a.ReturnType() != b.ReturnType() ||
-      a.IsDeprecated() != b.IsDeprecated() ||
-      a.Parameters().size() != b.Parameters().size()) {
-    return false;
-  }
-  for (size_t i = 0; i < a.Parameters().size(); i++) {
-    if (a.Parameters()[i] != b.Parameters()[i]) {
-      return false;
-    }
-  }
-  return true;
-}
-
 }  // namespace sem
 }  // namespace tint
diff --git a/src/sem/intrinsic.h b/src/sem/intrinsic.h
index 25c0c59..74f332f 100644
--- a/src/sem/intrinsic.h
+++ b/src/sem/intrinsic.h
@@ -88,13 +88,10 @@
   /// deprecated
   Intrinsic(IntrinsicType type,
             sem::Type* return_type,
-            const ParameterList& parameters,
+            ParameterList parameters,
             PipelineStageSet supported_stages,
             bool is_deprecated);
 
-  /// Copy constructor
-  Intrinsic(const Intrinsic&);
-
   /// Destructor
   ~Intrinsic() override;
 
@@ -147,18 +144,6 @@
   bool const is_deprecated_;
 };
 
-/// Emits the name of the intrinsic function type. The spelling, including case,
-/// matches the name in the WGSL spec.
-std::ostream& operator<<(std::ostream& out, IntrinsicType i);
-
-/// Equality operator for Intrinsics
-bool operator==(const Intrinsic& a, const Intrinsic& b);
-
-/// Inequality operator for Intrinsics
-static inline bool operator!=(const Intrinsic& a, const Intrinsic& b) {
-  return !(a == b);
-}
-
 }  // namespace sem
 }  // namespace tint
 
diff --git a/src/sem/intrinsic_type.cc b/src/sem/intrinsic_type.cc
index 75616ca..a9cac51 100644
--- a/src/sem/intrinsic_type.cc
+++ b/src/sem/intrinsic_type.cc
@@ -24,6 +24,8 @@
 
 #include "src/sem/intrinsic_type.h"
 
+#include <sstream>
+
 namespace tint {
 namespace sem {
 
@@ -529,5 +531,10 @@
   return "<unknown>";
 }
 
+std::ostream& operator<<(std::ostream& out, IntrinsicType i) {
+  out << str(i);
+  return out;
+}
+
 }  // namespace sem
 }  // namespace tint
diff --git a/src/sem/intrinsic_type.cc.tmpl b/src/sem/intrinsic_type.cc.tmpl
index e4059e3..f02c13f 100644
--- a/src/sem/intrinsic_type.cc.tmpl
+++ b/src/sem/intrinsic_type.cc.tmpl
@@ -10,6 +10,8 @@
 
 #include "src/sem/intrinsic_type.h"
 
+#include <sstream>
+
 namespace tint {
 namespace sem {
 
@@ -34,5 +36,10 @@
   return "<unknown>";
 }
 
+std::ostream& operator<<(std::ostream& out, IntrinsicType i) {
+  out << str(i);
+  return out;
+}
+
 }  // namespace sem
 }  // namespace tint
diff --git a/src/sem/intrinsic_type.h b/src/sem/intrinsic_type.h
index 29e754b..6e47c3e 100644
--- a/src/sem/intrinsic_type.h
+++ b/src/sem/intrinsic_type.h
@@ -26,6 +26,7 @@
 #define SRC_SEM_INTRINSIC_TYPE_H_
 
 #include <string>
+#include <sstream>
 
 namespace tint {
 namespace sem {
@@ -143,6 +144,10 @@
 /// case, matches the name in the WGSL spec.
 const char* str(IntrinsicType i);
 
+/// Emits the name of the intrinsic function type. The spelling, including case,
+/// matches the name in the WGSL spec.
+std::ostream& operator<<(std::ostream& out, IntrinsicType i);
+
 }  // namespace sem
 }  // namespace tint
 
diff --git a/src/sem/intrinsic_type.h.tmpl b/src/sem/intrinsic_type.h.tmpl
index 8882645..deb935c 100644
--- a/src/sem/intrinsic_type.h.tmpl
+++ b/src/sem/intrinsic_type.h.tmpl
@@ -12,6 +12,7 @@
 #define SRC_SEM_INTRINSIC_TYPE_H_
 
 #include <string>
+#include <sstream>
 
 namespace tint {
 namespace sem {
@@ -34,6 +35,10 @@
 /// case, matches the name in the WGSL spec.
 const char* str(IntrinsicType i);
 
+/// Emits the name of the intrinsic function type. The spelling, including case,
+/// matches the name in the WGSL spec.
+std::ostream& operator<<(std::ostream& out, IntrinsicType i);
+
 }  // namespace sem
 }  // namespace tint
 
diff --git a/src/sem/sampler_type_test.cc b/src/sem/sampler_type_test.cc
index f27ac2e..9f2d5fc 100644
--- a/src/sem/sampler_type_test.cc
+++ b/src/sem/sampler_type_test.cc
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "src/sem/sampler_type.h"
 #include "src/sem/test_helper.h"
 #include "src/sem/texture_type.h"
 
diff --git a/src/sem/variable.cc b/src/sem/variable.cc
index 490f890..fb6f48f 100644
--- a/src/sem/variable.cc
+++ b/src/sem/variable.cc
@@ -20,6 +20,9 @@
 #include "src/ast/variable.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::sem::Variable);
+TINT_INSTANTIATE_TYPEINFO(tint::sem::GlobalVariable);
+TINT_INSTANTIATE_TYPEINFO(tint::sem::LocalVariable);
+TINT_INSTANTIATE_TYPEINFO(tint::sem::Parameter);
 TINT_INSTANTIATE_TYPEINFO(tint::sem::VariableUser);
 
 namespace tint {
@@ -28,26 +31,51 @@
 Variable::Variable(const ast::Variable* declaration,
                    const sem::Type* type,
                    ast::StorageClass storage_class,
-                   ast::Access access,
-                   sem::BindingPoint binding_point)
+                   ast::Access access)
     : declaration_(declaration),
       type_(type),
       storage_class_(storage_class),
-      access_(access),
+      access_(access) {}
+
+Variable::~Variable() = default;
+
+LocalVariable::LocalVariable(const ast::Variable* declaration,
+                             const sem::Type* type,
+                             ast::StorageClass storage_class,
+                             ast::Access access)
+    : Base(declaration, type, storage_class, access) {}
+
+LocalVariable::~LocalVariable() = default;
+
+GlobalVariable::GlobalVariable(const ast::Variable* declaration,
+                               const sem::Type* type,
+                               ast::StorageClass storage_class,
+                               ast::Access access,
+                               sem::BindingPoint binding_point)
+    : Base(declaration, type, storage_class, access),
       binding_point_(binding_point),
       is_pipeline_constant_(false) {}
 
-Variable::Variable(const ast::Variable* declaration,
-                   const sem::Type* type,
-                   uint16_t constant_id)
-    : declaration_(declaration),
-      type_(type),
-      storage_class_(ast::StorageClass::kNone),
-      access_(ast::Access::kReadWrite),
+GlobalVariable::GlobalVariable(const ast::Variable* declaration,
+                               const sem::Type* type,
+                               uint16_t constant_id)
+    : Base(declaration,
+           type,
+           ast::StorageClass::kNone,
+           ast::Access::kReadWrite),
       is_pipeline_constant_(true),
       constant_id_(constant_id) {}
 
-Variable::~Variable() = default;
+GlobalVariable::~GlobalVariable() = default;
+
+Parameter::Parameter(const ast::Variable* declaration,
+                     const sem::Type* type,
+                     ast::StorageClass storage_class,
+                     ast::Access access,
+                     const ParameterUsage usage /* = ParameterUsage::kNone */)
+    : Base(declaration, type, storage_class, access), usage_(usage) {}
+
+Parameter::~Parameter() = default;
 
 VariableUser::VariableUser(ast::IdentifierExpression* declaration,
                            const sem::Type* type,
diff --git a/src/sem/variable.h b/src/sem/variable.h
index 356ed9b..fc8d9be 100644
--- a/src/sem/variable.h
+++ b/src/sem/variable.h
@@ -21,6 +21,7 @@
 #include "src/ast/storage_class.h"
 #include "src/sem/binding_point.h"
 #include "src/sem/expression.h"
+#include "src/sem/parameter_usage.h"
 
 namespace tint {
 
@@ -36,28 +37,19 @@
 class Type;
 class VariableUser;
 
-/// Variable holds the semantic information for variables.
+/// Variable is the base class for local variables, global variables and
+/// parameters.
 class Variable : public Castable<Variable, Node> {
  public:
-  /// Constructor for variables and non-overridable constants
+  /// Constructor
   /// @param declaration the AST declaration node
   /// @param type the variable type
   /// @param storage_class the variable storage class
   /// @param access the variable access control type
-  /// @param binding_point the optional resource binding point of the variable
   Variable(const ast::Variable* declaration,
            const sem::Type* type,
            ast::StorageClass storage_class,
-           ast::Access access,
-           sem::BindingPoint binding_point = {});
-
-  /// Constructor for overridable pipeline constants
-  /// @param declaration the AST declaration node
-  /// @param type the variable type
-  /// @param constant_id the pipeline constant ID
-  Variable(const ast::Variable* declaration,
-           const sem::Type* type,
-           uint16_t constant_id);
+           ast::Access access);
 
   /// Destructor
   ~Variable() override;
@@ -74,32 +66,109 @@
   /// @returns the access control for the variable
   ast::Access Access() const { return access_; }
 
-  /// @returns the resource binding point for the variable
-  sem::BindingPoint BindingPoint() const { return binding_point_; }
-
   /// @returns the expressions that use the variable
   const std::vector<const VariableUser*>& Users() const { return users_; }
 
   /// @param user the user to add
   void AddUser(const VariableUser* user) { users_.emplace_back(user); }
 
-  /// @returns true if this variable is an overridable pipeline constant
-  bool IsPipelineConstant() const { return is_pipeline_constant_; }
-
-  /// @returns the pipeline constant ID associated with the variable
-  uint16_t ConstantId() const { return constant_id_; }
-
  private:
   const ast::Variable* const declaration_;
   const sem::Type* const type_;
   ast::StorageClass const storage_class_;
   ast::Access const access_;
-  sem::BindingPoint binding_point_;
   std::vector<const VariableUser*> users_;
-  const bool is_pipeline_constant_;
-  const uint16_t constant_id_ = 0;
 };
 
+/// LocalVariable is a function-scope variable
+class LocalVariable : public Castable<LocalVariable, Variable> {
+ public:
+  /// Constructor
+  /// @param declaration the AST declaration node
+  /// @param type the variable type
+  /// @param storage_class the variable storage class
+  /// @param access the variable access control type
+  LocalVariable(const ast::Variable* declaration,
+                const sem::Type* type,
+                ast::StorageClass storage_class,
+                ast::Access access);
+
+  /// Destructor
+  ~LocalVariable() override;
+};
+
+/// GlobalVariable is a module-scope variable
+class GlobalVariable : public Castable<GlobalVariable, Variable> {
+ public:
+  /// Constructor for non-overridable constants
+  /// @param declaration the AST declaration node
+  /// @param type the variable type
+  /// @param storage_class the variable storage class
+  /// @param access the variable access control type
+  /// @param binding_point the optional resource binding point of the variable
+  GlobalVariable(const ast::Variable* declaration,
+                 const sem::Type* type,
+                 ast::StorageClass storage_class,
+                 ast::Access access,
+                 sem::BindingPoint binding_point = {});
+
+  /// Constructor for overridable pipeline constants
+  /// @param declaration the AST declaration node
+  /// @param type the variable type
+  /// @param constant_id the pipeline constant ID
+  GlobalVariable(const ast::Variable* declaration,
+                 const sem::Type* type,
+                 uint16_t constant_id);
+
+  /// Destructor
+  ~GlobalVariable() override;
+
+  /// @returns the resource binding point for the variable
+  sem::BindingPoint BindingPoint() const { return binding_point_; }
+
+  /// @returns the pipeline constant ID associated with the variable
+  uint16_t ConstantId() const { return constant_id_; }
+
+  /// @returns true if this variable is an overridable pipeline constant
+  bool IsPipelineConstant() const { return is_pipeline_constant_; }
+
+ private:
+  sem::BindingPoint binding_point_;
+  bool const is_pipeline_constant_;
+  uint16_t const constant_id_ = 0;
+};
+
+/// Parameter is a function parameter
+class Parameter : public Castable<Parameter, Variable> {
+ public:
+  /// Constructor for function parameters
+  /// @param declaration the AST declaration node
+  /// @param type the variable type
+  /// @param storage_class the variable storage class
+  /// @param access the variable access control type
+  /// @param usage the semantic usage for the parameter
+  Parameter(const ast::Variable* declaration,
+            const sem::Type* type,
+            ast::StorageClass storage_class,
+            ast::Access access,
+            const ParameterUsage usage = ParameterUsage::kNone);
+
+  /// Copy constructor
+  Parameter(const Parameter&);
+
+  /// Destructor
+  ~Parameter() override;
+
+  /// @returns the semantic usage for the parameter
+  ParameterUsage Usage() const { return usage_; }
+
+ private:
+  ParameterUsage const usage_;
+};
+
+/// ParameterList is a list of Parameter
+using ParameterList = std::vector<const Parameter*>;
+
 /// VariableUser holds the semantic information for an identifier expression
 /// node that resolves to a variable.
 class VariableUser : public Castable<VariableUser, Expression> {
diff --git a/src/transform/array_length_from_uniform.cc b/src/transform/array_length_from_uniform.cc
index 35fbcb9..8a0fa17 100644
--- a/src/transform/array_length_from_uniform.cc
+++ b/src/transform/array_length_from_uniform.cc
@@ -131,7 +131,13 @@
     }
 
     // Get the index to use for the buffer size array.
-    auto binding = storage_buffer_sem->Variable()->BindingPoint();
+    auto* var = tint::As<sem::GlobalVariable>(storage_buffer_sem->Variable());
+    if (!var) {
+      TINT_ICE(Transform, ctx.dst->Diagnostics())
+          << "storage buffer is not a global variable";
+      break;
+    }
+    auto binding = var->BindingPoint();
     auto idx_itr = cfg->bindpoint_to_size_index.find(binding);
     if (idx_itr == cfg->bindpoint_to_size_index.end()) {
       ctx.dst->Diagnostics().add_error(
diff --git a/src/transform/decompose_memory_access.cc b/src/transform/decompose_memory_access.cc
index f06b3ae..0e2b245 100644
--- a/src/transform/decompose_memory_access.cc
+++ b/src/transform/decompose_memory_access.cc
@@ -682,8 +682,8 @@
 
       // Other parameters are copied as-is:
       for (size_t i = 1; i < intrinsic->Parameters().size(); i++) {
-        auto& param = intrinsic->Parameters()[i];
-        auto* ty = CreateASTTypeFor(ctx, param.type);
+        auto* param = intrinsic->Parameters()[i];
+        auto* ty = CreateASTTypeFor(ctx, param->Type());
         params.emplace_back(b.Param("param_" + std::to_string(i), ty));
       }
 
diff --git a/src/transform/msl.cc b/src/transform/msl.cc
index 53ead18..5c86a03 100644
--- a/src/transform/msl.cc
+++ b/src/transform/msl.cc
@@ -66,10 +66,10 @@
 
   // Use the SSBO binding numbers as the indices for the buffer size lookups.
   for (auto* var : in->AST().GlobalVariables()) {
-    auto* sem_var = in->Sem().Get(var);
-    if (sem_var->StorageClass() == ast::StorageClass::kStorage) {
+    auto* global = in->Sem().Get<sem::GlobalVariable>(var);
+    if (global && global->StorageClass() == ast::StorageClass::kStorage) {
       array_length_from_uniform_cfg.bindpoint_to_size_index.emplace(
-          sem_var->BindingPoint(), sem_var->BindingPoint().binding);
+          global->BindingPoint(), global->BindingPoint().binding);
     }
   }
 
diff --git a/src/transform/robustness.cc b/src/transform/robustness.cc
index 92abd78..1e8b94e 100644
--- a/src/transform/robustness.cc
+++ b/src/transform/robustness.cc
@@ -219,7 +219,7 @@
 
     auto* texture_arg = expr->params()[texture_idx];
     auto* coords_arg = expr->params()[coords_idx];
-    auto* coords_ty = intrinsic->Parameters()[coords_idx].type;
+    auto* coords_ty = intrinsic->Parameters()[coords_idx]->Type();
 
     // If the level is provided, then we need to clamp this. As the level is
     // used by textureDimensions() and the texture[Load|Store]() calls, we need
diff --git a/src/transform/transform.cc b/src/transform/transform.cc
index cd788c5..f67c046 100644
--- a/src/transform/transform.cc
+++ b/src/transform/transform.cc
@@ -22,6 +22,7 @@
 #include "src/sem/block_statement.h"
 #include "src/sem/for_loop_statement.h"
 #include "src/sem/reference_type.h"
+#include "src/sem/sampler_type.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::transform::Transform);
 TINT_INSTANTIATE_TYPEINFO(tint::transform::Data);
diff --git a/src/utils/hash.h b/src/utils/hash.h
index bd78942..91792bf 100644
--- a/src/utils/hash.h
+++ b/src/utils/hash.h
@@ -43,16 +43,16 @@
   static constexpr inline uint64_t value() { return 0x9e3779b97f4a7c16; }
 };
 
-// When hashing sparse structures we want to iteratively build a hash value with
-// only parts of the data. HashCombine "hashes" together an existing hash and
-// hashable values.
+}  // namespace detail
+
+/// HashCombine "hashes" together an existing hash and hashable values.
 template <typename T>
 void HashCombine(size_t* hash, const T& value) {
-  constexpr size_t offset = HashCombineOffset<sizeof(size_t)>::value();
+  constexpr size_t offset = detail::HashCombineOffset<sizeof(size_t)>::value();
   *hash ^= std::hash<T>()(value) + offset + (*hash << 6) + (*hash >> 2);
 }
 
-// Helper for hashing vectors
+/// HashCombine "hashes" together an existing hash and hashable values.
 template <typename T>
 void HashCombine(size_t* hash, const std::vector<T>& vector) {
   HashCombine(hash, vector.size());
@@ -61,20 +61,19 @@
   }
 }
 
+/// HashCombine "hashes" together an existing hash and hashable values.
 template <typename T, typename... ARGS>
 void HashCombine(size_t* hash, const T& value, const ARGS&... args) {
   HashCombine(hash, value);
   HashCombine(hash, args...);
 }
 
-}  // namespace detail
-
 /// @returns a hash of the combined arguments. The returned hash is dependent on
 /// the order of the arguments.
 template <typename... ARGS>
 size_t Hash(const ARGS&... args) {
   size_t hash = 102931;  // seed with an arbitrary prime
-  detail::HashCombine(&hash, args...);
+  HashCombine(&hash, args...);
   return hash;
 }
 
diff --git a/src/writer/hlsl/generator_impl.cc b/src/writer/hlsl/generator_impl.cc
index fc9e505..65b44c9 100644
--- a/src/writer/hlsl/generator_impl.cc
+++ b/src/writer/hlsl/generator_impl.cc
@@ -1143,7 +1143,7 @@
       // InterlockedExchange and discard the returned value
       {  // T result = 0;
         auto pre = line();
-        auto* value_ty = intrinsic->Parameters()[1].type;
+        auto* value_ty = intrinsic->Parameters()[1]->Type();
         if (!EmitTypeAndName(pre, value_ty, ast::StorageClass::kNone,
                              ast::Access::kUndefined, result)) {
           return false;
@@ -1278,9 +1278,9 @@
   return CallIntrinsicHelper(
       out, expr, intrinsic,
       [&](TextBuffer* b, const std::vector<std::string>& params) {
-        auto* significand_ty = intrinsic->Parameters()[0].type;
+        auto* significand_ty = intrinsic->Parameters()[0]->Type();
         auto significand = params[0];
-        auto* exponent_ty = intrinsic->Parameters()[1].type;
+        auto* exponent_ty = intrinsic->Parameters()[1]->Type();
         auto exponent = params[1];
 
         std::string width;
@@ -1314,7 +1314,7 @@
   return CallIntrinsicHelper(
       out, expr, intrinsic,
       [&](TextBuffer* b, const std::vector<std::string>& params) {
-        auto* input_ty = intrinsic->Parameters()[0].type;
+        auto* input_ty = intrinsic->Parameters()[0]->Type();
 
         std::string width;
         if (auto* vec = input_ty->As<sem::Vector>()) {
@@ -2434,12 +2434,13 @@
         }
 
         if (wgsize[i].overridable_const) {
-          auto* sem_const = builder_.Sem().Get(wgsize[i].overridable_const);
-          if (!sem_const->IsPipelineConstant()) {
+          auto* global = builder_.Sem().Get<sem::GlobalVariable>(
+              wgsize[i].overridable_const);
+          if (!global->IsPipelineConstant()) {
             TINT_ICE(Writer, builder_.Diagnostics())
                 << "expected a pipeline-overridable constant";
           }
-          out << kSpecConstantPrefix << sem_const->ConstantId();
+          out << kSpecConstantPrefix << global->ConstantId();
         } else {
           out << std::to_string(wgsize[i].value);
         }
@@ -3187,8 +3188,9 @@
   auto* sem = builder_.Sem().Get(var);
   auto* type = sem->Type();
 
-  if (sem->IsPipelineConstant()) {
-    auto const_id = sem->ConstantId();
+  auto* global = sem->As<sem::GlobalVariable>();
+  if (global && global->IsPipelineConstant()) {
+    auto const_id = global->ConstantId();
 
     line() << "#ifndef " << kSpecConstantPrefix << const_id;
 
@@ -3247,12 +3249,12 @@
       }
       {
         ScopedParen sp(decl);
-        for (auto param : intrinsic->Parameters()) {
+        for (auto* param : intrinsic->Parameters()) {
           if (!parameter_names.empty()) {
             decl << ", ";
           }
           auto param_name = "param_" + std::to_string(parameter_names.size());
-          const auto* ty = param.type;
+          const auto* ty = param->Type();
           if (auto* ptr = ty->As<sem::Pointer>()) {
             decl << "inout ";
             ty = ptr->StoreType();
diff --git a/src/writer/hlsl/generator_impl_type_test.cc b/src/writer/hlsl/generator_impl_type_test.cc
index feedf4e..7db8426 100644
--- a/src/writer/hlsl/generator_impl_type_test.cc
+++ b/src/writer/hlsl/generator_impl_type_test.cc
@@ -19,6 +19,7 @@
 #include "src/sem/depth_texture_type.h"
 #include "src/sem/multisampled_texture_type.h"
 #include "src/sem/sampled_texture_type.h"
+#include "src/sem/sampler_type.h"
 #include "src/sem/storage_texture_type.h"
 #include "src/writer/hlsl/test_helper.h"
 
diff --git a/src/writer/msl/generator_impl.cc b/src/writer/msl/generator_impl.cc
index 39292bd..c22b146 100644
--- a/src/writer/msl/generator_impl.cc
+++ b/src/writer/msl/generator_impl.cc
@@ -2323,9 +2323,9 @@
     out << " " << program_->Symbols().NameFor(var->symbol());
   }
 
-  auto* sem_var = program_->Sem().Get(var);
-  if (sem_var->IsPipelineConstant()) {
-    out << " [[function_constant(" << sem_var->ConstantId() << ")]]";
+  auto* global = program_->Sem().Get<sem::GlobalVariable>(var);
+  if (global && global->IsPipelineConstant()) {
+    out << " [[function_constant(" << global->ConstantId() << ")]]";
   } else if (var->constructor() != nullptr) {
     out << " = ";
     if (!EmitExpression(out, var->constructor())) {
diff --git a/src/writer/msl/generator_impl_type_test.cc b/src/writer/msl/generator_impl_type_test.cc
index fa55b75..646ae7a 100644
--- a/src/writer/msl/generator_impl_type_test.cc
+++ b/src/writer/msl/generator_impl_type_test.cc
@@ -20,6 +20,7 @@
 #include "src/sem/depth_texture_type.h"
 #include "src/sem/multisampled_texture_type.h"
 #include "src/sem/sampled_texture_type.h"
+#include "src/sem/sampler_type.h"
 #include "src/sem/storage_texture_type.h"
 #include "src/writer/msl/test_helper.h"
 
diff --git a/src/writer/spirv/builder.cc b/src/writer/spirv/builder.cc
index d9bf363..f8e8ead 100644
--- a/src/writer/spirv/builder.cc
+++ b/src/writer/spirv/builder.cc
@@ -492,7 +492,8 @@
         auto constant = ScalarConstant::U32(wgsize[i].value);
         if (wgsize[i].overridable_const) {
           // Make the constant specializable.
-          auto* sem_const = builder_.Sem().Get(wgsize[i].overridable_const);
+          auto* sem_const = builder_.Sem().Get<sem::GlobalVariable>(
+              wgsize[i].overridable_const);
           if (!sem_const->IsPipelineConstant()) {
             TINT_ICE(Writer, builder_.Diagnostics())
                 << "expected a pipeline-overridable constant";
@@ -1635,10 +1636,10 @@
                                           ast::Literal* lit) {
   ScalarConstant constant;
 
-  auto* sem_var = builder_.Sem().Get(var);
-  if (sem_var && sem_var->IsPipelineConstant()) {
+  auto* global = builder_.Sem().Get<sem::GlobalVariable>(var);
+  if (global && global->IsPipelineConstant()) {
     constant.is_spec_op = true;
-    constant.constant_id = sem_var->ConstantId();
+    constant.constant_id = global->ConstantId();
   }
 
   if (auto* l = lit->As<ast::BoolLiteral>()) {
@@ -2295,13 +2296,13 @@
   // and loads it if necessary. Returns 0 on error.
   auto get_param_as_value_id = [&](size_t i) -> uint32_t {
     auto* arg = call->params()[i];
-    auto& param = intrinsic->Parameters()[i];
+    auto* param = intrinsic->Parameters()[i];
     auto val_id = GenerateExpression(arg);
     if (val_id == 0) {
       return 0;
     }
 
-    if (!param.type->Is<sem::Pointer>()) {
+    if (!param->Type()->Is<sem::Pointer>()) {
       val_id = GenerateLoadIfNeeded(TypeOf(arg), val_id);
     }
     return val_id;
@@ -2527,7 +2528,8 @@
       // splat the condition into a vector of the same size.
       // TODO(jrprice): If we're targeting SPIR-V 1.4, we don't need to do this.
       auto* result_vector_type = intrinsic->ReturnType()->As<sem::Vector>();
-      if (result_vector_type && intrinsic->Parameters()[2].type->is_scalar()) {
+      if (result_vector_type &&
+          intrinsic->Parameters()[2]->Type()->is_scalar()) {
         sem::Bool bool_type;
         sem::Vector bool_vec_type(&bool_type, result_vector_type->size());
         if (!GenerateTypeIfNeeded(&bool_vec_type)) {
@@ -3036,14 +3038,15 @@
                                       Operand result_type,
                                       Operand result_id) {
   auto is_value_signed = [&] {
-    return intrinsic->Parameters()[1].type->Is<sem::I32>();
+    return intrinsic->Parameters()[1]->Type()->Is<sem::I32>();
   };
 
   auto storage_class =
-      intrinsic->Parameters()[0].type->As<sem::Pointer>()->StorageClass();
+      intrinsic->Parameters()[0]->Type()->As<sem::Pointer>()->StorageClass();
 
   uint32_t memory_id = 0;
-  switch (intrinsic->Parameters()[0].type->As<sem::Pointer>()->StorageClass()) {
+  switch (
+      intrinsic->Parameters()[0]->Type()->As<sem::Pointer>()->StorageClass()) {
     case ast::StorageClass::kWorkgroup:
       memory_id = GenerateConstantIfNeeded(
           ScalarConstant::U32(static_cast<uint32_t>(spv::Scope::Workgroup)));
diff --git a/src/writer/spirv/builder.h b/src/writer/spirv/builder.h
index a81da1e..c33db7c 100644
--- a/src/writer/spirv/builder.h
+++ b/src/writer/spirv/builder.h
@@ -35,6 +35,7 @@
 #include "src/ast/variable_decl_statement.h"
 #include "src/program_builder.h"
 #include "src/scope_stack.h"
+#include "src/sem/intrinsic.h"
 #include "src/sem/storage_texture_type.h"
 #include "src/writer/spirv/function.h"
 #include "src/writer/spirv/scalar_constant.h"