tint/resolver: Start handling sem::Expression

Change Resolver::Expression() and Resolver::Identifier() to return a
sem::Expression instead of a sem::ValueExpression.

This is required as IdentifierExpressions may resolve to non-values.

Handle expressions that resolve to types, when they are expected to
resolve to a value.

Bug: tint:1810
Change-Id: I1cb069e3e736fc85af7bc395b388abe153b1f65a
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/118500
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Dan Sinclair <dsinclair@chromium.org>
diff --git a/src/tint/BUILD.gn b/src/tint/BUILD.gn
index 60f4fb7..f08b4ee 100644
--- a/src/tint/BUILD.gn
+++ b/src/tint/BUILD.gn
@@ -386,6 +386,7 @@
     "sem/struct.h",
     "sem/switch_statement.h",
     "sem/type_conversion.h",
+    "sem/type_expression.h",
     "sem/type_initializer.h",
     "sem/type_mappings.h",
     "sem/value_expression.h",
@@ -797,6 +798,8 @@
     "sem/switch_statement.h",
     "sem/type_conversion.cc",
     "sem/type_conversion.h",
+    "sem/type_expression.cc",
+    "sem/type_expression.h",
     "sem/type_initializer.cc",
     "sem/type_initializer.h",
     "sem/type_mappings.h",
@@ -1448,6 +1451,7 @@
       "resolver/diagnostic_control_test.cc",
       "resolver/entry_point_validation_test.cc",
       "resolver/evaluation_stage_test.cc",
+      "resolver/expression_kind_test.cc",
       "resolver/f16_extension_test.cc",
       "resolver/function_validation_test.cc",
       "resolver/host_shareable_validation_test.cc",
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index de1e543..46aa1c4 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -359,6 +359,8 @@
   sem/struct.h
   sem/switch_statement.cc
   sem/switch_statement.h
+  sem/type_expression.cc
+  sem/type_expression.h
   sem/type_initializer.cc
   sem/type_initializer.h
   sem/type_conversion.cc
@@ -943,6 +945,7 @@
     resolver/diagnostic_control_test.cc
     resolver/entry_point_validation_test.cc
     resolver/evaluation_stage_test.cc
+    resolver/expression_kind_test.cc
     resolver/f16_extension_test.cc
     resolver/function_validation_test.cc
     resolver/host_shareable_validation_test.cc
diff --git a/src/tint/resolver/builtin_validation_test.cc b/src/tint/resolver/builtin_validation_test.cc
index 51741d0..2750d97 100644
--- a/src/tint/resolver/builtin_validation_test.cc
+++ b/src/tint/resolver/builtin_validation_test.cc
@@ -118,27 +118,6 @@
     EXPECT_EQ(r()->error(), R"(56:78 error: missing '(' for function call)");
 }
 
-TEST_F(ResolverBuiltinValidationTest, BuiltinRedeclaredAsFunctionUsedAsType) {
-    Func(Source{{12, 34}}, "mix", utils::Empty, ty.i32(),
-         utils::Vector{
-             Return(1_i),
-         });
-    WrapInFunction(Call(ty(Source{{56, 78}}, "mix")));
-
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(56:78 error: cannot use function 'mix' as type
-12:34 note: 'mix' declared here)");
-}
-
-TEST_F(ResolverBuiltinValidationTest, BuiltinRedeclaredAsGlobalConstUsedAsFunction) {
-    GlobalConst(Source{{12, 34}}, "mix", ty.i32(), Expr(1_i));
-    WrapInFunction(Call(Ident(Source{{56, 78}}, "mix"), 1_f, 2_f, 3_f));
-
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(56:78 error: cannot call variable 'mix'
-12:34 note: 'mix' declared here)");
-}
-
 TEST_F(ResolverBuiltinValidationTest, BuiltinRedeclaredAsGlobalConstUsedAsVariable) {
     auto* mix = GlobalConst(Source{{12, 34}}, "mix", ty.i32(), Expr(1_i));
     auto* use = Expr("mix");
@@ -150,24 +129,6 @@
     EXPECT_EQ(sem->Variable(), Sem().Get(mix));
 }
 
-TEST_F(ResolverBuiltinValidationTest, BuiltinRedeclaredAsGlobalConstUsedAsType) {
-    GlobalConst(Source{{12, 34}}, "mix", ty.i32(), Expr(1_i));
-    WrapInFunction(Call(ty(Source{{56, 78}}, "mix")));
-
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(56:78 error: cannot use variable 'mix' as type
-12:34 note: 'mix' declared here)");
-}
-
-TEST_F(ResolverBuiltinValidationTest, BuiltinRedeclaredAsGlobalVarUsedAsFunction) {
-    GlobalVar(Source{{12, 34}}, "mix", ty.i32(), Expr(1_i), type::AddressSpace::kPrivate);
-    WrapInFunction(Call(Ident(Source{{56, 78}}, "mix"), 1_f, 2_f, 3_f));
-
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(56:78 error: cannot call variable 'mix'
-12:34 note: 'mix' declared here)");
-}
-
 TEST_F(ResolverBuiltinValidationTest, BuiltinRedeclaredAsGlobalVarUsedAsVariable) {
     auto* mix =
         GlobalVar(Source{{12, 34}}, "mix", ty.i32(), Expr(1_i), type::AddressSpace::kPrivate);
@@ -180,15 +141,6 @@
     EXPECT_EQ(sem->Variable(), Sem().Get(mix));
 }
 
-TEST_F(ResolverBuiltinValidationTest, BuiltinRedeclaredAsGlobalVarUsedAsType) {
-    GlobalVar(Source{{12, 34}}, "mix", ty.i32(), Expr(1_i), type::AddressSpace::kPrivate);
-    WrapInFunction(Call(ty(Source{{56, 78}}, "mix")));
-
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(56:78 error: cannot use variable 'mix' as type
-12:34 note: 'mix' declared here)");
-}
-
 TEST_F(ResolverBuiltinValidationTest, BuiltinRedeclaredAsAliasUsedAsFunction) {
     Alias(Source{{12, 34}}, "mix", ty.i32());
     WrapInFunction(Call(Source{{56, 78}}, "mix", 1_f, 2_f, 3_f));
@@ -205,14 +157,6 @@
 )");
 }
 
-TEST_F(ResolverBuiltinValidationTest, BuiltinRedeclaredAsAliasUsedAsVariable) {
-    Alias(Source{{12, 34}}, "mix", ty.i32());
-    WrapInFunction(Decl(Var("v", Expr(Source{{56, 78}}, "mix"))));
-
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(56:78 error: missing '(' for type initializer or cast)");
-}
-
 TEST_F(ResolverBuiltinValidationTest, BuiltinRedeclaredAsAliasUsedAsType) {
     auto* mix = Alias(Source{{12, 34}}, "mix", ty.i32());
     auto* use = Call(ty("mix"));
@@ -235,16 +179,6 @@
               R"(12:34 error: struct initializer has too many inputs: expected 1, found 3)");
 }
 
-TEST_F(ResolverBuiltinValidationTest, BuiltinRedeclaredAsStructUsedAsVariable) {
-    Structure("mix", utils::Vector{
-                         Member("m", ty.i32()),
-                     });
-    WrapInFunction(Decl(Var("v", Expr(Source{{12, 34}}, "mix"))));
-
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: missing '(' for type initializer or cast)");
-}
-
 TEST_F(ResolverBuiltinValidationTest, BuiltinRedeclaredAsStructUsedAsType) {
     auto* mix = Structure("mix", utils::Vector{
                                      Member("m", ty.i32()),
diff --git a/src/tint/resolver/call_validation_test.cc b/src/tint/resolver/call_validation_test.cc
index e811377..07e2271 100644
--- a/src/tint/resolver/call_validation_test.cc
+++ b/src/tint/resolver/call_validation_test.cc
@@ -457,39 +457,5 @@
     EXPECT_TRUE(r()->Resolve());
 }
 
-TEST_F(ResolverCallValidationTest, CallVariable) {
-    // var v : i32;
-    // fn f() {
-    //   v();
-    // }
-    GlobalVar("v", ty.i32(), type::AddressSpace::kPrivate);
-    Func("f", utils::Empty, ty.void_(),
-         utils::Vector{
-             CallStmt(Call(Source{{12, 34}}, "v")),
-         });
-
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(error: cannot call variable 'v'
-note: 'v' declared here)");
-}
-
-TEST_F(ResolverCallValidationTest, CallVariableShadowsFunction) {
-    // fn x() {}
-    // fn f() {
-    //   var x : i32;
-    //   x();
-    // }
-    Func("x", utils::Empty, ty.void_(), utils::Empty);
-    Func("f", utils::Empty, ty.void_(),
-         utils::Vector{
-             Decl(Var(Source{{56, 78}}, "x", ty.i32())),
-             CallStmt(Call(Source{{12, 34}}, "x")),
-         });
-
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(error: cannot call variable 'x'
-56:78 note: 'x' declared here)");
-}
-
 }  // namespace
 }  // namespace tint::resolver
diff --git a/src/tint/resolver/dependency_graph.cc b/src/tint/resolver/dependency_graph.cc
index 1c207f6..b77c812 100644
--- a/src/tint/resolver/dependency_graph.cc
+++ b/src/tint/resolver/dependency_graph.cc
@@ -28,6 +28,7 @@
 #include "src/tint/ast/break_statement.h"
 #include "src/tint/ast/call_statement.h"
 #include "src/tint/ast/compound_assignment_statement.h"
+#include "src/tint/ast/const.h"
 #include "src/tint/ast/continue_statement.h"
 #include "src/tint/ast/depth_multisampled_texture.h"
 #include "src/tint/ast/depth_texture.h"
@@ -49,6 +50,7 @@
 #include "src/tint/ast/loop_statement.h"
 #include "src/tint/ast/matrix.h"
 #include "src/tint/ast/multisampled_texture.h"
+#include "src/tint/ast/override.h"
 #include "src/tint/ast/pointer.h"
 #include "src/tint/ast/return_statement.h"
 #include "src/tint/ast/sampled_texture.h"
@@ -65,6 +67,7 @@
 #include "src/tint/ast/traverse_expressions.h"
 #include "src/tint/ast/type_name.h"
 #include "src/tint/ast/u32.h"
+#include "src/tint/ast/var.h"
 #include "src/tint/ast/variable_decl_statement.h"
 #include "src/tint/ast/vector.h"
 #include "src/tint/ast/while_statement.h"
@@ -822,20 +825,42 @@
     return da.Run(module);
 }
 
-std::ostream& operator<<(std::ostream& out, const ResolvedIdentifier& ri) {
-    if (!ri) {
-        return out << "<unresolved symbol>";
+std::string ResolvedIdentifier::String(const SymbolTable& symbols, diag::List& diagnostics) const {
+    if (!Resolved()) {
+        return "<unresolved symbol>";
     }
-    if (auto* node = ri.Node()) {
-        return out << "'" << node->TypeInfo().name << "' at " << node->source;
+    if (auto* node = Node()) {
+        return Switch(
+            node,
+            [&](const ast::TypeDecl* n) {  //
+                return "type '" + symbols.NameFor(n->name->symbol) + "'";
+            },
+            [&](const ast::Var* n) {  //
+                return "var '" + symbols.NameFor(n->name->symbol) + "'";
+            },
+            [&](const ast::Const* n) {  //
+                return "const '" + symbols.NameFor(n->name->symbol) + "'";
+            },
+            [&](const ast::Override* n) {  //
+                return "override '" + symbols.NameFor(n->name->symbol) + "'";
+            },
+            [&](const ast::Function* n) {  //
+                return "function '" + symbols.NameFor(n->name->symbol) + "'";
+            },
+            [&](Default) {
+                TINT_UNREACHABLE(Resolver, diagnostics)
+                    << "unhandled ast::Node: " << node->TypeInfo().name;
+                return "<unknown>";
+            });
     }
-    if (auto builtin_fn = ri.BuiltinFunction(); builtin_fn != sem::BuiltinType::kNone) {
-        return out << "builtin function '" << builtin_fn << "'";
+    if (auto builtin_fn = BuiltinFunction(); builtin_fn != sem::BuiltinType::kNone) {
+        return "builtin function '" + utils::ToString(builtin_fn) + "'";
     }
-    if (auto builtin_ty = ri.BuiltinType(); builtin_ty != type::Builtin::kUndefined) {
-        return out << "builtin function '" << builtin_ty << "'";
+    if (auto builtin_ty = BuiltinType(); builtin_ty != type::Builtin::kUndefined) {
+        return "builtin type '" + utils::ToString(builtin_ty) + "'";
     }
-    return out << "<unhandled ResolvedIdentifier value>";
+    TINT_UNREACHABLE(Resolver, diagnostics) << "unhandled ResolvedIdentifier";
+    return "<unknown>";
 }
 
 }  // namespace tint::resolver
diff --git a/src/tint/resolver/dependency_graph.h b/src/tint/resolver/dependency_graph.h
index 5bbc7a4..dc26289 100644
--- a/src/tint/resolver/dependency_graph.h
+++ b/src/tint/resolver/dependency_graph.h
@@ -15,6 +15,7 @@
 #ifndef SRC_TINT_RESOLVER_DEPENDENCY_GRAPH_H_
 #define SRC_TINT_RESOLVER_DEPENDENCY_GRAPH_H_
 
+#include <string>
 #include <vector>
 
 #include "src/tint/ast/module.h"
@@ -42,7 +43,7 @@
     ResolvedIdentifier(T value) : value_(value) {}  // NOLINT(runtime/explicit)
 
     /// @return true if the ResolvedIdentifier holds a value (successfully resolved)
-    operator bool() const { return !std::holds_alternative<std::monostate>(value_); }
+    bool Resolved() const { return !std::holds_alternative<std::monostate>(value_); }
 
     /// @return the node pointer if the ResolvedIdentifier holds an AST node, otherwise nullptr
     const ast::Node* Node() const {
@@ -87,15 +88,15 @@
         return !(*this == other);
     }
 
+    /// @param symbols the program's symbol table
+    /// @param diagnostics diagnostics used to report ICEs
+    /// @return a description of the resolved symbol
+    std::string String(const SymbolTable& symbols, diag::List& diagnostics) const;
+
   private:
     std::variant<std::monostate, const ast::Node*, sem::BuiltinType, type::Builtin> value_;
 };
 
-/// @param out the std::ostream to write to
-/// @param ri the ResolvedIdentifier
-/// @returns `out` so calls can be chained
-std::ostream& operator<<(std::ostream& out, const ResolvedIdentifier& ri);
-
 /// DependencyGraph holds information about module-scope declaration dependency
 /// analysis and symbol resolutions.
 struct DependencyGraph {
diff --git a/src/tint/resolver/dependency_graph_test.cc b/src/tint/resolver/dependency_graph_test.cc
index e6c460e..57e84f1 100644
--- a/src/tint/resolver/dependency_graph_test.cc
+++ b/src/tint/resolver/dependency_graph_test.cc
@@ -1174,7 +1174,7 @@
 
     auto resolved = Build().resolved_identifiers.Get(ident);
     ASSERT_TRUE(resolved);
-    EXPECT_EQ(resolved->BuiltinFunction(), builtin) << *resolved;
+    EXPECT_EQ(resolved->BuiltinFunction(), builtin) << resolved->String(Symbols(), Diagnostics());
 }
 
 TEST_P(ResolverDependencyGraphResolveToBuiltinFunc, ShadowedByGlobalVar) {
@@ -1189,7 +1189,7 @@
 
     auto resolved = Build().resolved_identifiers.Get(ident);
     ASSERT_TRUE(resolved);
-    EXPECT_EQ(resolved->Node(), decl) << *resolved;
+    EXPECT_EQ(resolved->Node(), decl) << resolved->String(Symbols(), Diagnostics());
 }
 
 TEST_P(ResolverDependencyGraphResolveToBuiltinFunc, ShadowedByStruct) {
@@ -1204,7 +1204,7 @@
 
     auto resolved = Build().resolved_identifiers.Get(ident);
     ASSERT_TRUE(resolved);
-    EXPECT_EQ(resolved->Node(), decl) << *resolved;
+    EXPECT_EQ(resolved->Node(), decl) << resolved->String(Symbols(), Diagnostics());
 }
 
 TEST_P(ResolverDependencyGraphResolveToBuiltinFunc, ShadowedByFunc) {
@@ -1219,7 +1219,7 @@
 
     auto resolved = Build().resolved_identifiers.Get(ident);
     ASSERT_TRUE(resolved);
-    EXPECT_EQ(resolved->Node(), decl) << *resolved;
+    EXPECT_EQ(resolved->Node(), decl) << resolved->String(Symbols(), Diagnostics());
 }
 
 INSTANTIATE_TEST_SUITE_P(Types,
@@ -1258,7 +1258,8 @@
 
     auto resolved = Build().resolved_identifiers.Get(ident);
     ASSERT_TRUE(resolved);
-    EXPECT_EQ(resolved->BuiltinType(), type::ParseBuiltin(name)) << *resolved;
+    EXPECT_EQ(resolved->BuiltinType(), type::ParseBuiltin(name))
+        << resolved->String(Symbols(), Diagnostics());
 }
 
 TEST_P(ResolverDependencyGraphResolveToBuiltinType, ShadowedByGlobalVar) {
@@ -1273,7 +1274,7 @@
 
     auto resolved = Build().resolved_identifiers.Get(ident);
     ASSERT_TRUE(resolved);
-    EXPECT_EQ(resolved->Node(), decl) << *resolved;
+    EXPECT_EQ(resolved->Node(), decl) << resolved->String(Symbols(), Diagnostics());
 }
 
 TEST_P(ResolverDependencyGraphResolveToBuiltinType, ShadowedByStruct) {
@@ -1288,7 +1289,7 @@
 
     auto resolved = Build().resolved_identifiers.Get(ident);
     ASSERT_TRUE(resolved);
-    EXPECT_EQ(resolved->Node(), decl) << *resolved;
+    EXPECT_EQ(resolved->Node(), decl) << resolved->String(Symbols(), Diagnostics());
 }
 
 TEST_P(ResolverDependencyGraphResolveToBuiltinType, ShadowedByFunc) {
@@ -1303,7 +1304,7 @@
 
     auto resolved = Build().resolved_identifiers.Get(ident);
     ASSERT_TRUE(resolved);
-    EXPECT_EQ(resolved->Node(), decl) << *resolved;
+    EXPECT_EQ(resolved->Node(), decl) << resolved->String(Symbols(), Diagnostics());
 }
 
 INSTANTIATE_TEST_SUITE_P(Types,
diff --git a/src/tint/resolver/expression_kind_test.cc b/src/tint/resolver/expression_kind_test.cc
new file mode 100644
index 0000000..e662570
--- /dev/null
+++ b/src/tint/resolver/expression_kind_test.cc
@@ -0,0 +1,227 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/resolver/resolver.h"
+
+#include "gmock/gmock.h"
+#include "src/tint/resolver/resolver_test_helper.h"
+
+using namespace tint::number_suffixes;  // NOLINT
+
+namespace tint::resolver {
+namespace {
+
+enum class Def {
+    kBuiltinFunction,
+    kBuiltinType,
+    kFunction,
+    kStruct,
+    kTypeAlias,
+    kVariable,
+};
+
+std::ostream& operator<<(std::ostream& out, Def def) {
+    switch (def) {
+        case Def::kBuiltinFunction:
+            return out << "Def::kBuiltinFunction";
+        case Def::kBuiltinType:
+            return out << "Def::kBuiltinType";
+        case Def::kFunction:
+            return out << "Def::kFunction";
+        case Def::kStruct:
+            return out << "Def::kStruct";
+        case Def::kTypeAlias:
+            return out << "Def::kTypeAlias";
+        case Def::kVariable:
+            return out << "Def::kVariable";
+    }
+    return out << "<unknown>";
+}
+
+enum class Use {
+    kCallExpr,
+    kCallStmt,
+    kFunctionReturnType,
+    kMemberType,
+    kValueExpression,
+    kVariableType,
+};
+
+std::ostream& operator<<(std::ostream& out, Use use) {
+    switch (use) {
+        case Use::kCallExpr:
+            return out << "Use::kCallExpr";
+        case Use::kCallStmt:
+            return out << "Use::kCallStmt";
+        case Use::kFunctionReturnType:
+            return out << "Use::kFunctionReturnType";
+        case Use::kMemberType:
+            return out << "Use::kMemberType";
+        case Use::kValueExpression:
+            return out << "Use::kValueExpression";
+        case Use::kVariableType:
+            return out << "Use::kVariableType";
+    }
+    return out << "<unknown>";
+}
+
+struct Case {
+    Def def;
+    Use use;
+    const char* error;
+};
+
+std::ostream& operator<<(std::ostream& out, Case c) {
+    return out << "{" << c.def << ", " << c.use << "}";
+}
+
+static const char* kPass = "<pass>";
+
+static const Source kDefSource{Source::Range{{1, 2}, {3, 4}}};
+static const Source kUseSource{Source::Range{{5, 6}, {7, 8}}};
+
+using ResolverExpressionKindTest = ResolverTestWithParam<Case>;
+
+TEST_P(ResolverExpressionKindTest, Test) {
+    Symbol sym;
+    switch (GetParam().def) {
+        case Def::kBuiltinFunction:
+            sym = Sym("workgroupBarrier");
+            break;
+        case Def::kBuiltinType:
+            sym = Sym("vec4f");
+            break;
+        case Def::kFunction:
+            Func(kDefSource, "FUNCTION", utils::Empty, ty.i32(), Return(1_i));
+            sym = Sym("FUNCTION");
+            break;
+        case Def::kStruct:
+            Structure(kDefSource, "STRUCT", utils::Vector{Member("m", ty.i32())});
+            sym = Sym("STRUCT");
+            break;
+        case Def::kTypeAlias:
+            Alias(kDefSource, "ALIAS", ty.i32());
+            sym = Sym("ALIAS");
+            break;
+        case Def::kVariable:
+            GlobalConst(kDefSource, "VARIABLE", Expr(1_i));
+            sym = Sym("VARIABLE");
+            break;
+    }
+
+    switch (GetParam().use) {
+        case Use::kCallExpr:
+            Func("f", utils::Empty, ty.void_(), Decl(Var("v", Call(Ident(kUseSource, sym)))));
+            break;
+        case Use::kCallStmt:
+            Func("f", utils::Empty, ty.void_(), CallStmt(Call(Ident(kUseSource, sym))));
+            break;
+        case Use::kFunctionReturnType:
+            Func("f", utils::Empty, ty(kUseSource, sym), Return(Call(sym)));
+            break;
+        case Use::kMemberType:
+            Structure("s", utils::Vector{Member("m", ty(kUseSource, sym))});
+            break;
+        case Use::kValueExpression:
+            GlobalVar("v", type::AddressSpace::kPrivate, Expr(kUseSource, sym));
+            break;
+        case Use::kVariableType:
+            GlobalVar("v", type::AddressSpace::kPrivate, ty(kUseSource, sym));
+            break;
+    }
+
+    if (GetParam().error == kPass) {
+        EXPECT_TRUE(r()->Resolve());
+        EXPECT_EQ(r()->error(), "");
+    } else {
+        EXPECT_FALSE(r()->Resolve());
+        EXPECT_EQ(r()->error(), GetParam().error);
+    }
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    ,
+    ResolverExpressionKindTest,
+    testing::ValuesIn(std::vector<Case>{
+        {Def::kBuiltinFunction, Use::kCallStmt, kPass},
+        {Def::kBuiltinFunction, Use::kFunctionReturnType,
+         R"(5:6 error: cannot use builtin function 'workgroupBarrier' as type)"},
+        {Def::kBuiltinFunction, Use::kMemberType,
+         R"(5:6 error: cannot use builtin function 'workgroupBarrier' as type)"},
+        {Def::kBuiltinFunction, Use::kValueExpression,
+         R"(7:8 error: missing '(' for builtin function call)"},
+        {Def::kBuiltinFunction, Use::kVariableType,
+         R"(5:6 error: cannot use builtin function 'workgroupBarrier' as type)"},
+
+        {Def::kBuiltinType, Use::kCallExpr, kPass},
+        {Def::kBuiltinType, Use::kCallStmt, kPass},
+        {Def::kBuiltinType, Use::kFunctionReturnType, kPass},
+        {Def::kBuiltinType, Use::kMemberType, kPass},
+        {Def::kBuiltinType, Use::kValueExpression,
+         R"(5:6 error: cannot use type 'vec4<f32>' as value
+7:8 note: are you missing '()' for type initializer?)"},
+        {Def::kBuiltinType, Use::kVariableType, kPass},
+
+        {Def::kFunction, Use::kCallExpr, kPass},
+        {Def::kFunction, Use::kCallStmt, kPass},
+        {Def::kFunction, Use::kFunctionReturnType,
+         R"(5:6 error: cannot use function 'FUNCTION' as type
+1:2 note: 'FUNCTION' declared here)"},
+        {Def::kFunction, Use::kMemberType,
+         R"(5:6 error: cannot use function 'FUNCTION' as type
+1:2 note: 'FUNCTION' declared here)"},
+        {Def::kFunction, Use::kValueExpression, R"(7:8 error: missing '(' for function call)"},
+        {Def::kFunction, Use::kVariableType,
+         R"(5:6 error: cannot use function 'FUNCTION' as type
+1:2 note: 'FUNCTION' declared here)"},
+
+        {Def::kStruct, Use::kCallExpr, kPass},
+        {Def::kStruct, Use::kCallStmt, kPass},
+        {Def::kStruct, Use::kFunctionReturnType, kPass},
+        {Def::kStruct, Use::kMemberType, kPass},
+        {Def::kStruct, Use::kValueExpression,
+         R"(5:6 error: cannot use type 'STRUCT' as value
+7:8 note: are you missing '()' for type initializer?
+1:2 note: 'STRUCT' declared here)"},
+        {Def::kStruct, Use::kVariableType, kPass},
+
+        {Def::kTypeAlias, Use::kCallExpr, kPass},
+        {Def::kTypeAlias, Use::kCallStmt, kPass},
+        {Def::kTypeAlias, Use::kFunctionReturnType, kPass},
+        {Def::kTypeAlias, Use::kMemberType, kPass},
+        {Def::kTypeAlias, Use::kValueExpression,
+         R"(5:6 error: cannot use type 'i32' as value
+7:8 note: are you missing '()' for type initializer?)"},
+        {Def::kTypeAlias, Use::kVariableType, kPass},
+
+        {Def::kVariable, Use::kCallStmt,
+         R"(5:6 error: cannot use const 'VARIABLE' as call target
+1:2 note: 'VARIABLE' declared here)"},
+        {Def::kVariable, Use::kCallExpr,
+         R"(5:6 error: cannot use const 'VARIABLE' as call target
+1:2 note: 'VARIABLE' declared here)"},
+        {Def::kVariable, Use::kFunctionReturnType,
+         R"(5:6 error: cannot use const 'VARIABLE' as type
+1:2 note: 'VARIABLE' declared here)"},
+        {Def::kVariable, Use::kMemberType,
+         R"(5:6 error: cannot use const 'VARIABLE' as type
+1:2 note: 'VARIABLE' declared here)"},
+        {Def::kVariable, Use::kValueExpression, kPass},
+        {Def::kVariable, Use::kVariableType,
+         R"(5:6 error: cannot use const 'VARIABLE' as type
+1:2 note: 'VARIABLE' declared here)"},
+    }));
+
+}  // namespace
+}  // namespace tint::resolver
diff --git a/src/tint/resolver/resolver.cc b/src/tint/resolver/resolver.cc
index 01bad34..47338a3 100644
--- a/src/tint/resolver/resolver.cc
+++ b/src/tint/resolver/resolver.cc
@@ -67,6 +67,7 @@
 #include "src/tint/sem/struct.h"
 #include "src/tint/sem/switch_statement.h"
 #include "src/tint/sem/type_conversion.h"
+#include "src/tint/sem/type_expression.h"
 #include "src/tint/sem/type_initializer.h"
 #include "src/tint/sem/variable.h"
 #include "src/tint/sem/while_statement.h"
@@ -115,7 +116,7 @@
 Resolver::~Resolver() = default;
 
 bool Resolver::Resolve() {
-    if (builder_->Diagnostics().contains_errors()) {
+    if (diagnostics_.contains_errors()) {
         return false;
     }
 
@@ -124,7 +125,7 @@
     // Pre-allocate the marked bitset with the total number of AST nodes.
     marked_.Resize(builder_->ASTNodes().Count());
 
-    if (!DependencyGraph::Build(builder_->AST(), builder_->Symbols(), builder_->Diagnostics(),
+    if (!DependencyGraph::Build(builder_->AST(), builder_->Symbols(), diagnostics_,
                                 dependencies_)) {
         return false;
     }
@@ -329,49 +330,30 @@
             Mark(t->name);
 
             if (t->name->Is<ast::TemplatedIdentifier>()) {
-                TINT_UNREACHABLE(Resolver, builder_->Diagnostics()) << "TODO(crbug.com/tint/1810)";
+                TINT_UNREACHABLE(Resolver, diagnostics_) << "TODO(crbug.com/tint/1810)";
             }
 
             auto resolved = dependencies_.resolved_identifiers.Get(t->name);
             if (!resolved) {
-                TINT_ICE(Resolver, builder_->Diagnostics()) << "identifier was not resolved";
+                TINT_ICE(Resolver, diagnostics_)
+                    << "identifier '" << builder_->Symbols().NameFor(t->name->symbol)
+                    << "' was not resolved";
                 return nullptr;
             }
 
             if (auto* ast_node = resolved->Node()) {
-                auto* resolved_node = sem_.Get(ast_node);
-                return Switch(
-                    resolved_node,  //
-                    [&](type::Type* type) { return type; },
-                    [&](sem::Variable* variable) {
-                        auto name =
-                            builder_->Symbols().NameFor(variable->Declaration()->name->symbol);
-                        AddError("cannot use variable '" + name + "' as type", ty->source);
-                        AddNote("'" + name + "' declared here", variable->Declaration()->source);
-                        return nullptr;
-                    },
-                    [&](sem::Function* func) {
-                        auto name = builder_->Symbols().NameFor(func->Declaration()->name->symbol);
-                        AddError("cannot use function '" + name + "' as type", ty->source);
-                        AddNote("'" + name + "' declared here", func->Declaration()->source);
-                        return nullptr;
-                    });
+                auto* type = sem_.Get<type::Type>(ast_node);
+                if (TINT_UNLIKELY(!type)) {
+                    ErrorMismatchedResolvedIdentifier(t->source, *resolved, "type");
+                    return nullptr;
+                }
+                return type;
+            }
+            if (auto b = resolved->BuiltinType(); b != type::Builtin::kUndefined) {
+                return BuiltinType(b, t->name);
             }
 
-            if (auto builtin_ty = resolved->BuiltinType();
-                builtin_ty != type::Builtin::kUndefined) {
-                return BuiltinType(builtin_ty, t->name);
-            }
-
-            if (auto builtin_fn = resolved->BuiltinFunction();
-                builtin_fn != sem::BuiltinType::kNone) {
-                auto name = builder_->Symbols().NameFor(t->name->symbol);
-                AddError("cannot use builtin '" + name + "' as type", ty->source);
-                return nullptr;
-            }
-
-            TINT_UNREACHABLE(Resolver, diagnostics_)
-                << "unhandled resolved identifier: " << *resolved;
+            ErrorMismatchedResolvedIdentifier(t->source, *resolved, "type");
             return nullptr;
         });
 
@@ -414,7 +396,7 @@
         return nullptr;
     }
 
-    auto* rhs = Load(Materialize(Expression(v->initializer), ty));
+    auto* rhs = Load(Materialize(ValueExpression(v->initializer), ty));
     if (!rhs) {
         return nullptr;
     }
@@ -473,7 +455,7 @@
 
         ExprEvalStageConstraint constraint{sem::EvaluationStage::kOverride, "override initializer"};
         TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
-        rhs = Materialize(Expression(v->initializer), ty);
+        rhs = Materialize(ValueExpression(v->initializer), ty);
         if (!rhs) {
             return nullptr;
         }
@@ -507,7 +489,7 @@
         ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant, "@id"};
         TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
 
-        auto* materialized = Materialize(Expression(id_attr->expr));
+        auto* materialized = Materialize(ValueExpression(id_attr->expr));
         if (!materialized) {
             return nullptr;
         }
@@ -560,7 +542,7 @@
     {
         ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant, "const initializer"};
         TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
-        rhs = Expression(c->initializer);
+        rhs = ValueExpression(c->initializer);
         if (!rhs) {
             return nullptr;
         }
@@ -625,7 +607,7 @@
         };
         TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
 
-        rhs = Load(Materialize(Expression(var->initializer), storage_ty));
+        rhs = Load(Materialize(ValueExpression(var->initializer), storage_ty));
         if (!rhs) {
             return nullptr;
         }
@@ -689,7 +671,7 @@
                 TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
 
                 auto* attr = ast::GetAttribute<ast::BindingAttribute>(var->attributes);
-                auto* materialized = Materialize(Expression(attr->expr));
+                auto* materialized = Materialize(ValueExpression(attr->expr));
                 if (!materialized) {
                     return nullptr;
                 }
@@ -713,7 +695,7 @@
                 TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
 
                 auto* attr = ast::GetAttribute<ast::GroupAttribute>(var->attributes);
-                auto* materialized = Materialize(Expression(attr->expr));
+                auto* materialized = Materialize(ValueExpression(attr->expr));
                 if (!materialized) {
                     return nullptr;
                 }
@@ -799,7 +781,7 @@
             TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
 
             auto* attr = ast::GetAttribute<ast::BindingAttribute>(param->attributes);
-            auto* materialized = Materialize(Expression(attr->expr));
+            auto* materialized = Materialize(ValueExpression(attr->expr));
             if (!materialized) {
                 return nullptr;
             }
@@ -810,7 +792,7 @@
             TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
 
             auto* attr = ast::GetAttribute<ast::GroupAttribute>(param->attributes);
-            auto* materialized = Materialize(Expression(attr->expr));
+            auto* materialized = Materialize(ValueExpression(attr->expr));
             if (!materialized) {
                 return nullptr;
             }
@@ -838,7 +820,7 @@
     ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant, "@location value"};
     TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
 
-    auto* materialized = Materialize(Expression(attr->expr));
+    auto* materialized = Materialize(ValueExpression(attr->expr));
     if (!materialized) {
         return utils::Failure;
     }
@@ -924,7 +906,7 @@
     for (auto it : dependencies_.shadows) {
         CastableBase* b = sem_.Get(it.value);
         if (TINT_UNLIKELY(!b)) {
-            TINT_ICE(Resolver, builder_->Diagnostics())
+            TINT_ICE(Resolver, diagnostics_)
                 << "AST node '" << it.value->TypeInfo().name << "' had no semantic info\n"
                 << "At: " << it.value->source << "\n"
                 << "Pointer: " << it.value;
@@ -978,7 +960,7 @@
 sem::Statement* Resolver::ConstAssert(const ast::ConstAssert* assertion) {
     ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant, "const assertion"};
     TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
-    auto* expr = Expression(assertion->condition);
+    auto* expr = ValueExpression(assertion->condition);
     if (!expr) {
         return nullptr;
     }
@@ -1196,7 +1178,7 @@
         if (!value) {
             break;
         }
-        const auto* expr = Expression(value);
+        const auto* expr = ValueExpression(value);
         if (!expr) {
             return false;
         }
@@ -1368,7 +1350,7 @@
     auto* sem =
         builder_->create<sem::IfStatement>(stmt, current_compound_statement_, current_function_);
     return StatementScope(stmt, sem, [&] {
-        auto* cond = Load(Expression(stmt->condition));
+        auto* cond = Load(ValueExpression(stmt->condition));
         if (!cond) {
             return false;
         }
@@ -1463,7 +1445,7 @@
         }
 
         if (auto* cond_expr = stmt->condition) {
-            auto* cond = Load(Expression(cond_expr));
+            auto* cond = Load(ValueExpression(cond_expr));
             if (!cond) {
                 return false;
             }
@@ -1506,7 +1488,7 @@
     return StatementScope(stmt, sem, [&] {
         auto& behaviors = sem->Behaviors();
 
-        auto* cond = Load(Expression(stmt->condition));
+        auto* cond = Load(ValueExpression(stmt->condition));
         if (!cond) {
             return false;
         }
@@ -1533,7 +1515,7 @@
     });
 }
 
-sem::ValueExpression* Resolver::Expression(const ast::Expression* root) {
+sem::Expression* Resolver::Expression(const ast::Expression* root) {
     utils::Vector<const ast::Expression*, 64> sorted;
     constexpr size_t kMaxExpressionDepth = 512U;
     bool failed = false;
@@ -1567,30 +1549,15 @@
 
     for (auto* expr : utils::Reverse(sorted)) {
         auto* sem_expr = Switch(
-            expr,
-            [&](const ast::IndexAccessorExpression* array) -> sem::ValueExpression* {
-                return IndexAccessor(array);
-            },
-            [&](const ast::BinaryExpression* bin_op) -> sem::ValueExpression* {
-                return Binary(bin_op);
-            },
-            [&](const ast::BitcastExpression* bitcast) -> sem::ValueExpression* {
-                return Bitcast(bitcast);
-            },
-            [&](const ast::CallExpression* call) -> sem::ValueExpression* { return Call(call); },
-            [&](const ast::IdentifierExpression* ident) -> sem::ValueExpression* {
-                return Identifier(ident);
-            },
-            [&](const ast::LiteralExpression* literal) -> sem::ValueExpression* {
-                return Literal(literal);
-            },
-            [&](const ast::MemberAccessorExpression* member) -> sem::ValueExpression* {
-                return MemberAccessor(member);
-            },
-            [&](const ast::UnaryOpExpression* unary) -> sem::ValueExpression* {
-                return UnaryOp(unary);
-            },
-            [&](const ast::PhonyExpression*) -> sem::ValueExpression* {
+            expr, [&](const ast::IndexAccessorExpression* array) { return IndexAccessor(array); },
+            [&](const ast::BinaryExpression* bin_op) { return Binary(bin_op); },
+            [&](const ast::BitcastExpression* bitcast) { return Bitcast(bitcast); },
+            [&](const ast::CallExpression* call) { return Call(call); },
+            [&](const ast::IdentifierExpression* ident) { return Identifier(ident); },
+            [&](const ast::LiteralExpression* literal) { return Literal(literal); },
+            [&](const ast::MemberAccessorExpression* member) { return MemberAccessor(member); },
+            [&](const ast::UnaryOpExpression* unary) { return UnaryOp(unary); },
+            [&](const ast::PhonyExpression*) {
                 return builder_->create<sem::ValueExpression>(expr, builder_->create<type::Void>(),
                                                               sem::EvaluationStage::kRuntime,
                                                               current_statement_,
@@ -1606,10 +1573,14 @@
             return nullptr;
         }
 
-        if (auto* constraint = expr_eval_stage_constraint_.constraint) {
-            if (!validator_.EvaluationStage(sem_expr, expr_eval_stage_constraint_.stage,
-                                            constraint)) {
-                return nullptr;
+        auto* val = sem_expr->As<sem::ValueExpression>();
+
+        if (val) {
+            if (auto* constraint = expr_eval_stage_constraint_.constraint) {
+                if (!validator_.EvaluationStage(val, expr_eval_stage_constraint_.stage,
+                                                constraint)) {
+                    return nullptr;
+                }
             }
         }
 
@@ -1618,11 +1589,11 @@
             return sem_expr;
         }
 
-        // If we just processed the lhs of a constexpr logical binary expression, mark the rhs
-        // for short-circuiting.
-        if (sem_expr->ConstantValue()) {
+        // If we just processed the lhs of a constexpr logical binary expression, mark the rhs for
+        // short-circuiting.
+        if (val && val->ConstantValue()) {
             if (auto binary = logical_binary_lhs_to_parent_.Find(expr)) {
-                const bool lhs_is_true = sem_expr->ConstantValue()->ValueAs<bool>();
+                const bool lhs_is_true = val->ConstantValue()->ValueAs<bool>();
                 if (((*binary)->IsLogicalAnd() && !lhs_is_true) ||
                     ((*binary)->IsLogicalOr() && lhs_is_true)) {
                     // Mark entire expression tree to not const-evaluate
@@ -1643,6 +1614,10 @@
     return nullptr;
 }
 
+sem::ValueExpression* Resolver::ValueExpression(const ast::Expression* expr) {
+    return sem_.AsValue(Expression(expr));
+}
+
 void Resolver::RegisterStore(const sem::ValueExpression* expr) {
     auto& info = alias_analysis_infos_[current_function_];
     Switch(
@@ -1811,7 +1786,7 @@
 
 const sem::ValueExpression* Resolver::Load(const sem::ValueExpression* expr) {
     if (!expr) {
-        // Allow for Load(Expression(blah)), where failures pass through Load()
+        // Allow for Load(ValueExpression(blah)), where failures pass through Load()
         return nullptr;
     }
 
@@ -1839,7 +1814,7 @@
 const sem::ValueExpression* Resolver::Materialize(const sem::ValueExpression* expr,
                                                   const type::Type* target_type /* = nullptr */) {
     if (!expr) {
-        // Allow for Materialize(Expression(blah)), where failures pass through Materialize()
+        // Allow for Materialize(ValueExpression(blah)), where failures pass through Materialize()
         return nullptr;
     }
 
@@ -1859,7 +1834,7 @@
     if (!skip_const_eval_.Contains(decl)) {
         auto expr_val = expr->ConstantValue();
         if (TINT_UNLIKELY(!expr_val)) {
-            TINT_ICE(Resolver, builder_->Diagnostics())
+            TINT_ICE(Resolver, diagnostics_)
                 << decl->source << "Materialize(" << decl->TypeInfo().name
                 << ") called on expression with no constant value";
             return nullptr;
@@ -1872,7 +1847,7 @@
         }
         materialized_val = val.Get();
         if (TINT_UNLIKELY(!materialized_val)) {
-            TINT_ICE(Resolver, builder_->Diagnostics())
+            TINT_ICE(Resolver, diagnostics_)
                 << decl->source << "ConvertValue(" << builder_->FriendlyName(expr_val->Type())
                 << " -> " << builder_->FriendlyName(concrete_ty) << ") returned invalid value";
             return nullptr;
@@ -2328,14 +2303,15 @@
 
         auto resolved = dependencies_.resolved_identifiers.Get(ident);
         if (!resolved) {
-            TINT_ICE(Resolver, builder_->Diagnostics()) << "identifier was not resolved";
+            TINT_ICE(Resolver, diagnostics_)
+                << "identifier '" << builder_->Symbols().NameFor(ident->symbol)
+                << "' was not resolved";
             return nullptr;
         }
 
         if (auto* ast_node = resolved->Node()) {
-            auto* resolved_node = sem_.Get(ast_node);
             return Switch(
-                resolved_node,  //
+                sem_.Get(ast_node),  //
                 [&](const type::Type* ty) {
                     // A type initializer or conversions.
                     // Note: Unlike the code path where we're resolving the call target from an
@@ -2344,24 +2320,22 @@
                     return ty_init_or_conv(ty);
                 },
                 [&](sem::Function* func) { return FunctionCall(expr, func, args, arg_behaviors); },
-                [&](sem::Variable* var) {
-                    auto name = builder_->Symbols().NameFor(var->Declaration()->name->symbol);
-                    AddError("cannot call variable '" + name + "'", ident->source);
-                    AddNote("'" + name + "' declared here", var->Declaration()->source);
+                [&](Default) {
+                    ErrorMismatchedResolvedIdentifier(ident->source, *resolved, "call target");
                     return nullptr;
                 });
         }
 
-        if (auto builtin_fn = resolved->BuiltinFunction(); builtin_fn != sem::BuiltinType::kNone) {
-            return BuiltinCall(expr, builtin_fn, args);
+        if (auto f = resolved->BuiltinFunction(); f != sem::BuiltinType::kNone) {
+            return BuiltinCall(expr, f, args);
         }
 
-        if (auto builtin_ty = resolved->BuiltinType(); builtin_ty != type::Builtin::kUndefined) {
-            auto* ty = BuiltinType(builtin_ty, expr->target.name);
+        if (auto b = resolved->BuiltinType(); b != type::Builtin::kUndefined) {
+            auto* ty = BuiltinType(b, expr->target.name);
             return ty ? ty_init_or_conv(ty) : nullptr;
         }
 
-        TINT_UNREACHABLE(Resolver, diagnostics_) << "unhandled resolved identifier: " << *resolved;
+        ErrorMismatchedResolvedIdentifier(ident->source, *resolved, "call target");
         return nullptr;
     }
 
@@ -2669,7 +2643,7 @@
                 case ast::IntLiteralExpression::Suffix::kU:
                     return builder_->create<type::U32>();
             }
-            TINT_UNREACHABLE(Resolver, builder_->Diagnostics())
+            TINT_UNREACHABLE(Resolver, diagnostics_)
                 << "Unhandled integer literal suffix: " << i->suffix;
             return nullptr;
         },
@@ -2684,13 +2658,13 @@
                                ? builder_->create<type::F16>()
                                : nullptr;
             }
-            TINT_UNREACHABLE(Resolver, builder_->Diagnostics())
+            TINT_UNREACHABLE(Resolver, diagnostics_)
                 << "Unhandled float literal suffix: " << f->suffix;
             return nullptr;
         },
         [&](const ast::BoolLiteralExpression*) { return builder_->create<type::Bool>(); },
         [&](Default) {
-            TINT_UNREACHABLE(Resolver, builder_->Diagnostics())
+            TINT_UNREACHABLE(Resolver, diagnostics_)
                 << "Unhandled literal type: " << literal->TypeInfo().name;
             return nullptr;
         });
@@ -2716,12 +2690,14 @@
                                                   /* has_side_effects */ false);
 }
 
-sem::ValueExpression* Resolver::Identifier(const ast::IdentifierExpression* expr) {
+sem::Expression* Resolver::Identifier(const ast::IdentifierExpression* expr) {
     Mark(expr->identifier);
 
     auto resolved = dependencies_.resolved_identifiers.Get(expr->identifier);
     if (!resolved) {
-        TINT_ICE(Resolver, builder_->Diagnostics()) << "identifier was not resolved";
+        TINT_ICE(Resolver, diagnostics_)
+            << "identifier '" << builder_->Symbols().NameFor(expr->identifier->symbol)
+            << "' was not resolved";
         return nullptr;
     }
 
@@ -2801,9 +2777,8 @@
                 variable->AddUser(user);
                 return user;
             },
-            [&](const type::Type*) {
-                AddError("missing '(' for type initializer or cast", expr->source.End());
-                return nullptr;
+            [&](const type::Type* ty) {
+                return builder_->create<sem::TypeExpression>(expr, current_statement_, ty);
             },
             [&](const sem::Function*) {
                 AddError("missing '(' for function call", expr->source.End());
@@ -2811,17 +2786,21 @@
             });
     }
 
-    if (resolved->BuiltinType() != type::Builtin::kUndefined) {
-        AddError("missing '(' for type initializer or cast", expr->source.End());
-        return nullptr;
+    if (auto builtin_ty = resolved->BuiltinType(); builtin_ty != type::Builtin::kUndefined) {
+        auto* ty = BuiltinType(builtin_ty, expr->identifier);
+        if (!ty) {
+            return nullptr;
+        }
+        return builder_->create<sem::TypeExpression>(expr, current_statement_, ty);
     }
 
     if (resolved->BuiltinFunction() != sem::BuiltinType::kNone) {
-        AddError("missing '(' for builtin call", expr->source.End());
+        AddError("missing '(' for builtin function call", expr->source.End());
         return nullptr;
     }
 
-    TINT_UNREACHABLE(Resolver, diagnostics_) << "unhandled resolved identifier: " << *resolved;
+    TINT_UNREACHABLE(Resolver, diagnostics_)
+        << "unhandled resolved identifier: " << resolved->String(builder_->Symbols(), diagnostics_);
     return nullptr;
 }
 
@@ -3231,7 +3210,7 @@
 
 const type::ArrayCount* Resolver::ArrayCount(const ast::Expression* count_expr) {
     // Evaluate the constant array count expression.
-    const auto* count_sem = Materialize(Expression(count_expr));
+    const auto* count_sem = Materialize(ValueExpression(count_expr));
     if (!count_sem) {
         return nullptr;
     }
@@ -3413,7 +3392,7 @@
                                                        "@offset value"};
                     TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
 
-                    auto* materialized = Materialize(Expression(o->expr));
+                    auto* materialized = Materialize(ValueExpression(o->expr));
                     if (!materialized) {
                         return false;
                     }
@@ -3435,7 +3414,7 @@
                     ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant, "@align"};
                     TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
 
-                    auto* materialized = Materialize(Expression(a->expr));
+                    auto* materialized = Materialize(ValueExpression(a->expr));
                     if (!materialized) {
                         return false;
                     }
@@ -3464,7 +3443,7 @@
                     ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant, "@size"};
                     TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
 
-                    auto* materialized = Materialize(Expression(s->expr));
+                    auto* materialized = Materialize(ValueExpression(s->expr));
                     if (!materialized) {
                         return false;
                     }
@@ -3590,7 +3569,7 @@
 
         const type::Type* value_ty = nullptr;
         if (auto* value = stmt->value) {
-            const auto* expr = Load(Expression(value));
+            const auto* expr = Load(ValueExpression(value));
             if (!expr) {
                 return false;
             }
@@ -3620,7 +3599,7 @@
     return StatementScope(stmt, sem, [&] {
         auto& behaviors = sem->Behaviors();
 
-        const auto* cond = Load(Expression(stmt->condition));
+        const auto* cond = Load(ValueExpression(stmt->condition));
         if (!cond) {
             return false;
         }
@@ -3637,7 +3616,7 @@
                 if (sel->IsDefault()) {
                     continue;
                 }
-                auto* sem_expr = Expression(sel->expr);
+                auto* sem_expr = ValueExpression(sel->expr);
                 if (!sem_expr) {
                     return false;
                 }
@@ -3710,14 +3689,14 @@
     auto* sem =
         builder_->create<sem::Statement>(stmt, current_compound_statement_, current_function_);
     return StatementScope(stmt, sem, [&] {
-        auto* lhs = Expression(stmt->lhs);
+        auto* lhs = ValueExpression(stmt->lhs);
         if (!lhs) {
             return false;
         }
 
         const bool is_phony_assignment = stmt->lhs->Is<ast::PhonyExpression>();
 
-        const auto* rhs = Expression(stmt->rhs);
+        const auto* rhs = ValueExpression(stmt->rhs);
         if (!rhs) {
             return false;
         }
@@ -3762,7 +3741,7 @@
     auto* sem = builder_->create<sem::BreakIfStatement>(stmt, current_compound_statement_,
                                                         current_function_);
     return StatementScope(stmt, sem, [&] {
-        auto* cond = Load(Expression(stmt->condition));
+        auto* cond = Load(ValueExpression(stmt->condition));
         if (!cond) {
             return false;
         }
@@ -3778,7 +3757,7 @@
     auto* sem =
         builder_->create<sem::Statement>(stmt, current_compound_statement_, current_function_);
     return StatementScope(stmt, sem, [&] {
-        if (auto* expr = Expression(stmt->expr)) {
+        if (auto* expr = ValueExpression(stmt->expr)) {
             sem->Behaviors() = expr->Behaviors();
             return true;
         }
@@ -3791,12 +3770,12 @@
     auto* sem =
         builder_->create<sem::Statement>(stmt, current_compound_statement_, current_function_);
     return StatementScope(stmt, sem, [&] {
-        auto* lhs = Expression(stmt->lhs);
+        auto* lhs = ValueExpression(stmt->lhs);
         if (!lhs) {
             return false;
         }
 
-        auto* rhs = Load(Expression(stmt->rhs));
+        auto* rhs = Load(ValueExpression(stmt->rhs));
         if (!rhs) {
             return false;
         }
@@ -3849,7 +3828,7 @@
     auto* sem =
         builder_->create<sem::Statement>(stmt, current_compound_statement_, current_function_);
     return StatementScope(stmt, sem, [&] {
-        auto* lhs = Expression(stmt->lhs);
+        auto* lhs = ValueExpression(stmt->lhs);
         if (!lhs) {
             return false;
         }
@@ -4004,6 +3983,29 @@
     }
 }
 
+void Resolver::ErrorMismatchedResolvedIdentifier(const Source& source,
+                                                 const ResolvedIdentifier& resolved,
+                                                 std::string_view wanted) {
+    AddError("cannot use " + resolved.String(builder_->Symbols(), diagnostics_) + " as " +
+                 std::string(wanted),
+             source);
+
+    Switch(
+        resolved.Node(),
+        [&](const ast::TypeDecl* n) {
+            AddNote("'" + builder_->Symbols().NameFor(n->name->symbol) + "' declared here",
+                    n->source);
+        },
+        [&](const ast::Variable* n) {
+            AddNote("'" + builder_->Symbols().NameFor(n->name->symbol) + "' declared here",
+                    n->source);
+        },
+        [&](const ast::Function* n) {
+            AddNote("'" + builder_->Symbols().NameFor(n->name->symbol) + "' declared here",
+                    n->source);
+        });
+}
+
 void Resolver::AddError(const std::string& msg, const Source& source) const {
     diagnostics_.add_error(diag::System::Resolver, msg, source);
 }
diff --git a/src/tint/resolver/resolver.h b/src/tint/resolver/resolver.h
index ac6ac5d..0d3c879 100644
--- a/src/tint/resolver/resolver.h
+++ b/src/tint/resolver/resolver.h
@@ -122,11 +122,15 @@
     /// ProgramBuilder.
     void CreateSemanticNodes() const;
 
+    /// @returns the call of Expression() cast to a sem::ValueExpression. If the sem::Expression is
+    /// not a sem::ValueExpression, then an error diagnostic is raised and nullptr is returned.
+    sem::ValueExpression* ValueExpression(const ast::Expression* expr);
+
     /// Expression traverses the graph of expressions starting at `expr`, building a postordered
     /// list (leaf-first) of all the expression nodes. Each of the expressions are then resolved by
     /// dispatching to the appropriate expression handlers below.
     /// @returns the resolved semantic node for the expression `expr`, or nullptr on failure.
-    sem::ValueExpression* Expression(const ast::Expression* expr);
+    sem::Expression* Expression(const ast::Expression* expr);
 
     ////////////////////////////////////////////////////////////////////////////////////////////////
     // Expression resolving methods
@@ -147,7 +151,7 @@
                             sem::Function* target,
                             utils::Vector<const sem::ValueExpression*, N>& args,
                             sem::Behaviors arg_behaviors);
-    sem::ValueExpression* Identifier(const ast::IdentifierExpression*);
+    sem::Expression* Identifier(const ast::IdentifierExpression*);
     template <size_t N>
     sem::Call* BuiltinCall(const ast::CallExpression*,
                            sem::BuiltinType,
@@ -419,6 +423,15 @@
     template <typename NODE>
     void ApplyDiagnosticSeverities(NODE* node);
 
+    /// Raises an error diagnostic that the resolved identifier @p resolved was not of the expected
+    /// kind.
+    /// @param source the source of the error diagnostic
+    /// @param resolved the resolved identifier
+    /// @param wanted the expected kind
+    void ErrorMismatchedResolvedIdentifier(const Source& source,
+                                           const ResolvedIdentifier& resolved,
+                                           std::string_view wanted);
+
     /// Adds the given error message to the diagnostics
     void AddError(const std::string& msg, const Source& source) const;
 
diff --git a/src/tint/resolver/sem_helper.cc b/src/tint/resolver/sem_helper.cc
index 836fb97..2c11659 100644
--- a/src/tint/resolver/sem_helper.cc
+++ b/src/tint/resolver/sem_helper.cc
@@ -14,6 +14,7 @@
 
 #include "src/tint/resolver/sem_helper.h"
 
+#include "src/tint/sem/type_expression.h"
 #include "src/tint/sem/value_expression.h"
 
 namespace tint::resolver {
@@ -35,4 +36,35 @@
     return sem ? const_cast<type::Type*>(sem->Type()) : nullptr;
 }
 
+void SemHelper::ErrorExpectedValueExpr(const sem::Expression* expr) const {
+    Switch(
+        expr,  //
+        [&](const sem::TypeExpression* ty_expr) {
+            auto name = ty_expr->Type()->FriendlyName(builder_->Symbols());
+            AddError("cannot use type '" + name + "' as value", ty_expr->Declaration()->source);
+            if (auto* ident = ty_expr->Declaration()->As<ast::IdentifierExpression>()) {
+                AddNote("are you missing '()' for type initializer?",
+                        Source{{ident->source.range.end}});
+            }
+            if (auto* str = ty_expr->Type()->As<type::Struct>()) {
+                AddNote("'" + name + "' declared here", str->Source());
+            }
+        },
+        [&](Default) {
+            TINT_ICE(Resolver, builder_->Diagnostics())
+                << "unhandled sem::Expression type: " << (expr ? expr->TypeInfo().name : "<null>");
+        });
+}
+
+void SemHelper::AddError(const std::string& msg, const Source& source) const {
+    builder_->Diagnostics().add_error(diag::System::Resolver, msg, source);
+}
+
+void SemHelper::AddWarning(const std::string& msg, const Source& source) const {
+    builder_->Diagnostics().add_warning(diag::System::Resolver, msg, source);
+}
+
+void SemHelper::AddNote(const std::string& msg, const Source& source) const {
+    builder_->Diagnostics().add_note(diag::System::Resolver, msg, source);
+}
 }  // namespace tint::resolver
diff --git a/src/tint/resolver/sem_helper.h b/src/tint/resolver/sem_helper.h
index 2a7241b..9c918cd 100644
--- a/src/tint/resolver/sem_helper.h
+++ b/src/tint/resolver/sem_helper.h
@@ -57,39 +57,58 @@
     /// @returns the sem node for @p ast
     template <typename AST = ast::Node>
     auto* GetVal(const AST* ast) const {
-        if constexpr (traits::IsTypeOrDerived<sem::SemanticNodeTypeFor<AST>,
-                                              sem::ValueExpression>) {
-            return Get(ast);
+        return AsValue(Get(ast));
+    }
+
+    /// @param expr the semantic node
+    /// @returns one of:
+    /// * nullptr if @p expr is nullptr
+    /// * @p expr if the static pointer type already derives from sem::ValueExpression
+    /// * @p expr cast to sem::ValueExpression if the cast is successful
+    /// * nullptr if @p expr is not a sem::ValueExpression. In this case an error diagnostic is
+    ///   raised.
+    template <typename EXPR>
+    auto* AsValue(EXPR* expr) const {
+        if constexpr (traits::IsTypeOrDerived<EXPR, sem::ValueExpression>) {
+            return expr;
         } else {
-            if (auto* sem = Get(ast); TINT_LIKELY(sem)) {
-                auto* val = sem->template As<sem::ValueExpression>();
-                if (TINT_LIKELY(val)) {
+            if (TINT_LIKELY(expr)) {
+                if (auto* val = expr->template As<sem::ValueExpression>(); TINT_LIKELY(val)) {
                     return val;
                 }
-                // TODO(crbug.com/tint/1810): Improve error
-                builder_->Diagnostics().add_error(diag::System::Resolver,
-                                                  "required value expression, got something else",
-                                                  ast->source);
+                ErrorExpectedValueExpr(expr);
             }
             return static_cast<sem::ValueExpression*>(nullptr);
         }
     }
 
-    /// @returns the resolved type of the ast::Expression `expr`
+    /// @returns the resolved type of the ast::Expression @p expr
     /// @param expr the expression
     type::Type* TypeOf(const ast::Expression* expr) const;
 
-    /// @returns the type name of the given semantic type, unwrapping
-    /// references.
+    /// @returns the type name of the given semantic type, unwrapping references.
     /// @param ty the type to look up
     std::string TypeNameOf(const type::Type* ty) const;
 
-    /// @returns the type name of the given semantic type, without unwrapping
-    /// references.
+    /// @returns the type name of the given semantic type, without unwrapping references.
     /// @param ty the type to look up
     std::string RawTypeNameOf(const type::Type* ty) const;
 
+    /// Raises an error diagnostic that the expression @p got was expected to be a
+    /// sem::ValueExpression, but the expression evaluated to something different.
+    /// @param expr the expression
+    void ErrorExpectedValueExpr(const sem::Expression* expr) const;
+
   private:
+    /// Adds the given error message to the diagnostics
+    void AddError(const std::string& msg, const Source& source) const;
+
+    /// Adds the given warning message to the diagnostics
+    void AddWarning(const std::string& msg, const Source& source) const;
+
+    /// Adds the given note message to the diagnostics
+    void AddNote(const std::string& msg, const Source& source) const;
+
     ProgramBuilder* builder_;
 };
 
diff --git a/src/tint/resolver/type_validation_test.cc b/src/tint/resolver/type_validation_test.cc
index b4e83c3..b28cecf 100644
--- a/src/tint/resolver/type_validation_test.cc
+++ b/src/tint/resolver/type_validation_test.cc
@@ -883,38 +883,6 @@
               "12:34 error: ptr<uniform, u32, read> cannot be used as an element type of an array");
 }
 
-TEST_F(ResolverTypeValidationTest, VariableAsType) {
-    // var<private> a : i32;
-    // var<private> b : a;
-    GlobalVar("a", ty.i32(), type::AddressSpace::kPrivate);
-    GlobalVar("b", ty("a"), type::AddressSpace::kPrivate);
-
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              R"(error: cannot use variable 'a' as type
-note: 'a' declared here)");
-}
-
-TEST_F(ResolverTypeValidationTest, FunctionAsType) {
-    // fn f() {}
-    // var<private> v : f;
-    Func("f", utils::Empty, ty.void_(), {});
-    GlobalVar("v", ty("f"), type::AddressSpace::kPrivate);
-
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              R"(error: cannot use function 'f' as type
-note: 'f' declared here)");
-}
-
-TEST_F(ResolverTypeValidationTest, BuiltinAsType) {
-    // var<private> v : max;
-    GlobalVar("v", ty("max"), type::AddressSpace::kPrivate);
-
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "error: cannot use builtin 'max' as type");
-}
-
 namespace GetCanonicalTests {
 struct Params {
     builder::ast_type_func_ptr create_ast_type;
diff --git a/src/tint/resolver/validation_test.cc b/src/tint/resolver/validation_test.cc
index 128feb5..bbfd4aa 100644
--- a/src/tint/resolver/validation_test.cc
+++ b/src/tint/resolver/validation_test.cc
@@ -134,7 +134,7 @@
 
     EXPECT_FALSE(r()->Resolve());
 
-    EXPECT_EQ(r()->error(), "12:34 error: if statement condition must be bool, got f32");
+    EXPECT_EQ(r()->error(), R"(12:34 error: if statement condition must be bool, got f32)");
 }
 
 TEST_F(ResolverValidationTest, Stmt_ElseIf_NonBool) {
@@ -144,7 +144,7 @@
 
     EXPECT_FALSE(r()->Resolve());
 
-    EXPECT_EQ(r()->error(), "12:34 error: if statement condition must be bool, got f32");
+    EXPECT_EQ(r()->error(), R"(12:34 error: if statement condition must be bool, got f32)");
 }
 
 TEST_F(ResolverValidationTest, Expr_ErrUnknownExprType) {
@@ -158,48 +158,6 @@
         "tint::resolver::FakeExpr");
 }
 
-TEST_F(ResolverValidationTest, Expr_DontCall_Function) {
-    Func("func", utils::Empty, ty.void_(), utils::Empty, {});
-    WrapInFunction(Expr(Source{{12, 34}}, "func"));
-
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: missing '(' for function call");
-}
-
-TEST_F(ResolverValidationTest, Expr_DontCall_Builtin) {
-    WrapInFunction(Expr(Source{{12, 34}}, "round"));
-
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: missing '(' for builtin call");
-}
-
-TEST_F(ResolverValidationTest, Expr_DontCall_Type) {
-    Alias("T", ty.u32());
-    WrapInFunction(Expr(Source{{12, 34}}, "T"));
-
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: missing '(' for type initializer or cast");
-}
-
-TEST_F(ResolverValidationTest, Expr_DontCall_BuiltinType) {
-    WrapInFunction(Expr(Source{{12, 34}}, "vec3f"));
-
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: missing '(' for type initializer or cast");
-}
-
-TEST_F(ResolverValidationTest, AssignmentStmt_InvalidLHS_BuiltinFunctionName) {
-    // normalize = 2;
-
-    auto* lhs = Expr(Source{{12, 34}}, "normalize");
-    auto* rhs = Expr(2_i);
-    auto* assign = Assign(lhs, rhs);
-    WrapInFunction(assign);
-
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: missing '(' for builtin call");
-}
-
 TEST_F(ResolverValidationTest, UsingUndefinedVariable_Fail) {
     // b = 2;
 
@@ -209,7 +167,7 @@
     WrapInFunction(assign);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: unknown identifier: 'b'");
+    EXPECT_EQ(r()->error(), R"(12:34 error: unknown identifier: 'b')");
 }
 
 TEST_F(ResolverValidationTest, UsingUndefinedVariableInBlockStatement_Fail) {
@@ -224,7 +182,7 @@
     WrapInFunction(body);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: unknown identifier: 'b'");
+    EXPECT_EQ(r()->error(), R"(12:34 error: unknown identifier: 'b')");
 }
 
 TEST_F(ResolverValidationTest, UsingUndefinedVariableGlobalVariable_Pass) {
@@ -264,7 +222,7 @@
     WrapInFunction(outer_body);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: unknown identifier: 'a'");
+    EXPECT_EQ(r()->error(), R"(12:34 error: unknown identifier: 'a')");
 }
 
 TEST_F(ResolverValidationTest, UsingUndefinedVariableOuterScope_Pass) {
@@ -304,7 +262,7 @@
     WrapInFunction(outer_body);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: unknown identifier: 'a'");
+    EXPECT_EQ(r()->error(), R"(12:34 error: unknown identifier: 'a')");
 }
 
 TEST_F(ResolverValidationTest, AddressSpace_FunctionVariableWorkgroupClass) {
@@ -362,7 +320,7 @@
     WrapInFunction(mem);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "3:5 error: invalid vector swizzle character");
+    EXPECT_EQ(r()->error(), R"(3:5 error: invalid vector swizzle character)");
 }
 
 TEST_F(ResolverValidationTest, Expr_MemberAccessor_VectorSwizzle_MixedChars) {
@@ -383,7 +341,7 @@
     WrapInFunction(mem);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "3:3 error: invalid vector swizzle size");
+    EXPECT_EQ(r()->error(), R"(3:3 error: invalid vector swizzle size)");
 }
 
 TEST_F(ResolverValidationTest, Expr_MemberAccessor_VectorSwizzle_BadIndex) {
@@ -393,7 +351,7 @@
     WrapInFunction(mem);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "3:3 error: invalid vector swizzle member");
+    EXPECT_EQ(r()->error(), R"(3:3 error: invalid vector swizzle member)");
 }
 
 TEST_F(ResolverValidationTest, Expr_MemberAccessor_BadParent) {
@@ -791,7 +749,8 @@
               Continue(Source{{12, 34}}))));
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: continuing blocks must not contain a continue statement");
+    EXPECT_EQ(r()->error(),
+              R"(12:34 error: continuing blocks must not contain a continue statement)");
 }
 
 TEST_F(ResolverTest, Stmt_Loop_ContinueInContinuing_Indirect) {
@@ -938,7 +897,8 @@
                        Block(Break())));
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: continuing blocks must not contain a continue statement");
+    EXPECT_EQ(r()->error(),
+              R"(12:34 error: continuing blocks must not contain a continue statement)");
 }
 
 TEST_F(ResolverTest, Stmt_ForLoop_ContinueInContinuing_Indirect) {
@@ -972,7 +932,7 @@
     WrapInFunction(For(nullptr, Expr(Source{{12, 34}}, 1_f), nullptr, Block()));
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: for-loop condition must be bool, got f32");
+    EXPECT_EQ(r()->error(), R"(12:34 error: for-loop condition must be bool, got f32)");
 }
 
 TEST_F(ResolverTest, Stmt_While_CondIsBoolRef) {
@@ -992,7 +952,7 @@
     WrapInFunction(While(Expr(Source{{12, 34}}, 1_f), Block()));
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: while condition must be bool, got f32");
+    EXPECT_EQ(r()->error(), R"(12:34 error: while condition must be bool, got f32)");
 }
 
 TEST_F(ResolverValidationTest, Stmt_ContinueInLoop) {
@@ -1004,7 +964,7 @@
 TEST_F(ResolverValidationTest, Stmt_ContinueNotInLoop) {
     WrapInFunction(Continue(Source{{12, 34}}));
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: continue statement must be in a loop");
+    EXPECT_EQ(r()->error(), R"(12:34 error: continue statement must be in a loop)");
 }
 
 TEST_F(ResolverValidationTest, Stmt_BreakInLoop) {
@@ -1164,7 +1124,7 @@
 TEST_F(ResolverValidationTest, Stmt_BreakNotInLoopOrSwitch) {
     WrapInFunction(Break(Source{{12, 34}}));
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: break statement must be in a loop or switch case");
+    EXPECT_EQ(r()->error(), R"(12:34 error: break statement must be in a loop or switch case)");
 }
 
 TEST_F(ResolverValidationTest, StructMemberDuplicateName) {
@@ -1205,7 +1165,8 @@
                    });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: @align value must be a positive, power-of-two integer");
+    EXPECT_EQ(r()->error(),
+              R"(12:34 error: @align value must be a positive, power-of-two integer)");
 }
 
 TEST_F(ResolverValidationTest, NonPOTStructMemberAlignAttribute) {
@@ -1214,7 +1175,8 @@
                    });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: @align value must be a positive, power-of-two integer");
+    EXPECT_EQ(r()->error(),
+              R"(12:34 error: @align value must be a positive, power-of-two integer)");
 }
 
 TEST_F(ResolverValidationTest, ZeroStructMemberAlignAttribute) {
@@ -1223,7 +1185,8 @@
                    });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: @align value must be a positive, power-of-two integer");
+    EXPECT_EQ(r()->error(),
+              R"(12:34 error: @align value must be a positive, power-of-two integer)");
 }
 
 TEST_F(ResolverValidationTest, ZeroStructMemberSizeAttribute) {
@@ -1232,7 +1195,7 @@
                    });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: @size must be at least as big as the type's size (4)");
+    EXPECT_EQ(r()->error(), R"(12:34 error: @size must be at least as big as the type's size (4))");
 }
 
 TEST_F(ResolverValidationTest, OffsetAndSizeAttribute) {
@@ -1242,7 +1205,7 @@
                    });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: @offset cannot be used with @align or @size");
+    EXPECT_EQ(r()->error(), R"(12:34 error: @offset cannot be used with @align or @size)");
 }
 
 TEST_F(ResolverValidationTest, OffsetAndAlignAttribute) {
@@ -1252,7 +1215,7 @@
                    });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: @offset cannot be used with @align or @size");
+    EXPECT_EQ(r()->error(), R"(12:34 error: @offset cannot be used with @align or @size)");
 }
 
 TEST_F(ResolverValidationTest, OffsetAndAlignAndSizeAttribute) {
@@ -1262,7 +1225,7 @@
                    });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: @offset cannot be used with @align or @size");
+    EXPECT_EQ(r()->error(), R"(12:34 error: @offset cannot be used with @align or @size)");
 }
 
 TEST_F(ResolverTest, Expr_Initializer_Cast_Pointer) {
@@ -1272,28 +1235,28 @@
     WrapInFunction(Decl(vf), Decl(ip));
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: type is not constructible");
+    EXPECT_EQ(r()->error(), R"(12:34 error: type is not constructible)");
 }
 
 TEST_F(ResolverTest, I32_Overflow) {
     GlobalVar("v", ty.i32(), type::AddressSpace::kPrivate, Expr(Source{{12, 24}}, 2147483648_a));
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:24 error: value 2147483648 cannot be represented as 'i32'");
+    EXPECT_EQ(r()->error(), R"(12:24 error: value 2147483648 cannot be represented as 'i32')");
 }
 
 TEST_F(ResolverTest, I32_Underflow) {
     GlobalVar("v", ty.i32(), type::AddressSpace::kPrivate, Expr(Source{{12, 24}}, -2147483649_a));
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:24 error: value -2147483649 cannot be represented as 'i32'");
+    EXPECT_EQ(r()->error(), R"(12:24 error: value -2147483649 cannot be represented as 'i32')");
 }
 
 TEST_F(ResolverTest, U32_Overflow) {
     GlobalVar("v", ty.u32(), type::AddressSpace::kPrivate, Expr(Source{{12, 24}}, 4294967296_a));
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:24 error: value 4294967296 cannot be represented as 'u32'");
+    EXPECT_EQ(r()->error(), R"(12:24 error: value 4294967296 cannot be represented as 'u32')");
 }
 
 //    var a: array<i32,2>;
@@ -1310,7 +1273,8 @@
     WrapInFunction(a, idx);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "error: cannot index type 'ptr<function, array<i32, 2>, read_write>'");
+    EXPECT_EQ(r()->error(),
+              R"(error: cannot index type 'ptr<function, array<i32, 2>, read_write>')");
 }
 
 }  // namespace
diff --git a/src/tint/sem/type_expression.cc b/src/tint/sem/type_expression.cc
new file mode 100644
index 0000000..5129def
--- /dev/null
+++ b/src/tint/sem/type_expression.cc
@@ -0,0 +1,28 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/sem/type_expression.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::sem::TypeExpression);
+
+namespace tint::sem {
+
+TypeExpression::TypeExpression(const ast::Expression* declaration,
+                               const Statement* statement,
+                               const type::Type* type)
+    : Base(declaration, statement), type_(type) {}
+
+TypeExpression::~TypeExpression() = default;
+
+}  // namespace tint::sem
diff --git a/src/tint/sem/type_expression.h b/src/tint/sem/type_expression.h
new file mode 100644
index 0000000..0d4e43b
--- /dev/null
+++ b/src/tint/sem/type_expression.h
@@ -0,0 +1,50 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_SEM_TYPE_EXPRESSION_H_
+#define SRC_TINT_SEM_TYPE_EXPRESSION_H_
+
+#include "src/tint/sem/expression.h"
+
+// Forward declarations
+namespace tint::type {
+class Type;
+}  // namespace tint::type
+
+namespace tint::sem {
+
+/// TypeExpression holds the semantic information for expression nodes that resolve to types.
+class TypeExpression : public Castable<TypeExpression, Expression> {
+  public:
+    /// Constructor
+    /// @param declaration the AST node
+    /// @param statement the statement that owns this expression
+    /// @param type the type that this expression resolved to
+    TypeExpression(const ast::Expression* declaration,
+                   const Statement* statement,
+                   const type::Type* type);
+
+    /// Destructor
+    ~TypeExpression() override;
+
+    /// @return the type that the expression resolved to
+    const type::Type* Type() const { return type_; }
+
+  private:
+    type::Type const* const type_;
+};
+
+}  // namespace tint::sem
+
+#endif  // SRC_TINT_SEM_TYPE_EXPRESSION_H_