tint/ast: Derive off `ast::Variable`

Add the new classes:
* `ast::Let`
* `ast::Override`
* `ast::Parameter`
* `ast::Var`

Limit the fields to those that are only applicable for their type.

Note: The resolver and validator is a tangled mess for each of the
variable types. This CL tries to keep the functionality exactly the
same. I'll clean this up in another change.

Bug: tint:1582
Change-Id: Iee83324167ffd4d92ae3032b2134677629c90079
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/93780
Reviewed-by: Dan Sinclair <dsinclair@chromium.org>
Commit-Queue: Ben Clayton <bclayton@chromium.org>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/src/tint/resolver/dependency_graph.cc b/src/tint/resolver/dependency_graph.cc
index cf61149..6b3b779 100644
--- a/src/tint/resolver/dependency_graph.cc
+++ b/src/tint/resolver/dependency_graph.cc
@@ -487,11 +487,13 @@
     /// declaration
     std::string KindOf(const ast::Node* node) {
         return Switch(
-            node,  //
-            [&](const ast::Struct*) { return "struct"; },
-            [&](const ast::Alias*) { return "alias"; },
-            [&](const ast::Function*) { return "function"; },
-            [&](const ast::Variable* var) { return var->is_const ? "let" : "var"; },
+            node,                                              //
+            [&](const ast::Struct*) { return "struct"; },      //
+            [&](const ast::Alias*) { return "alias"; },        //
+            [&](const ast::Function*) { return "function"; },  //
+            [&](const ast::Let*) { return "let"; },            //
+            [&](const ast::Var*) { return "var"; },            //
+            [&](const ast::Override*) { return "override"; },  //
             [&](Default) {
                 UnhandledNode(diagnostics_, node);
                 return "<error>";
diff --git a/src/tint/resolver/pipeline_overridable_constant_test.cc b/src/tint/resolver/pipeline_overridable_constant_test.cc
index 035936c3..583cc16 100644
--- a/src/tint/resolver/pipeline_overridable_constant_test.cc
+++ b/src/tint/resolver/pipeline_overridable_constant_test.cc
@@ -31,7 +31,7 @@
         auto* sem = Sem().Get<sem::GlobalVariable>(var);
         ASSERT_NE(sem, nullptr);
         EXPECT_EQ(sem->Declaration(), var);
-        EXPECT_TRUE(sem->IsOverridable());
+        EXPECT_TRUE(sem->Declaration()->Is<ast::Override>());
         EXPECT_EQ(sem->ConstantId(), id);
         EXPECT_FALSE(sem->ConstantValue());
     }
@@ -45,7 +45,7 @@
     auto* sem_a = Sem().Get<sem::GlobalVariable>(a);
     ASSERT_NE(sem_a, nullptr);
     EXPECT_EQ(sem_a->Declaration(), a);
-    EXPECT_FALSE(sem_a->IsOverridable());
+    EXPECT_FALSE(sem_a->Declaration()->Is<ast::Override>());
     EXPECT_TRUE(sem_a->ConstantValue());
 }
 
diff --git a/src/tint/resolver/resolver.cc b/src/tint/resolver/resolver.cc
index 3d58930..3438a13 100644
--- a/src/tint/resolver/resolver.cc
+++ b/src/tint/resolver/resolver.cc
@@ -303,59 +303,63 @@
     return s;
 }
 
-sem::Variable* Resolver::Variable(const ast::Variable* var,
-                                  VariableKind kind,
+sem::Variable* Resolver::Variable(const ast::Variable* v,
+                                  bool is_global,
                                   uint32_t index /* = 0 */) {
     const sem::Type* storage_ty = nullptr;
 
     // If the variable has a declared type, resolve it.
-    if (auto* ty = var->type) {
+    if (auto* ty = v->type) {
         storage_ty = Type(ty);
         if (!storage_ty) {
             return nullptr;
         }
     }
 
+    auto* as_var = v->As<ast::Var>();
+    auto* as_let = v->As<ast::Let>();
+    auto* as_override = v->As<ast::Override>();
+    auto* as_param = v->As<ast::Parameter>();
+
     const sem::Expression* rhs = nullptr;
 
     // Does the variable have a constructor?
-    if (var->constructor) {
-        rhs = Materialize(Expression(var->constructor), storage_ty);
+    if (v->constructor) {
+        rhs = Materialize(Expression(v->constructor), storage_ty);
         if (!rhs) {
             return nullptr;
         }
 
         // If the variable has no declared type, infer it from the RHS
         if (!storage_ty) {
-            if (!var->is_const && kind == VariableKind::kGlobal) {
-                AddError("module-scope 'var' declaration must specify a type", var->source);
+            if (as_var && is_global) {
+                AddError("module-scope 'var' declaration must specify a type", v->source);
                 return nullptr;
             }
 
             storage_ty = rhs->Type()->UnwrapRef();  // Implicit load of RHS
         }
-    } else if (var->is_const && !var->is_overridable && kind != VariableKind::kParameter) {
-        AddError("'let' declaration must have an initializer", var->source);
+    } else if (as_let) {
+        AddError("'let' declaration must have an initializer", v->source);
         return nullptr;
-    } else if (!var->type) {
-        AddError((kind == VariableKind::kGlobal)
-                     ? "module-scope 'var' declaration requires a type or initializer"
-                     : "function-scope 'var' declaration requires a type or initializer",
-                 var->source);
+    } else if (!v->type) {
+        AddError((is_global) ? "module-scope 'var' declaration requires a type or initializer"
+                             : "function-scope 'var' declaration requires a type or initializer",
+                 v->source);
         return nullptr;
     }
 
     if (!storage_ty) {
         TINT_ICE(Resolver, diagnostics_) << "failed to determine storage type for variable '" +
-                                                builder_->Symbols().NameFor(var->symbol) + "'\n"
-                                         << "Source: " << var->source;
+                                                builder_->Symbols().NameFor(v->symbol) + "'\n"
+                                         << "Source: " << v->source;
         return nullptr;
     }
 
-    auto storage_class = var->declared_storage_class;
-    if (storage_class == ast::StorageClass::kNone && !var->is_const) {
+    auto storage_class = as_var ? as_var->declared_storage_class : ast::StorageClass::kNone;
+    if (storage_class == ast::StorageClass::kNone && as_var) {
         // No declared storage class. Infer from usage / type.
-        if (kind == VariableKind::kLocal) {
+        if (!is_global) {
             storage_class = ast::StorageClass::kFunction;
         } else if (storage_ty->UnwrapRef()->is_handle()) {
             // https://gpuweb.github.io/gpuweb/wgsl/#module-scope-variables
@@ -366,93 +370,83 @@
         }
     }
 
-    if (kind == VariableKind::kLocal && !var->is_const &&
-        storage_class != ast::StorageClass::kFunction &&
-        validator_.IsValidationEnabled(var->attributes,
+    if (!is_global && as_var && storage_class != ast::StorageClass::kFunction &&
+        validator_.IsValidationEnabled(v->attributes,
                                        ast::DisabledValidation::kIgnoreStorageClass)) {
-        AddError("function-scope 'var' declaration must use 'function' storage class", var->source);
+        AddError("function-scope 'var' declaration must use 'function' storage class", v->source);
         return nullptr;
     }
 
-    auto access = var->declared_access;
+    auto access = as_var ? as_var->declared_access : ast::Access::kUndefined;
     if (access == ast::Access::kUndefined) {
         access = DefaultAccessForStorageClass(storage_class);
     }
 
     auto* var_ty = storage_ty;
-    if (!var->is_const) {
-        // Variable declaration. Unlike `let`, `var` has storage.
+    if (as_var) {
+        // Variable declaration. Unlike `let` and parameters, `var` has storage.
         // Variables are always of a reference type to the declared storage type.
         var_ty = builder_->create<sem::Reference>(storage_ty, storage_class, access);
     }
 
-    if (rhs && !validator_.VariableConstructorOrCast(var, storage_class, storage_ty, rhs->Type())) {
+    if (rhs && !validator_.VariableConstructorOrCast(v, storage_class, storage_ty, rhs->Type())) {
         return nullptr;
     }
 
-    if (!ApplyStorageClassUsageToType(storage_class, const_cast<sem::Type*>(var_ty), var->source)) {
-        AddNote(std::string("while instantiating ") +
-                    ((kind == VariableKind::kParameter) ? "parameter " : "variable ") +
-                    builder_->Symbols().NameFor(var->symbol),
-                var->source);
+    if (!ApplyStorageClassUsageToType(storage_class, const_cast<sem::Type*>(var_ty), v->source)) {
+        AddNote(std::string("while instantiating ") + ((as_param) ? "parameter " : "variable ") +
+                    builder_->Symbols().NameFor(v->symbol),
+                v->source);
         return nullptr;
     }
 
-    if (kind == VariableKind::kParameter) {
+    if (as_param) {
         if (auto* ptr = var_ty->As<sem::Pointer>()) {
             // For MSL, we push module-scope variables into the entry point as pointer
             // parameters, so we also need to handle their store type.
             if (!ApplyStorageClassUsageToType(
-                    ptr->StorageClass(), const_cast<sem::Type*>(ptr->StoreType()), var->source)) {
-                AddNote("while instantiating parameter " + builder_->Symbols().NameFor(var->symbol),
-                        var->source);
+                    ptr->StorageClass(), const_cast<sem::Type*>(ptr->StoreType()), v->source)) {
+                AddNote("while instantiating parameter " + builder_->Symbols().NameFor(v->symbol),
+                        v->source);
                 return nullptr;
             }
         }
+        auto* param =
+            builder_->create<sem::Parameter>(as_param, index, var_ty, storage_class, access);
+        builder_->Sem().Add(as_param, param);
+        return param;
     }
 
-    switch (kind) {
-        case VariableKind::kGlobal: {
-            sem::BindingPoint binding_point;
-            if (auto bp = var->BindingPoint()) {
+    if (is_global) {
+        sem::BindingPoint binding_point;
+        if (as_var) {
+            if (auto bp = as_var->BindingPoint()) {
                 binding_point = {bp.group->value, bp.binding->value};
             }
+        }
 
-            bool has_const_val = rhs && var->is_const && !var->is_overridable;
-            auto* global = builder_->create<sem::GlobalVariable>(
-                var, var_ty, storage_class, access,
-                has_const_val ? rhs->ConstantValue() : sem::Constant{}, binding_point);
+        bool has_const_val = rhs && as_let && !as_override;
+        auto* global = builder_->create<sem::GlobalVariable>(
+            v, var_ty, storage_class, access,
+            has_const_val ? rhs->ConstantValue() : sem::Constant{}, binding_point);
 
-            if (var->is_overridable) {
-                global->SetIsOverridable();
-                if (auto* id = ast::GetAttribute<ast::IdAttribute>(var->attributes)) {
-                    global->SetConstantId(static_cast<uint16_t>(id->value));
-                }
+        if (as_override) {
+            if (auto* id = ast::GetAttribute<ast::IdAttribute>(v->attributes)) {
+                global->SetConstantId(static_cast<uint16_t>(id->value));
             }
+        }
 
-            global->SetConstructor(rhs);
-
-            builder_->Sem().Add(var, global);
-            return global;
-        }
-        case VariableKind::kLocal: {
-            auto* local = builder_->create<sem::LocalVariable>(
-                var, var_ty, storage_class, access, current_statement_,
-                (rhs && var->is_const) ? rhs->ConstantValue() : sem::Constant{});
-            builder_->Sem().Add(var, local);
-            local->SetConstructor(rhs);
-            return local;
-        }
-        case VariableKind::kParameter: {
-            auto* param =
-                builder_->create<sem::Parameter>(var, index, var_ty, storage_class, access);
-            builder_->Sem().Add(var, param);
-            return param;
-        }
+        global->SetConstructor(rhs);
+        builder_->Sem().Add(v, global);
+        return global;
     }
 
-    TINT_UNREACHABLE(Resolver, diagnostics_) << "unhandled VariableKind " << static_cast<int>(kind);
-    return nullptr;
+    auto* local = builder_->create<sem::LocalVariable>(
+        v, var_ty, storage_class, access, current_statement_,
+        (rhs && as_let) ? rhs->ConstantValue() : sem::Constant{});
+    builder_->Sem().Add(v, local);
+    local->SetConstructor(rhs);
+    return local;
 }
 
 ast::Access Resolver::DefaultAccessForStorageClass(ast::StorageClass storage_class) {
@@ -477,13 +471,13 @@
     // TODO(crbug.com/tint/1192): If a transform changes the order or removes an
     // unused constant, the allocation may change on the next Resolver pass.
     for (auto* decl : builder_->AST().GlobalDeclarations()) {
-        auto* var = decl->As<ast::Variable>();
-        if (!var || !var->is_overridable) {
+        auto* override = decl->As<ast::Override>();
+        if (!override) {
             continue;
         }
 
         uint16_t constant_id;
-        if (auto* id_attr = ast::GetAttribute<ast::IdAttribute>(var->attributes)) {
+        if (auto* id_attr = ast::GetAttribute<ast::IdAttribute>(override->attributes)) {
             constant_id = static_cast<uint16_t>(id_attr->value);
         } else {
             // No ID was specified, so allocate the next available ID.
@@ -499,7 +493,7 @@
             next_constant_id = constant_id + 1;
         }
 
-        auto* sem = sem_.Get<sem::GlobalVariable>(var);
+        auto* sem = sem_.Get<sem::GlobalVariable>(override);
         const_cast<sem::GlobalVariable*>(sem)->SetConstantId(constant_id);
     }
 }
@@ -513,25 +507,21 @@
     }
 }
 
-sem::GlobalVariable* Resolver::GlobalVariable(const ast::Variable* var) {
-    auto* sem = Variable(var, VariableKind::kGlobal);
+sem::GlobalVariable* Resolver::GlobalVariable(const ast::Variable* v) {
+    auto* sem = As<sem::GlobalVariable>(Variable(v, /* is_global */ true));
     if (!sem) {
         return nullptr;
     }
 
+    const bool is_var = v->Is<ast::Var>();
+
     auto storage_class = sem->StorageClass();
-    if (!var->is_const && storage_class == ast::StorageClass::kNone) {
-        AddError("module-scope 'var' declaration must have a storage class", var->source);
-        return nullptr;
-    }
-    if (var->is_const && storage_class != ast::StorageClass::kNone) {
-        AddError(var->is_overridable ? "'override' declaration must not have a storage class"
-                                     : "'let' declaration must not have a storage class",
-                 var->source);
+    if (is_var && storage_class == ast::StorageClass::kNone) {
+        AddError("module-scope 'var' declaration must have a storage class", v->source);
         return nullptr;
     }
 
-    for (auto* attr : var->attributes) {
+    for (auto* attr : v->attributes) {
         Mark(attr);
 
         if (auto* id_attr = attr->As<ast::IdAttribute>()) {
@@ -540,7 +530,7 @@
         }
     }
 
-    if (!validator_.NoDuplicateAttributes(var->attributes)) {
+    if (!validator_.NoDuplicateAttributes(v->attributes)) {
         return nullptr;
     }
 
@@ -576,9 +566,8 @@
             }
         }
 
-        auto* var =
-            As<sem::Parameter>(Variable(param, VariableKind::kParameter, parameter_index++));
-        if (!var) {
+        auto* p = As<sem::Parameter>(Variable(param, false, parameter_index++));
+        if (!p) {
             return nullptr;
         }
 
@@ -589,10 +578,10 @@
             return nullptr;
         }
 
-        parameters.emplace_back(var);
+        parameters.emplace_back(p);
 
-        auto* var_ty = const_cast<sem::Type*>(var->Type());
-        if (auto* str = var_ty->As<sem::Struct>()) {
+        auto* p_ty = const_cast<sem::Type*>(p->Type());
+        if (auto* str = p_ty->As<sem::Struct>()) {
             switch (decl->PipelineStage()) {
                 case ast::PipelineStage::kVertex:
                     str->AddUsage(sem::PipelineStageUsage::kVertexInput);
@@ -777,12 +766,12 @@
         if (auto* user = args[i]->As<sem::VariableUser>()) {
             // We have an variable of a module-scope constant.
             auto* decl = user->Variable()->Declaration();
-            if (!decl->is_const) {
+            if (!decl->IsAnyOf<ast::Let, ast::Override>()) {
                 AddError(kErrBadType, values[i]->source);
                 return false;
             }
             // Capture the constant if it is pipeline-overridable.
-            if (decl->is_overridable) {
+            if (decl->Is<ast::Override>()) {
                 ws[i].overridable_const = decl;
             }
 
@@ -2104,19 +2093,19 @@
             return nullptr;
         }
 
+        constexpr const char* kErrInvalidExpr =
+            "array size identifier must be a literal or a module-scope 'let'";
+
         if (auto* ident = count_expr->As<ast::IdentifierExpression>()) {
-            // Make sure the identifier is a non-overridable module-scope constant.
-            auto* var = sem_.ResolvedSymbol<sem::GlobalVariable>(ident);
-            if (!var || !var->Declaration()->is_const || var->IsOverridable()) {
-                AddError("array size identifier must be a literal or a module-scope 'let'",
-                         size_source);
+            // Make sure the identifier is a non-overridable module-scope 'let'.
+            auto* global = sem_.ResolvedSymbol<sem::GlobalVariable>(ident);
+            if (!global || !global->Declaration()->Is<ast::Let>()) {
+                AddError(kErrInvalidExpr, size_source);
                 return nullptr;
             }
-
-            count_expr = var->Declaration()->constructor;
+            count_expr = global->Declaration()->constructor;
         } else if (!count_expr->Is<ast::LiteralExpression>()) {
-            AddError("array size identifier must be a literal or a module-scope 'let'",
-                     size_source);
+            AddError(kErrInvalidExpr, size_source);
             return nullptr;
         }
 
@@ -2437,7 +2426,7 @@
     return StatementScope(stmt, sem, [&] {
         Mark(stmt->variable);
 
-        auto* var = Variable(stmt->variable, VariableKind::kLocal);
+        auto* var = Variable(stmt->variable, /* is_global */ false);
         if (!var) {
             return false;
         }
diff --git a/src/tint/resolver/resolver.h b/src/tint/resolver/resolver.h
index e619322..5f24169 100644
--- a/src/tint/resolver/resolver.h
+++ b/src/tint/resolver/resolver.h
@@ -109,9 +109,6 @@
     const Validator* GetValidatorForTesting() const { return &validator_; }
 
   private:
-    /// Describes the context in which a variable is declared
-    enum class VariableKind { kParameter, kLocal, kGlobal };
-
     Validator::ValidTypeStorageLayouts valid_type_storage_layouts_;
 
     /// Structure holding semantic information about a block (i.e. scope), such as
@@ -298,9 +295,9 @@
     /// @note this method does not resolve the attributes as these are
     /// context-dependent (global, local, parameter)
     /// @param var the variable to create or return the `VariableInfo` for
-    /// @param kind what kind of variable we are declaring
+    /// @param is_global true if this is module scope, otherwise function scope
     /// @param index the index of the parameter, if this variable is a parameter
-    sem::Variable* Variable(const ast::Variable* var, VariableKind kind, uint32_t index = 0);
+    sem::Variable* Variable(const ast::Variable* var, bool is_global, uint32_t index = 0);
 
     /// Records the storage class usage for the given type, and any transient
     /// dependencies of the type. Validates that the type can be used for the
diff --git a/src/tint/resolver/type_validation_test.cc b/src/tint/resolver/type_validation_test.cc
index 27ee04d..8f7ef1a 100644
--- a/src/tint/resolver/type_validation_test.cc
+++ b/src/tint/resolver/type_validation_test.cc
@@ -87,26 +87,6 @@
     EXPECT_TRUE(r()->Resolve()) << r()->error();
 }
 
-TEST_F(ResolverTypeValidationTest, GlobalLetWithStorageClass_Fail) {
-    // let<private> global_var: f32;
-    AST().AddGlobalVariable(create<ast::Variable>(
-        Source{{12, 34}}, Symbols().Register("global_let"), ast::StorageClass::kPrivate,
-        ast::Access::kUndefined, ty.f32(), true, false, Expr(1.23_f), ast::AttributeList{}));
-
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: 'let' declaration must not have a storage class");
-}
-
-TEST_F(ResolverTypeValidationTest, OverrideWithStorageClass_Fail) {
-    // let<private> global_var: f32;
-    AST().AddGlobalVariable(create<ast::Variable>(
-        Source{{12, 34}}, Symbols().Register("global_override"), ast::StorageClass::kPrivate,
-        ast::Access::kUndefined, ty.f32(), true, true, Expr(1.23_f), ast::AttributeList{}));
-
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: 'override' declaration must not have a storage class");
-}
-
 TEST_F(ResolverTypeValidationTest, GlobalConstNoStorageClass_Pass) {
     // let global_var: f32;
     GlobalConst(Source{{12, 34}}, "global_var", ty.f32(), Construct(ty.f32()));
diff --git a/src/tint/resolver/uniformity.cc b/src/tint/resolver/uniformity.cc
index 18fa425..15ca72f 100644
--- a/src/tint/resolver/uniformity.cc
+++ b/src/tint/resolver/uniformity.cc
@@ -949,7 +949,7 @@
                 }
                 current_function_->variables.Set(sem_.Get(decl->variable), node);
 
-                if (!decl->variable->is_const) {
+                if (decl->variable->Is<ast::Var>()) {
                     current_function_->local_var_decls.insert(
                         sem_.Get<sem::LocalVariable>(decl->variable));
                 }
@@ -1018,7 +1018,8 @@
             },
 
             [&](const sem::GlobalVariable* global) {
-                if (global->Declaration()->is_const || global->Access() == ast::Access::kRead) {
+                if (!global->Declaration()->Is<ast::Var>() ||
+                    global->Access() == ast::Access::kRead) {
                     node->AddEdge(cf);
                 } else {
                     node->AddEdge(current_function_->may_be_non_uniform);
diff --git a/src/tint/resolver/validator.cc b/src/tint/resolver/validator.cc
index 39eb31a..7562b79 100644
--- a/src/tint/resolver/validator.cc
+++ b/src/tint/resolver/validator.cc
@@ -297,7 +297,7 @@
     return true;
 }
 
-bool Validator::VariableConstructorOrCast(const ast::Variable* var,
+bool Validator::VariableConstructorOrCast(const ast::Variable* v,
                                           ast::StorageClass storage_class,
                                           const sem::Type* storage_ty,
                                           const sem::Type* rhs_ty) const {
@@ -305,14 +305,14 @@
 
     // Value type has to match storage type
     if (storage_ty != value_type) {
-        std::string decl = var->is_const ? "let" : "var";
+        std::string decl = v->Is<ast::Let>() ? "let" : "var";
         AddError("cannot initialize " + decl + " of type '" + sem_.TypeNameOf(storage_ty) +
                      "' with value of type '" + sem_.TypeNameOf(rhs_ty) + "'",
-                 var->source);
+                 v->source);
         return false;
     }
 
-    if (!var->is_const) {
+    if (v->Is<ast::Var>()) {
         switch (storage_class) {
             case ast::StorageClass::kPrivate:
             case ast::StorageClass::kFunction:
@@ -325,7 +325,7 @@
                              "' cannot have an initializer. var initializers are only "
                              "supported for the storage classes "
                              "'private' and 'function'",
-                         var->source);
+                         v->source);
                 return false;
         }
     }
@@ -502,21 +502,22 @@
 }
 
 bool Validator::GlobalVariable(
-    const sem::Variable* var,
+    const sem::GlobalVariable* global,
     std::unordered_map<uint32_t, const sem::Variable*> constant_ids,
     std::unordered_map<const sem::Type*, const Source&> atomic_composite_info) const {
-    auto* decl = var->Declaration();
+    auto* decl = global->Declaration();
     if (!NoDuplicateAttributes(decl->attributes)) {
         return false;
     }
 
-    for (auto* attr : decl->attributes) {
-        if (decl->is_const) {
-            if (decl->is_overridable) {
+    bool ok = Switch(
+        decl,  //
+        [&](const ast::Override*) {
+            for (auto* attr : decl->attributes) {
                 if (auto* id_attr = attr->As<ast::IdAttribute>()) {
                     uint32_t id = id_attr->value;
                     auto it = constant_ids.find(id);
-                    if (it != constant_ids.end() && it->second != var) {
+                    if (it != constant_ids.end() && it->second != global) {
                         AddError("pipeline constant IDs must be unique", attr->source);
                         AddNote("a pipeline constant with an ID of " + std::to_string(id) +
                                     " was previously declared here:",
@@ -533,32 +534,45 @@
                     AddError("attribute is not valid for 'override' declaration", attr->source);
                     return false;
                 }
-            } else {
-                AddError("attribute is not valid for module-scope 'let' declaration", attr->source);
+            }
+            return true;
+        },
+        [&](const ast::Let*) {
+            if (!decl->attributes.empty()) {
+                AddError("attribute is not valid for module-scope 'let' declaration",
+                         decl->attributes[0]->source);
                 return false;
             }
-        } else {
-            bool is_shader_io_attribute =
-                attr->IsAnyOf<ast::BuiltinAttribute, ast::InterpolateAttribute,
-                              ast::InvariantAttribute, ast::LocationAttribute>();
-            bool has_io_storage_class = var->StorageClass() == ast::StorageClass::kInput ||
-                                        var->StorageClass() == ast::StorageClass::kOutput;
-            if (!(attr->IsAnyOf<ast::BindingAttribute, ast::GroupAttribute,
-                                ast::InternalAttribute>()) &&
-                (!is_shader_io_attribute || !has_io_storage_class)) {
-                AddError("attribute is not valid for module-scope 'var'", attr->source);
-                return false;
+            return true;
+        },
+        [&](const ast::Var*) {
+            for (auto* attr : decl->attributes) {
+                bool is_shader_io_attribute =
+                    attr->IsAnyOf<ast::BuiltinAttribute, ast::InterpolateAttribute,
+                                  ast::InvariantAttribute, ast::LocationAttribute>();
+                bool has_io_storage_class = global->StorageClass() == ast::StorageClass::kInput ||
+                                            global->StorageClass() == ast::StorageClass::kOutput;
+                if (!attr->IsAnyOf<ast::BindingAttribute, ast::GroupAttribute,
+                                   ast::InternalAttribute>() &&
+                    (!is_shader_io_attribute || !has_io_storage_class)) {
+                    AddError("attribute is not valid for module-scope 'var'", attr->source);
+                    return false;
+                }
             }
-        }
+            return true;
+        });
+
+    if (!ok) {
+        return false;
     }
 
-    if (var->StorageClass() == ast::StorageClass::kFunction) {
+    if (global->StorageClass() == ast::StorageClass::kFunction) {
         AddError("module-scope 'var' must not use storage class 'function'", decl->source);
         return false;
     }
 
     auto binding_point = decl->BindingPoint();
-    switch (var->StorageClass()) {
+    switch (global->StorageClass()) {
         case ast::StorageClass::kUniform:
         case ast::StorageClass::kStorage:
         case ast::StorageClass::kHandle: {
@@ -581,23 +595,23 @@
             }
     }
 
-    // https://gpuweb.github.io/gpuweb/wgsl/#variable-declaration
-    // The access mode always has a default, and except for variables in the
-    // storage storage class, must not be written.
-    if (var->StorageClass() != ast::StorageClass::kStorage &&
-        decl->declared_access != ast::Access::kUndefined) {
-        AddError("only variables in <storage> storage class may declare an access mode",
-                 decl->source);
-        return false;
-    }
+    if (auto* var = decl->As<ast::Var>()) {
+        // https://gpuweb.github.io/gpuweb/wgsl/#variable-declaration
+        // The access mode always has a default, and except for variables in the
+        // storage storage class, must not be written.
+        if (global->StorageClass() != ast::StorageClass::kStorage &&
+            var->declared_access != ast::Access::kUndefined) {
+            AddError("only variables in <storage> storage class may declare an access mode",
+                     var->source);
+            return false;
+        }
 
-    if (!decl->is_const) {
-        if (!AtomicVariable(var, atomic_composite_info)) {
+        if (!AtomicVariable(global, atomic_composite_info)) {
             return false;
         }
     }
 
-    return Variable(var);
+    return Variable(global);
 }
 
 // https://gpuweb.github.io/gpuweb/wgsl/#atomic-types
@@ -641,14 +655,17 @@
     return true;
 }
 
-bool Validator::Variable(const sem::Variable* var) const {
-    auto* decl = var->Declaration();
-    auto* storage_ty = var->Type()->UnwrapRef();
+bool Validator::Variable(const sem::Variable* v) const {
+    auto* decl = v->Declaration();
+    auto* storage_ty = v->Type()->UnwrapRef();
 
-    if (var->Is<sem::GlobalVariable>()) {
+    auto* as_let = decl->As<ast::Let>();
+    auto* as_var = decl->As<ast::Var>();
+
+    if (v->Is<sem::GlobalVariable>()) {
         auto name = symbols_.NameFor(decl->symbol);
         if (sem::ParseBuiltinType(name) != sem::BuiltinType::kNone) {
-            auto* kind = var->Declaration()->is_const ? "let" : "var";
+            auto* kind = as_let ? "let" : "var";
             AddError(
                 "'" + name + "' is a builtin and cannot be redeclared as a module-scope " + kind,
                 decl->source);
@@ -656,14 +673,13 @@
         }
     }
 
-    if (!decl->is_const && !IsStorable(storage_ty)) {
+    if (as_var && !IsStorable(storage_ty)) {
         AddError(sem_.TypeNameOf(storage_ty) + " cannot be used as the type of a var",
                  decl->source);
         return false;
     }
 
-    if (decl->is_const && !var->Is<sem::Parameter>() &&
-        !(storage_ty->IsConstructible() || storage_ty->Is<sem::Pointer>())) {
+    if (as_let && !(storage_ty->IsConstructible() || storage_ty->Is<sem::Pointer>())) {
         AddError(sem_.TypeNameOf(storage_ty) + " cannot be used as the type of a let",
                  decl->source);
         return false;
@@ -688,16 +704,17 @@
         }
     }
 
-    if (var->Is<sem::LocalVariable>() && !decl->is_const &&
+    if (v->Is<sem::LocalVariable>() && as_var &&
         IsValidationEnabled(decl->attributes, ast::DisabledValidation::kIgnoreStorageClass)) {
-        if (!var->Type()->UnwrapRef()->IsConstructible()) {
+        if (!v->Type()->UnwrapRef()->IsConstructible()) {
             AddError("function variable must have a constructible type",
                      decl->type ? decl->type->source : decl->source);
             return false;
         }
     }
 
-    if (storage_ty->is_handle() && decl->declared_storage_class != ast::StorageClass::kNone) {
+    if (as_var && storage_ty->is_handle() &&
+        as_var->declared_storage_class != ast::StorageClass::kNone) {
         // https://gpuweb.github.io/gpuweb/wgsl/#module-scope-variables
         // If the store type is a texture type or a sampler type, then the
         // variable declaration must not have a storage class attribute. The
@@ -709,9 +726,10 @@
     }
 
     if (IsValidationEnabled(decl->attributes, ast::DisabledValidation::kIgnoreStorageClass) &&
-        (decl->declared_storage_class == ast::StorageClass::kInput ||
-         decl->declared_storage_class == ast::StorageClass::kOutput)) {
-        AddError("invalid use of input/output storage class", decl->source);
+        as_var &&
+        (as_var->declared_storage_class == ast::StorageClass::kInput ||
+         as_var->declared_storage_class == ast::StorageClass::kOutput)) {
+        AddError("invalid use of input/output storage class", as_var->source);
         return false;
     }
     return true;
@@ -1223,12 +1241,12 @@
 
     // Validate there are no resource variable binding collisions
     std::unordered_map<sem::BindingPoint, const ast::Variable*> binding_points;
-    for (auto* var : func->TransitivelyReferencedGlobals()) {
-        auto* var_decl = var->Declaration();
-        if (!var_decl->BindingPoint()) {
+    for (auto* global : func->TransitivelyReferencedGlobals()) {
+        auto* var_decl = global->Declaration()->As<ast::Var>();
+        if (!var_decl || !var_decl->BindingPoint()) {
             continue;
         }
-        auto bp = var->BindingPoint();
+        auto bp = global->BindingPoint();
         auto res = binding_points.emplace(bp, var_decl);
         if (!res.second &&
             IsValidationEnabled(decl->attributes,
@@ -1663,12 +1681,6 @@
                             TINT_ICE(Resolver, diagnostics_) << "failed to resolve identifier";
                             return false;
                         }
-                        if (var->Declaration()->is_const) {
-                            TINT_ICE(Resolver, diagnostics_)
-                                << "Resolver::FunctionCall() encountered an address-of "
-                                   "expression of a constant identifier expression";
-                            return false;
-                        }
                         is_valid = true;
                     }
                 }
@@ -2172,18 +2184,16 @@
     // https://gpuweb.github.io/gpuweb/wgsl/#assignment-statement
     auto const* lhs_ty = sem_.TypeOf(lhs);
 
-    if (auto* var = sem_.ResolvedSymbol<sem::Variable>(lhs)) {
-        auto* decl = var->Declaration();
-        if (var->Is<sem::Parameter>()) {
-            AddError("cannot assign to function parameter", lhs->source);
-            AddNote("'" + symbols_.NameFor(decl->symbol) + "' is declared here:", decl->source);
-            return false;
-        }
-        if (decl->is_const) {
-            AddError(
-                decl->is_overridable ? "cannot assign to 'override'" : "cannot assign to 'let'",
-                lhs->source);
-            AddNote("'" + symbols_.NameFor(decl->symbol) + "' is declared here:", decl->source);
+    if (auto* variable = sem_.ResolvedSymbol<sem::Variable>(lhs)) {
+        auto* v = variable->Declaration();
+        const char* err = Switch(
+            v,  //
+            [&](const ast::Parameter*) { return "cannot assign to function parameter"; },
+            [&](const ast::Let*) { return "cannot assign to 'let'"; },
+            [&](const ast::Override*) { return "cannot assign to 'override'"; });
+        if (err) {
+            AddError(err, lhs->source);
+            AddNote("'" + symbols_.NameFor(v->symbol) + "' is declared here:", v->source);
             return false;
         }
     }
@@ -2222,17 +2232,16 @@
 
     // https://gpuweb.github.io/gpuweb/wgsl/#increment-decrement
 
-    if (auto* var = sem_.ResolvedSymbol<sem::Variable>(lhs)) {
-        auto* decl = var->Declaration();
-        if (var->Is<sem::Parameter>()) {
-            AddError("cannot modify function parameter", lhs->source);
-            AddNote("'" + symbols_.NameFor(decl->symbol) + "' is declared here:", decl->source);
-            return false;
-        }
-        if (decl->is_const) {
-            AddError(decl->is_overridable ? "cannot modify 'override'" : "cannot modify 'let'",
-                     lhs->source);
-            AddNote("'" + symbols_.NameFor(decl->symbol) + "' is declared here:", decl->source);
+    if (auto* variable = sem_.ResolvedSymbol<sem::Variable>(lhs)) {
+        auto* v = variable->Declaration();
+        const char* err = Switch(
+            v,  //
+            [&](const ast::Parameter*) { return "cannot modify function parameter"; },
+            [&](const ast::Let*) { return "cannot modify 'let'"; },
+            [&](const ast::Override*) { return "cannot modify 'override'"; });
+        if (err) {
+            AddError(err, lhs->source);
+            AddNote("'" + symbols_.NameFor(v->symbol) + "' is declared here:", v->source);
             return false;
         }
     }
diff --git a/src/tint/resolver/validator.h b/src/tint/resolver/validator.h
index e3912ba..223f3cf 100644
--- a/src/tint/resolver/validator.h
+++ b/src/tint/resolver/validator.h
@@ -237,7 +237,7 @@
     /// @param atomic_composite_info atomic composite info in the module
     /// @returns true on success, false otherwise
     bool GlobalVariable(
-        const sem::Variable* var,
+        const sem::GlobalVariable* var,
         std::unordered_map<uint32_t, const sem::Variable*> constant_ids,
         std::unordered_map<const sem::Type*, const Source&> atomic_composite_info) const;
 
@@ -345,12 +345,12 @@
     bool Variable(const sem::Variable* var) const;
 
     /// Validates a variable constructor or cast
-    /// @param var the variable to validate
+    /// @param v the variable to validate
     /// @param storage_class the storage class of the variable
     /// @param storage_type the type of the storage
     /// @param rhs_type the right hand side of the expression
     /// @returns true on succes, false otherwise
-    bool VariableConstructorOrCast(const ast::Variable* var,
+    bool VariableConstructorOrCast(const ast::Variable* v,
                                    ast::StorageClass storage_class,
                                    const sem::Type* storage_type,
                                    const sem::Type* rhs_type) const;
diff --git a/src/tint/resolver/var_let_validation_test.cc b/src/tint/resolver/var_let_validation_test.cc
index eea5e72..67ecd53 100644
--- a/src/tint/resolver/var_let_validation_test.cc
+++ b/src/tint/resolver/var_let_validation_test.cc
@@ -24,22 +24,6 @@
 
 struct ResolverVarLetValidationTest : public resolver::TestHelper, public testing::Test {};
 
-TEST_F(ResolverVarLetValidationTest, LetNoInitializer) {
-    // let a : i32;
-    WrapInFunction(Let(Source{{12, 34}}, "a", ty.i32(), nullptr));
-
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: 'let' declaration must have an initializer");
-}
-
-TEST_F(ResolverVarLetValidationTest, GlobalLetNoInitializer) {
-    // let a : i32;
-    GlobalConst(Source{{12, 34}}, "a", ty.i32(), nullptr);
-
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: 'let' declaration must have an initializer");
-}
-
 TEST_F(ResolverVarLetValidationTest, VarNoInitializerNoType) {
     // var a;
     WrapInFunction(Var(Source{{12, 34}}, "a", nullptr));