tint/resolver: Track evaluation stage

Keep track of the earliest evaluation point for an expression.
Required to properly track what can be assigned to a `const`, `override`, `let`, `var`.

Bug: tint:1601
Bug: chromium:1343242
Change-Id: I301eec21b71e9036dc1bf6c9af8079317d724762
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/95949
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Antonio Maiorano <amaiorano@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
diff --git a/src/tint/BUILD.gn b/src/tint/BUILD.gn
index 1b2e7d5..c0c0b62 100644
--- a/src/tint/BUILD.gn
+++ b/src/tint/BUILD.gn
@@ -420,6 +420,7 @@
     "sem/constant.h",
     "sem/depth_multisampled_texture.h",
     "sem/depth_texture.h",
+    "sem/evaluation_stage.h",
     "sem/expression.h",
     "sem/external_texture.h",
     "sem/f16.h",
@@ -621,6 +622,7 @@
     "sem/depth_multisampled_texture.h",
     "sem/depth_texture.cc",
     "sem/depth_texture.h",
+    "sem/evaluation_stage.h",
     "sem/expression.cc",
     "sem/expression.h",
     "sem/external_texture.cc",
@@ -1093,6 +1095,7 @@
       "resolver/control_block_validation_test.cc",
       "resolver/dependency_graph_test.cc",
       "resolver/entry_point_validation_test.cc",
+      "resolver/evaluation_stage_test.cc",
       "resolver/function_validation_test.cc",
       "resolver/host_shareable_validation_test.cc",
       "resolver/increment_decrement_validation_test.cc",
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index 1d22340..6c611c3 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -298,6 +298,7 @@
   sem/depth_multisampled_texture.h
   sem/depth_texture.cc
   sem/depth_texture.h
+  sem/evaluation_stage.h
   sem/expression.cc
   sem/expression.h
   sem/external_texture.cc
@@ -777,6 +778,7 @@
     resolver/control_block_validation_test.cc
     resolver/dependency_graph_test.cc
     resolver/entry_point_validation_test.cc
+    resolver/evaluation_stage_test.cc
     resolver/function_validation_test.cc
     resolver/host_shareable_validation_test.cc
     resolver/increment_decrement_validation_test.cc
diff --git a/src/tint/resolver/evaluation_stage_test.cc b/src/tint/resolver/evaluation_stage_test.cc
new file mode 100644
index 0000000..236e4b3
--- /dev/null
+++ b/src/tint/resolver/evaluation_stage_test.cc
@@ -0,0 +1,297 @@
+// Copyright 2022 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 {
+
+using ResolverEvaluationStageTest = ResolverTest;
+
+TEST_F(ResolverEvaluationStageTest, Literal_i32) {
+    auto* expr = Expr(123_i);
+    WrapInFunction(expr);
+
+    ASSERT_TRUE(r()->Resolve()) << r()->error();
+    EXPECT_EQ(Sem().Get(expr)->Stage(), sem::EvaluationStage::kConstant);
+}
+
+TEST_F(ResolverEvaluationStageTest, Literal_f32) {
+    auto* expr = Expr(123_f);
+    WrapInFunction(expr);
+
+    ASSERT_TRUE(r()->Resolve()) << r()->error();
+    EXPECT_EQ(Sem().Get(expr)->Stage(), sem::EvaluationStage::kConstant);
+}
+
+TEST_F(ResolverEvaluationStageTest, Vector_Ctor) {
+    auto* expr = vec3<f32>();
+    WrapInFunction(expr);
+
+    ASSERT_TRUE(r()->Resolve()) << r()->error();
+    EXPECT_EQ(Sem().Get(expr)->Stage(), sem::EvaluationStage::kConstant);
+}
+
+TEST_F(ResolverEvaluationStageTest, Vector_Ctor_Const_Const) {
+    // const f = 1.f;
+    // vec2<f32>(f, f);
+    auto* f = Const("f", nullptr, Expr(1_f));
+    auto* expr = vec2<f32>(f, f);
+    WrapInFunction(f, expr);
+
+    ASSERT_TRUE(r()->Resolve()) << r()->error();
+    EXPECT_EQ(Sem().Get(f)->Stage(), sem::EvaluationStage::kConstant);
+    EXPECT_EQ(Sem().Get(expr)->Stage(), sem::EvaluationStage::kConstant);
+}
+
+TEST_F(ResolverEvaluationStageTest, Vector_Ctor_Runtime_Runtime) {
+    // var f = 1.f;
+    // vec2<f32>(f, f);
+    auto* f = Var("f", nullptr, Expr(1_f));
+    auto* expr = vec2<f32>(f, f);
+    WrapInFunction(f, expr);
+
+    ASSERT_TRUE(r()->Resolve()) << r()->error();
+    EXPECT_EQ(Sem().Get(f)->Stage(), sem::EvaluationStage::kRuntime);
+    EXPECT_EQ(Sem().Get(expr)->Stage(), sem::EvaluationStage::kRuntime);
+}
+
+TEST_F(ResolverEvaluationStageTest, Vector_Conv_Const) {
+    // const f = 1.f;
+    // vec2<u32>(vec2<f32>(f));
+    auto* f = Const("f", nullptr, Expr(1_f));
+    auto* expr = vec2<u32>(vec2<f32>(f));
+    WrapInFunction(f, expr);
+
+    ASSERT_TRUE(r()->Resolve()) << r()->error();
+    EXPECT_EQ(Sem().Get(f)->Stage(), sem::EvaluationStage::kConstant);
+    EXPECT_EQ(Sem().Get(expr)->Stage(), sem::EvaluationStage::kConstant);
+}
+
+TEST_F(ResolverEvaluationStageTest, Vector_Conv_Runtime) {
+    // var f = 1.f;
+    // vec2<u32>(vec2<f32>(f));
+    auto* f = Var("f", nullptr, Expr(1_f));
+    auto* expr = vec2<u32>(vec2<f32>(f));
+    WrapInFunction(f, expr);
+
+    ASSERT_TRUE(r()->Resolve()) << r()->error();
+    EXPECT_EQ(Sem().Get(f)->Stage(), sem::EvaluationStage::kRuntime);
+    EXPECT_EQ(Sem().Get(expr)->Stage(), sem::EvaluationStage::kRuntime);
+}
+
+TEST_F(ResolverEvaluationStageTest, Matrix_Ctor) {
+    auto* expr = mat2x2<f32>();
+    WrapInFunction(expr);
+
+    ASSERT_TRUE(r()->Resolve()) << r()->error();
+    EXPECT_EQ(Sem().Get(expr)->Stage(), sem::EvaluationStage::kConstant);
+}
+
+TEST_F(ResolverEvaluationStageTest, Array_Ctor) {
+    auto* expr = array<f32, 3>();
+    WrapInFunction(expr);
+
+    ASSERT_TRUE(r()->Resolve()) << r()->error();
+    EXPECT_EQ(Sem().Get(expr)->Stage(), sem::EvaluationStage::kConstant);
+}
+
+TEST_F(ResolverEvaluationStageTest, Array_Ctor_Const_Const) {
+    // const f = 1.f;
+    // array<f32, 2>(f, f);
+    auto* f = Const("f", nullptr, Expr(1_f));
+    auto* expr = Construct(ty.array<f32, 2>(), f, f);
+    WrapInFunction(f, expr);
+
+    ASSERT_TRUE(r()->Resolve()) << r()->error();
+    EXPECT_EQ(Sem().Get(f)->Stage(), sem::EvaluationStage::kConstant);
+    EXPECT_EQ(Sem().Get(expr)->Stage(), sem::EvaluationStage::kConstant);
+}
+
+TEST_F(ResolverEvaluationStageTest, Array_Ctor_Const_Override) {
+    // const f1 = 1.f;
+    // override f2 = 2.f;
+    // array<f32, 2>(f1, f2);
+    auto* f1 = Const("f1", nullptr, Expr(1_f));
+    auto* f2 = Override("f2", nullptr, Expr(2_f));
+    auto* expr = Construct(ty.array<f32, 2>(), f1, f2);
+    WrapInFunction(f1, expr);
+
+    ASSERT_TRUE(r()->Resolve()) << r()->error();
+    EXPECT_EQ(Sem().Get(f1)->Stage(), sem::EvaluationStage::kConstant);
+    EXPECT_EQ(Sem().Get(f2)->Stage(), sem::EvaluationStage::kOverride);
+    EXPECT_EQ(Sem().Get(expr)->Stage(), sem::EvaluationStage::kOverride);
+}
+
+TEST_F(ResolverEvaluationStageTest, Array_Ctor_Override_Runtime) {
+    // override f1 = 1.f;
+    // var f2 = 2.f;
+    // array<f32, 2>(f1, f2);
+    auto* f1 = Override("f1", nullptr, Expr(1_f));
+    auto* f2 = Var("f2", nullptr, Expr(2_f));
+    auto* expr = Construct(ty.array<f32, 2>(), f1, f2);
+    WrapInFunction(f2, expr);
+
+    ASSERT_TRUE(r()->Resolve()) << r()->error();
+    EXPECT_EQ(Sem().Get(f1)->Stage(), sem::EvaluationStage::kOverride);
+    EXPECT_EQ(Sem().Get(f2)->Stage(), sem::EvaluationStage::kRuntime);
+    EXPECT_EQ(Sem().Get(expr)->Stage(), sem::EvaluationStage::kRuntime);
+}
+
+TEST_F(ResolverEvaluationStageTest, Array_Ctor_Const_Runtime) {
+    // const f1 = 1.f;
+    // var f2 = 2.f;
+    // array<f32, 2>(f1, f2);
+    auto* f1 = Const("f1", nullptr, Expr(1_f));
+    auto* f2 = Var("f2", nullptr, Expr(2_f));
+    auto* expr = Construct(ty.array<f32, 2>(), f1, f2);
+    WrapInFunction(f1, f2, expr);
+
+    ASSERT_TRUE(r()->Resolve()) << r()->error();
+    EXPECT_EQ(Sem().Get(f1)->Stage(), sem::EvaluationStage::kConstant);
+    EXPECT_EQ(Sem().Get(f2)->Stage(), sem::EvaluationStage::kRuntime);
+    EXPECT_EQ(Sem().Get(expr)->Stage(), sem::EvaluationStage::kRuntime);
+}
+
+TEST_F(ResolverEvaluationStageTest, Array_Ctor_Runtime_Runtime) {
+    // var f = 1.f;
+    // array<f32, 2>(f, f);
+    auto* f = Var("f", nullptr, Expr(1_f));
+    auto* expr = Construct(ty.array<f32, 2>(), f, f);
+    WrapInFunction(f, expr);
+
+    ASSERT_TRUE(r()->Resolve()) << r()->error();
+    EXPECT_EQ(Sem().Get(f)->Stage(), sem::EvaluationStage::kRuntime);
+    EXPECT_EQ(Sem().Get(expr)->Stage(), sem::EvaluationStage::kRuntime);
+}
+
+TEST_F(ResolverEvaluationStageTest, IndexAccessor_Const_Const) {
+    // const vec = vec4<f32>();
+    // const idx = 1_i;
+    // vec[idx]
+    auto* vec = Const("vec", nullptr, vec4<f32>());
+    auto* idx = Const("idx", nullptr, Expr(1_i));
+    auto* expr = IndexAccessor(vec, idx);
+    WrapInFunction(vec, idx, expr);
+
+    ASSERT_TRUE(r()->Resolve()) << r()->error();
+    EXPECT_EQ(Sem().Get(vec)->Stage(), sem::EvaluationStage::kConstant);
+    EXPECT_EQ(Sem().Get(idx)->Stage(), sem::EvaluationStage::kConstant);
+    EXPECT_EQ(Sem().Get(expr)->Stage(), sem::EvaluationStage::kConstant);
+}
+
+TEST_F(ResolverEvaluationStageTest, IndexAccessor_Runtime_Const) {
+    // var vec = vec4<f32>();
+    // const idx = 1_i;
+    // vec[idx]
+    auto* vec = Var("vec", nullptr, vec4<f32>());
+    auto* idx = Const("idx", nullptr, Expr(1_i));
+    auto* expr = IndexAccessor(vec, idx);
+    WrapInFunction(vec, idx, expr);
+
+    ASSERT_TRUE(r()->Resolve()) << r()->error();
+    EXPECT_EQ(Sem().Get(vec)->Stage(), sem::EvaluationStage::kRuntime);
+    EXPECT_EQ(Sem().Get(idx)->Stage(), sem::EvaluationStage::kConstant);
+    EXPECT_EQ(Sem().Get(expr)->Stage(), sem::EvaluationStage::kRuntime);
+}
+
+TEST_F(ResolverEvaluationStageTest, IndexAccessor_Const_Override) {
+    // const vec = vec4<f32>();
+    // override idx = 1_i;
+    // vec[idx]
+    auto* vec = Const("vec", nullptr, vec4<f32>());
+    auto* idx = Override("idx", nullptr, Expr(1_i));
+    auto* expr = IndexAccessor(vec, idx);
+    WrapInFunction(vec, expr);
+
+    ASSERT_TRUE(r()->Resolve()) << r()->error();
+    EXPECT_EQ(Sem().Get(vec)->Stage(), sem::EvaluationStage::kConstant);
+    EXPECT_EQ(Sem().Get(idx)->Stage(), sem::EvaluationStage::kOverride);
+    EXPECT_EQ(Sem().Get(expr)->Stage(), sem::EvaluationStage::kOverride);
+}
+
+TEST_F(ResolverEvaluationStageTest, IndexAccessor_Const_Runtime) {
+    // const vec = vec4<f32>();
+    // let idx = 1_i;
+    // vec[idx]
+    auto* vec = Const("vec", nullptr, vec4<f32>());
+    auto* idx = Let("idx", nullptr, Expr(1_i));
+    auto* expr = IndexAccessor(vec, idx);
+    WrapInFunction(vec, idx, expr);
+
+    ASSERT_TRUE(r()->Resolve()) << r()->error();
+    EXPECT_EQ(Sem().Get(vec)->Stage(), sem::EvaluationStage::kConstant);
+    EXPECT_EQ(Sem().Get(idx)->Stage(), sem::EvaluationStage::kRuntime);
+    EXPECT_EQ(Sem().Get(expr)->Stage(), sem::EvaluationStage::kRuntime);
+}
+
+TEST_F(ResolverEvaluationStageTest, Swizzle_Const) {
+    // const vec = S();
+    // vec.m
+    auto* vec = Const("vec", nullptr, vec4<f32>());
+    auto* expr = MemberAccessor(vec, "xz");
+    WrapInFunction(vec, expr);
+
+    ASSERT_TRUE(r()->Resolve()) << r()->error();
+    EXPECT_EQ(Sem().Get(vec)->Stage(), sem::EvaluationStage::kConstant);
+    EXPECT_EQ(Sem().Get(expr)->Stage(), sem::EvaluationStage::kConstant);
+}
+
+TEST_F(ResolverEvaluationStageTest, Swizzle_Runtime) {
+    // var vec = S();
+    // vec.m
+    auto* vec = Var("vec", nullptr, vec4<f32>());
+    auto* expr = MemberAccessor(vec, "rg");
+    WrapInFunction(vec, expr);
+
+    ASSERT_TRUE(r()->Resolve()) << r()->error();
+    EXPECT_EQ(Sem().Get(vec)->Stage(), sem::EvaluationStage::kRuntime);
+    EXPECT_EQ(Sem().Get(expr)->Stage(), sem::EvaluationStage::kRuntime);
+}
+
+TEST_F(ResolverEvaluationStageTest, MemberAccessor_Const) {
+    // struct S { m : i32 };
+    // const str = S();
+    // str.m
+    Structure("S", {Member("m", ty.i32())});
+    auto* str = Const("str", nullptr, Construct(ty.type_name("S")));
+    auto* expr = MemberAccessor(str, "m");
+    WrapInFunction(str, expr);
+
+    ASSERT_TRUE(r()->Resolve()) << r()->error();
+    EXPECT_EQ(Sem().Get(str)->Stage(), sem::EvaluationStage::kConstant);
+    EXPECT_EQ(Sem().Get(expr)->Stage(), sem::EvaluationStage::kConstant);
+}
+
+TEST_F(ResolverEvaluationStageTest, MemberAccessor_Runtime) {
+    // struct S { m : i32 };
+    // var str = S();
+    // str.m
+    Structure("S", {Member("m", ty.i32())});
+    auto* str = Var("str", nullptr, Construct(ty.type_name("S")));
+    auto* expr = MemberAccessor(str, "m");
+    WrapInFunction(str, expr);
+
+    ASSERT_TRUE(r()->Resolve()) << r()->error();
+    EXPECT_EQ(Sem().Get(str)->Stage(), sem::EvaluationStage::kRuntime);
+    EXPECT_EQ(Sem().Get(expr)->Stage(), sem::EvaluationStage::kRuntime);
+}
+
+}  // namespace
+}  // namespace tint::resolver
diff --git a/src/tint/resolver/intrinsic_table.cc b/src/tint/resolver/intrinsic_table.cc
index bd7dc30..68a1068 100644
--- a/src/tint/resolver/intrinsic_table.cc
+++ b/src/tint/resolver/intrinsic_table.cc
@@ -1119,8 +1119,10 @@
         if (match.overload->flags.Contains(OverloadFlag::kSupportsComputePipeline)) {
             supported_stages.Add(ast::PipelineStage::kCompute);
         }
+        auto eval_stage = match.overload->const_eval_fn ? sem::EvaluationStage::kConstant
+                                                        : sem::EvaluationStage::kRuntime;
         return builder.create<sem::Builtin>(
-            builtin_type, match.return_type, std::move(params), supported_stages,
+            builtin_type, match.return_type, std::move(params), eval_stage, supported_stages,
             match.overload->flags.Contains(OverloadFlag::kIsDeprecated));
     });
     return Builtin{sem, match.overload->const_eval_fn};
@@ -1292,8 +1294,11 @@
                 nullptr, static_cast<uint32_t>(params.size()), p.type, ast::StorageClass::kNone,
                 ast::Access::kUndefined, p.usage));
         }
+        auto eval_stage = match.overload->const_eval_fn ? sem::EvaluationStage::kConstant
+                                                        : sem::EvaluationStage::kRuntime;
         auto* target = utils::GetOrCreate(constructors, match, [&]() {
-            return builder.create<sem::TypeConstructor>(match.return_type, std::move(params));
+            return builder.create<sem::TypeConstructor>(match.return_type, std::move(params),
+                                                        eval_stage);
         });
         return CtorOrConv{target, match.overload->const_eval_fn};
     }
@@ -1303,7 +1308,9 @@
         auto param = builder.create<sem::Parameter>(
             nullptr, 0u, match.parameters[0].type, ast::StorageClass::kNone,
             ast::Access::kUndefined, match.parameters[0].usage);
-        return builder.create<sem::TypeConversion>(match.return_type, param);
+        auto eval_stage = match.overload->const_eval_fn ? sem::EvaluationStage::kConstant
+                                                        : sem::EvaluationStage::kRuntime;
+        return builder.create<sem::TypeConversion>(match.return_type, param, eval_stage);
     });
     return CtorOrConv{target, match.overload->const_eval_fn};
 }
diff --git a/src/tint/resolver/resolver.cc b/src/tint/resolver/resolver.cc
index 8d09d19..7fc09cc 100644
--- a/src/tint/resolver/resolver.cc
+++ b/src/tint/resolver/resolver.cc
@@ -367,10 +367,11 @@
     sem::Variable* sem = nullptr;
     if (is_global) {
         sem = builder_->create<sem::GlobalVariable>(
-            v, ty, ast::StorageClass::kNone, ast::Access::kUndefined, /* constant_value */ nullptr,
-            sem::BindingPoint{});
+            v, ty, sem::EvaluationStage::kRuntime, ast::StorageClass::kNone,
+            ast::Access::kUndefined, /* constant_value */ nullptr, sem::BindingPoint{});
     } else {
-        sem = builder_->create<sem::LocalVariable>(v, ty, ast::StorageClass::kNone,
+        sem = builder_->create<sem::LocalVariable>(v, ty, sem::EvaluationStage::kRuntime,
+                                                   ast::StorageClass::kNone,
                                                    ast::Access::kUndefined, current_statement_,
                                                    /* constant_value */ nullptr);
     }
@@ -421,8 +422,8 @@
     }
 
     auto* sem = builder_->create<sem::GlobalVariable>(
-        v, ty, ast::StorageClass::kNone, ast::Access::kUndefined, /* constant_value */ nullptr,
-        sem::BindingPoint{});
+        v, ty, sem::EvaluationStage::kOverride, ast::StorageClass::kNone, ast::Access::kUndefined,
+        /* constant_value */ nullptr, sem::BindingPoint{});
 
     if (auto* id = ast::GetAttribute<ast::IdAttribute>(v->attributes)) {
         sem->SetConstantId(static_cast<uint16_t>(id->value));
@@ -482,11 +483,11 @@
     }
 
     auto* sem = is_global ? static_cast<sem::Variable*>(builder_->create<sem::GlobalVariable>(
-                                c, ty, ast::StorageClass::kNone, ast::Access::kUndefined, value,
-                                sem::BindingPoint{}))
+                                c, ty, sem::EvaluationStage::kConstant, ast::StorageClass::kNone,
+                                ast::Access::kUndefined, value, sem::BindingPoint{}))
                           : static_cast<sem::Variable*>(builder_->create<sem::LocalVariable>(
-                                c, ty, ast::StorageClass::kNone, ast::Access::kUndefined,
-                                current_statement_, value));
+                                c, ty, sem::EvaluationStage::kConstant, ast::StorageClass::kNone,
+                                ast::Access::kUndefined, current_statement_, value));
 
     sem->SetConstructor(rhs);
     builder_->Sem().Add(c, sem);
@@ -567,12 +568,14 @@
         if (auto bp = var->BindingPoint()) {
             binding_point = {bp.group->value, bp.binding->value};
         }
-        sem = builder_->create<sem::GlobalVariable>(var, var_ty, storage_class, access,
+        sem = builder_->create<sem::GlobalVariable>(var, var_ty, sem::EvaluationStage::kRuntime,
+                                                    storage_class, access,
                                                     /* constant_value */ nullptr, binding_point);
 
     } else {
-        sem = builder_->create<sem::LocalVariable>(
-            var, var_ty, storage_class, access, current_statement_, /* constant_value */ nullptr);
+        sem = builder_->create<sem::LocalVariable>(var, var_ty, sem::EvaluationStage::kRuntime,
+                                                   storage_class, access, current_statement_,
+                                                   /* constant_value */ nullptr);
     }
 
     sem->SetConstructor(rhs);
@@ -1270,6 +1273,7 @@
             [&](const ast::UnaryOpExpression* unary) -> sem::Expression* { return UnaryOp(unary); },
             [&](const ast::PhonyExpression*) -> sem::Expression* {
                 return builder_->create<sem::Expression>(expr, builder_->create<sem::Void>(),
+                                                         sem::EvaluationStage::kRuntime,
                                                          current_statement_,
                                                          /* constant_value */ nullptr,
                                                          /* has_side_effects */ false);
@@ -1423,10 +1427,11 @@
         ty = builder_->create<sem::Reference>(ty, ref->StorageClass(), ref->Access());
     }
 
+    auto stage = sem::EarliestStage(obj->Stage(), idx->Stage());
     auto val = const_eval_.Index(obj, idx);
     bool has_side_effects = idx->HasSideEffects() || obj->HasSideEffects();
     auto* sem = builder_->create<sem::IndexAccessorExpression>(
-        expr, ty, obj, idx, current_statement_, std::move(val), has_side_effects,
+        expr, ty, stage, obj, idx, current_statement_, std::move(val), has_side_effects,
         obj->SourceVariable());
     sem->Behaviors() = idx->Behaviors() + obj->Behaviors();
     return sem;
@@ -1443,8 +1448,9 @@
     }
 
     auto val = const_eval_.Bitcast(ty, inner);
-    auto* sem = builder_->create<sem::Expression>(expr, ty, current_statement_, std::move(val),
-                                                  inner->HasSideEffects());
+    auto stage = sem::EvaluationStage::kRuntime;  // TODO(crbug.com/tint/1581)
+    auto* sem = builder_->create<sem::Expression>(expr, ty, stage, current_statement_,
+                                                  std::move(val), inner->HasSideEffects());
 
     sem->Behaviors() = inner->Behaviors();
 
@@ -1464,6 +1470,7 @@
 
     // Resolve all of the arguments, their types and the set of behaviors.
     std::vector<const sem::Expression*> args(expr->args.size());
+    auto args_stage = sem::EvaluationStage::kConstant;
     sem::Behaviors arg_behaviors;
     for (size_t i = 0; i < expr->args.size(); i++) {
         auto* arg = sem_.Get(expr->args[i]);
@@ -1471,6 +1478,7 @@
             return nullptr;
         }
         args[i] = arg;
+        args_stage = sem::EarliestStage(args_stage, arg->Stage());
         arg_behaviors.Add(arg->Behaviors());
     }
     arg_behaviors.Remove(sem::Behavior::kNext);
@@ -1491,14 +1499,38 @@
             return nullptr;
         }
         const sem::Constant* value = nullptr;
-        if (ctor_or_conv.const_eval_fn) {
+        auto stage = sem::EarliestStage(ctor_or_conv.target->Stage(), args_stage);
+        if (stage == sem::EvaluationStage::kConstant) {
             value = (const_eval_.*ctor_or_conv.const_eval_fn)(ctor_or_conv.target->ReturnType(),
                                                               args.data(), args.size());
         }
-        return builder_->create<sem::Call>(expr, ctor_or_conv.target, std::move(args),
+        return builder_->create<sem::Call>(expr, ctor_or_conv.target, stage, std::move(args),
                                            current_statement_, value, has_side_effects);
     };
 
+    // ct_ctor_or_conv is a helper for building a sem::TypeConstructor for an array or structure
+    // constructor call target.
+    auto arr_or_str_ctor = [&](const sem::Type* ty,
+                               const sem::CallTarget* call_target) -> sem::Call* {
+        if (!MaterializeArguments(args, call_target)) {
+            return nullptr;
+        }
+
+        auto stage = args_stage;               // The evaluation stage of the call
+        const sem::Constant* value = nullptr;  // The constant value for the call
+        if (stage == sem::EvaluationStage::kConstant) {
+            value = const_eval_.ArrayOrStructCtor(ty, args);
+            if (!value) {
+                // Constant evaluation failed.
+                // Can happen for expressions that will fail validation (later).
+                stage = sem::EvaluationStage::kRuntime;
+            }
+        }
+
+        return builder_->create<sem::Call>(expr, call_target, stage, std::move(args),
+                                           current_statement_, std::move(value), has_side_effects);
+    };
+
     // ct_ctor_or_conv is a helper for building either a sem::TypeConstructor or sem::TypeConversion
     // call for the given semantic type.
     auto ty_ctor_or_conv = [&](const sem::Type* ty) {
@@ -1517,7 +1549,7 @@
             [&](const sem::Bool*) { return ct_ctor_or_conv(CtorConvIntrinsic::kBool, nullptr); },
             [&](const sem::Array* arr) -> sem::Call* {
                 auto* call_target = utils::GetOrCreate(
-                    array_ctors_, ArrayConstructorSig{{arr, args.size()}},
+                    array_ctors_, ArrayConstructorSig{{arr, args.size(), args_stage}},
                     [&]() -> sem::TypeConstructor* {
                         sem::ParameterList params(args.size());
                         for (size_t i = 0; i < args.size(); i++) {
@@ -1528,18 +1560,14 @@
                                 ast::StorageClass::kNone,  // storage_class
                                 ast::Access::kUndefined);  // access
                         }
-                        return builder_->create<sem::TypeConstructor>(arr, std::move(params));
+                        return builder_->create<sem::TypeConstructor>(arr, std::move(params),
+                                                                      args_stage);
                     });
-                if (!MaterializeArguments(args, call_target)) {
-                    return nullptr;
-                }
-                auto val = const_eval_.ArrayOrStructCtor(arr, args);
-                return builder_->create<sem::Call>(expr, call_target, std::move(args),
-                                                   current_statement_, val, has_side_effects);
+                return arr_or_str_ctor(arr, call_target);
             },
             [&](const sem::Struct* str) -> sem::Call* {
                 auto* call_target = utils::GetOrCreate(
-                    struct_ctors_, StructConstructorSig{{str, args.size()}},
+                    struct_ctors_, StructConstructorSig{{str, args.size(), args_stage}},
                     [&]() -> sem::TypeConstructor* {
                         sem::ParameterList params(std::min(args.size(), str->Members().size()));
                         for (size_t i = 0, n = params.size(); i < n; i++) {
@@ -1550,15 +1578,10 @@
                                 ast::StorageClass::kNone,   // storage_class
                                 ast::Access::kUndefined);   // access
                         }
-                        return builder_->create<sem::TypeConstructor>(str, std::move(params));
+                        return builder_->create<sem::TypeConstructor>(str, std::move(params),
+                                                                      args_stage);
                     });
-                if (!MaterializeArguments(args, call_target)) {
-                    return nullptr;
-                }
-                auto val = const_eval_.ArrayOrStructCtor(str, args);
-                return builder_->create<sem::Call>(expr, call_target, std::move(args),
-                                                   current_statement_, std::move(val),
-                                                   has_side_effects);
+                return arr_or_str_ctor(str, call_target);
             },
             [&](Default) {
                 AddError("type is not constructible", expr->source);
@@ -1686,9 +1709,16 @@
         AddWarning("use of deprecated builtin", expr->source);
     }
 
+    auto stage = builtin.sem->Stage();
+    if (stage == sem::EvaluationStage::kConstant) {
+        for (auto* arg : args) {
+            stage = sem::EarliestStage(stage, arg->Stage());
+        }
+    }
+
     // If the builtin is @const, and all arguments have constant values, evaluate the builtin now.
     const sem::Constant* value = nullptr;
-    if (builtin.const_eval_fn) {
+    if (stage == sem::EvaluationStage::kConstant && builtin.const_eval_fn) {
         value = (const_eval_.*builtin.const_eval_fn)(builtin.sem->ReturnType(), args.data(),
                                                      args.size());
     }
@@ -1696,8 +1726,8 @@
     bool has_side_effects =
         builtin.sem->HasSideEffects() ||
         std::any_of(args.begin(), args.end(), [](auto* e) { return e->HasSideEffects(); });
-    auto* call = builder_->create<sem::Call>(expr, builtin.sem, std::move(args), current_statement_,
-                                             value, has_side_effects);
+    auto* call = builder_->create<sem::Call>(expr, builtin.sem, stage, std::move(args),
+                                             current_statement_, value, has_side_effects);
 
     if (current_function_) {
         current_function_->AddDirectlyCalledBuiltin(builtin.sem);
@@ -1755,7 +1785,8 @@
     // TODO(crbug.com/tint/1420): For now, assume all function calls have side
     // effects.
     bool has_side_effects = true;
-    auto* call = builder_->create<sem::Call>(expr, target, std::move(args), current_statement_,
+    auto* call = builder_->create<sem::Call>(expr, target, sem::EvaluationStage::kRuntime,
+                                             std::move(args), current_statement_,
                                              /* constant_value */ nullptr, has_side_effects);
 
     target->AddCallSite(call);
@@ -1851,7 +1882,8 @@
     }
 
     auto val = const_eval_.Literal(ty, literal);
-    return builder_->create<sem::Expression>(literal, ty, current_statement_, std::move(val),
+    return builder_->create<sem::Expression>(literal, ty, sem::EvaluationStage::kConstant,
+                                             current_statement_, std::move(val),
                                              /* has_side_effects */ false);
 }
 
@@ -2054,6 +2086,7 @@
     const auto* rhs = sem_.Get(expr->rhs);
     auto* lhs_ty = lhs->Type()->UnwrapRef();
     auto* rhs_ty = rhs->Type()->UnwrapRef();
+    auto stage = sem::EvaluationStage::kRuntime;  // TODO(crbug.com/tint/1581)
 
     auto op = intrinsic_table_->Lookup(expr->op, lhs_ty, rhs_ty, expr->source, false);
     if (!op.result) {
@@ -2079,7 +2112,7 @@
     }
 
     bool has_side_effects = lhs->HasSideEffects() || rhs->HasSideEffects();
-    auto* sem = builder_->create<sem::Expression>(expr, op.result, current_statement_, value,
+    auto* sem = builder_->create<sem::Expression>(expr, op.result, stage, current_statement_, value,
                                                   has_side_effects);
     sem->Behaviors() = lhs->Behaviors() + rhs->Behaviors();
 
@@ -2096,6 +2129,7 @@
     const sem::Type* ty = nullptr;
     const sem::Variable* source_var = nullptr;
     const sem::Constant* value = nullptr;
+    auto stage = sem::EvaluationStage::kRuntime;  // TODO(crbug.com/tint/1581)
 
     switch (unary->op) {
         case ast::UnaryOp::kAddressOf:
@@ -2155,7 +2189,7 @@
         }
     }
 
-    auto* sem = builder_->create<sem::Expression>(unary, ty, current_statement_, value,
+    auto* sem = builder_->create<sem::Expression>(unary, ty, stage, current_statement_, value,
                                                   expr->HasSideEffects(), source_var);
     sem->Behaviors() = expr->Behaviors();
     return sem;
diff --git a/src/tint/resolver/resolver.h b/src/tint/resolver/resolver.h
index 2455127..f3aa38b 100644
--- a/src/tint/resolver/resolver.h
+++ b/src/tint/resolver/resolver.h
@@ -400,12 +400,15 @@
     bool IsBuiltin(Symbol) const;
 
     // ArrayConstructorSig represents a unique array constructor signature.
-    // It is a tuple of the array type and number of arguments provided.
-    using ArrayConstructorSig = utils::UnorderedKeyWrapper<std::tuple<const sem::Array*, size_t>>;
+    // It is a tuple of the array type, number of arguments provided and earliest evaluation stage.
+    using ArrayConstructorSig =
+        utils::UnorderedKeyWrapper<std::tuple<const sem::Array*, size_t, sem::EvaluationStage>>;
 
     // StructConstructorSig represents a unique structure constructor signature.
-    // It is a tuple of the structure type and number of arguments provided.
-    using StructConstructorSig = utils::UnorderedKeyWrapper<std::tuple<const sem::Struct*, size_t>>;
+    // It is a tuple of the structure type, number of arguments provided and earliest evaluation
+    // stage.
+    using StructConstructorSig =
+        utils::UnorderedKeyWrapper<std::tuple<const sem::Struct*, size_t, sem::EvaluationStage>>;
 
     ProgramBuilder* const builder_;
     diag::List& diagnostics_;
diff --git a/src/tint/resolver/validator.cc b/src/tint/resolver/validator.cc
index c7afb54..29e06a2 100644
--- a/src/tint/resolver/validator.cc
+++ b/src/tint/resolver/validator.cc
@@ -547,10 +547,9 @@
                 }
             }
             return Var(v);
-        },                                                  //
-        [&](const ast::Let*) { return Let(v); },            //
-        [&](const ast::Override*) { return Override(v); },  //
-        [&](const ast::Const*) { return true; },            //
+        },                                        //
+        [&](const ast::Let*) { return Let(v); },  //
+        [&](const ast::Const*) { return true; },  //
         [&](Default) {
             TINT_ICE(Resolver, diagnostics_)
                 << "Validator::Variable() called with a unknown variable type: "
@@ -567,6 +566,13 @@
     bool ok = Switch(
         decl,  //
         [&](const ast::Var* var) {
+            if (auto* init = global->Constructor();
+                init && init->Stage() > sem::EvaluationStage::kOverride) {
+                AddError("module-scope 'var' initializer must be a constant or override expression",
+                         init->Declaration()->source);
+                return false;
+            }
+
             if (global->StorageClass() == ast::StorageClass::kNone) {
                 AddError("module-scope 'var' declaration must have a storage class", decl->source);
                 return false;
@@ -619,31 +625,7 @@
 
             return Var(global);
         },
-        [&](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 != 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:",
-                                ast::GetAttribute<ast::IdAttribute>(
-                                    it->second->Declaration()->attributes)
-                                    ->source);
-                        return false;
-                    }
-                    if (id > 65535) {
-                        AddError("pipeline constant IDs must be between 0 and 65535", attr->source);
-                        return false;
-                    }
-                } else {
-                    AddError("attribute is not valid for 'override' declaration", attr->source);
-                    return false;
-                }
-            }
-            return Override(global);
-        },
+        [&](const ast::Override*) { return Override(global, constant_ids); },
         [&](const ast::Const*) {
             if (!decl->attributes.empty()) {
                 AddError("attribute is not valid for module-scope 'const' declaration",
@@ -777,10 +759,39 @@
     return true;
 }
 
-bool Validator::Override(const sem::Variable* v) const {
+bool Validator::Override(const sem::Variable* v,
+                         std::unordered_map<uint32_t, const sem::Variable*> constant_ids) const {
     auto* decl = v->Declaration();
     auto* storage_ty = v->Type()->UnwrapRef();
 
+    if (auto* init = v->Constructor(); init && init->Stage() > sem::EvaluationStage::kOverride) {
+        AddError("'override' initializer must be an override expression",
+                 init->Declaration()->source);
+        return false;
+    }
+
+    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 != v) {
+                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:",
+                        ast::GetAttribute<ast::IdAttribute>(it->second->Declaration()->attributes)
+                            ->source);
+                return false;
+            }
+            if (id > 65535) {
+                AddError("pipeline constant IDs must be between 0 and 65535", attr->source);
+                return false;
+            }
+        } else {
+            AddError("attribute is not valid for 'override' declaration", attr->source);
+            return false;
+        }
+    }
+
     auto name = symbols_.NameFor(decl->symbol);
     if (sem::ParseBuiltinType(name) != sem::BuiltinType::kNone) {
         AddError("'" + name + "' is a builtin and cannot be redeclared as a 'override'",
diff --git a/src/tint/resolver/validator.h b/src/tint/resolver/validator.h
index 4d41959..1f15a9e 100644
--- a/src/tint/resolver/validator.h
+++ b/src/tint/resolver/validator.h
@@ -371,8 +371,10 @@
 
     /// Validates a 'override' variable declaration
     /// @param v the variable to validate
+    /// @param constant_ids the set of constant ids in the module
     /// @returns true on success, false otherwise.
-    bool Override(const sem::Variable* v) const;
+    bool Override(const sem::Variable* v,
+                  std::unordered_map<uint32_t, const sem::Variable*> constant_ids) const;
 
     /// Validates a 'const' variable declaration
     /// @param v the variable to validate
diff --git a/src/tint/sem/builtin.cc b/src/tint/sem/builtin.cc
index bb2878b..c688c4c 100644
--- a/src/tint/sem/builtin.cc
+++ b/src/tint/sem/builtin.cc
@@ -90,9 +90,10 @@
 Builtin::Builtin(BuiltinType type,
                  const sem::Type* return_type,
                  std::vector<Parameter*> parameters,
+                 EvaluationStage eval_stage,
                  PipelineStageSet supported_stages,
                  bool is_deprecated)
-    : Base(return_type, utils::ToConstPtrVec(parameters)),
+    : Base(return_type, utils::ToConstPtrVec(parameters), eval_stage),
       type_(type),
       supported_stages_(supported_stages),
       is_deprecated_(is_deprecated) {
diff --git a/src/tint/sem/builtin.h b/src/tint/sem/builtin.h
index 1dc61ad..783e6ca 100644
--- a/src/tint/sem/builtin.h
+++ b/src/tint/sem/builtin.h
@@ -83,6 +83,7 @@
     /// @param type the builtin type
     /// @param return_type the return type for the builtin call
     /// @param parameters the parameters for the builtin overload
+    /// @param eval_stage the earliest evaluation stage for a call to the builtin
     /// @param supported_stages the pipeline stages that this builtin can be
     /// used in
     /// @param is_deprecated true if the particular overload is considered
@@ -90,6 +91,7 @@
     Builtin(BuiltinType type,
             const sem::Type* return_type,
             std::vector<Parameter*> parameters,
+            EvaluationStage eval_stage,
             PipelineStageSet supported_stages,
             bool is_deprecated);
 
diff --git a/src/tint/sem/call.cc b/src/tint/sem/call.cc
index bfce3b1..a20649d 100644
--- a/src/tint/sem/call.cc
+++ b/src/tint/sem/call.cc
@@ -23,13 +23,17 @@
 
 Call::Call(const ast::CallExpression* declaration,
            const CallTarget* target,
+           EvaluationStage stage,
            std::vector<const sem::Expression*> arguments,
            const Statement* statement,
            const Constant* constant,
            bool has_side_effects)
-    : Base(declaration, target->ReturnType(), statement, constant, has_side_effects),
+    : Base(declaration, target->ReturnType(), stage, statement, constant, has_side_effects),
       target_(target),
-      arguments_(std::move(arguments)) {}
+      arguments_(std::move(arguments)) {
+    // Check that the stage is no earlier than the target supports
+    TINT_ASSERT(Semantic, target->Stage() <= stage);
+}
 
 Call::~Call() = default;
 
diff --git a/src/tint/sem/call.h b/src/tint/sem/call.h
index 1955bf9..2d82306 100644
--- a/src/tint/sem/call.h
+++ b/src/tint/sem/call.h
@@ -30,12 +30,14 @@
     /// Constructor
     /// @param declaration the AST node
     /// @param target the call target
+    /// @param stage the earliest evaluation stage for the expression
     /// @param arguments the call arguments
     /// @param statement the statement that owns this expression
     /// @param constant the constant value of this expression
     /// @param has_side_effects whether this expression may have side effects
     Call(const ast::CallExpression* declaration,
          const CallTarget* target,
+         EvaluationStage stage,
          std::vector<const sem::Expression*> arguments,
          const Statement* statement,
          const Constant* constant,
diff --git a/src/tint/sem/call_target.cc b/src/tint/sem/call_target.cc
index 67bde0e..f8bcdd8 100644
--- a/src/tint/sem/call_target.cc
+++ b/src/tint/sem/call_target.cc
@@ -21,8 +21,10 @@
 
 namespace tint::sem {
 
-CallTarget::CallTarget(const sem::Type* return_type, const ParameterList& parameters)
-    : signature_{return_type, parameters} {
+CallTarget::CallTarget(const sem::Type* return_type,
+                       const ParameterList& parameters,
+                       EvaluationStage stage)
+    : signature_{return_type, parameters}, stage_(stage) {
     TINT_ASSERT(Semantic, return_type);
 }
 
diff --git a/src/tint/sem/call_target.h b/src/tint/sem/call_target.h
index 64716b2..be1b96a 100644
--- a/src/tint/sem/call_target.h
+++ b/src/tint/sem/call_target.h
@@ -63,9 +63,12 @@
 class CallTarget : public Castable<CallTarget, Node> {
   public:
     /// Constructor
+    /// @param stage the earliest evaluation stage for a call to this target
     /// @param return_type the return type of the call target
     /// @param parameters the parameters for the call target
-    CallTarget(const sem::Type* return_type, const ParameterList& parameters);
+    CallTarget(const sem::Type* return_type,
+               const ParameterList& parameters,
+               EvaluationStage stage);
 
     /// Copy constructor
     CallTarget(const CallTarget&);
@@ -82,8 +85,12 @@
     /// @return the signature of the call target
     const CallTargetSignature& Signature() const { return signature_; }
 
+    /// @return the earliest evaluation stage for a call to this target
+    EvaluationStage Stage() const { return stage_; }
+
   private:
     CallTargetSignature signature_;
+    EvaluationStage stage_;
 };
 
 }  // namespace tint::sem
diff --git a/src/tint/sem/evaluation_stage.h b/src/tint/sem/evaluation_stage.h
new file mode 100644
index 0000000..b5e554d
--- /dev/null
+++ b/src/tint/sem/evaluation_stage.h
@@ -0,0 +1,60 @@
+// Copyright 2022 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_EVALUATION_STAGE_H_
+#define SRC_TINT_SEM_EVALUATION_STAGE_H_
+
+#include <algorithm>
+#include <initializer_list>
+
+namespace tint::sem {
+
+/// The earliest point in time that an expression can be evaluated
+enum class EvaluationStage {
+    /// Expression can be evaluated at shader creation time
+    kConstant,
+    /// Expression can be evaluated at pipeline creation time
+    kOverride,
+    /// Expression can be evaluated at runtime
+    kRuntime,
+};
+
+/// @returns true if stage `a` comes earlier than stage `b`
+inline bool operator<(EvaluationStage a, EvaluationStage b) {
+    return static_cast<int>(a) < static_cast<int>(b);
+}
+
+/// @returns true if stage `a` comes later than stage `b`
+inline bool operator>(EvaluationStage a, EvaluationStage b) {
+    return static_cast<int>(a) > static_cast<int>(b);
+}
+
+/// @param stages a list of EvaluationStage.
+/// @returns the earliest stage supported by all the provided stages
+inline EvaluationStage EarliestStage(std::initializer_list<EvaluationStage> stages) {
+    auto earliest = EvaluationStage::kConstant;
+    for (auto stage : stages) {
+        earliest = std::max(stage, earliest);
+    }
+    return static_cast<EvaluationStage>(earliest);
+}
+
+template <typename... ARGS>
+inline EvaluationStage EarliestStage(ARGS... args) {
+    return EarliestStage({args...});
+}
+
+}  // namespace tint::sem
+
+#endif  // SRC_TINT_SEM_EVALUATION_STAGE_H_
diff --git a/src/tint/sem/expression.cc b/src/tint/sem/expression.cc
index 4415db5..cdaa2bc 100644
--- a/src/tint/sem/expression.cc
+++ b/src/tint/sem/expression.cc
@@ -24,6 +24,7 @@
 
 Expression::Expression(const ast::Expression* declaration,
                        const sem::Type* type,
+                       EvaluationStage stage,
                        const Statement* statement,
                        const Constant* constant,
                        bool has_side_effects,
@@ -31,10 +32,12 @@
     : declaration_(declaration),
       source_variable_(source_var),
       type_(type),
+      stage_(stage),
       statement_(statement),
       constant_(std::move(constant)),
       has_side_effects_(has_side_effects) {
     TINT_ASSERT(Semantic, type_);
+    TINT_ASSERT(Semantic, (constant != nullptr) == (stage == EvaluationStage::kConstant));
 }
 
 Expression::~Expression() = default;
diff --git a/src/tint/sem/expression.h b/src/tint/sem/expression.h
index 45a67ca..5942a71 100644
--- a/src/tint/sem/expression.h
+++ b/src/tint/sem/expression.h
@@ -18,6 +18,7 @@
 #include "src/tint/ast/expression.h"
 #include "src/tint/sem/behavior.h"
 #include "src/tint/sem/constant.h"
+#include "src/tint/sem/evaluation_stage.h"
 #include "src/tint/sem/node.h"
 
 // Forward declarations
@@ -28,18 +29,21 @@
 }  // namespace tint::sem
 
 namespace tint::sem {
+
 /// Expression holds the semantic information for expression nodes.
 class Expression : public Castable<Expression, Node> {
   public:
     /// Constructor
     /// @param declaration the AST node
     /// @param type the resolved type of the expression
+    /// @param stage the earliest evaluation stage for the expression
     /// @param statement the statement that owns this expression
     /// @param constant the constant value of the expression. May be null
     /// @param has_side_effects true if this expression may have side-effects
     /// @param source_var the (optional) source variable for this expression
     Expression(const ast::Expression* declaration,
                const sem::Type* type,
+               EvaluationStage stage,
                const Statement* statement,
                const Constant* constant,
                bool has_side_effects,
@@ -54,6 +58,9 @@
     /// @return the resolved type of the expression
     const sem::Type* Type() const { return type_; }
 
+    /// @return the earliest evaluation stage for the expression
+    EvaluationStage Stage() const { return stage_; }
+
     /// @return the statement that owns this expression
     const Statement* Stmt() const { return statement_; }
 
@@ -87,6 +94,7 @@
 
   private:
     const sem::Type* const type_;
+    const EvaluationStage stage_;
     const Statement* const statement_;
     const Constant* const constant_;
     sem::Behaviors behaviors_{sem::Behavior::kNext};
diff --git a/src/tint/sem/expression_test.cc b/src/tint/sem/expression_test.cc
index cc4bf0e..63e01fb 100644
--- a/src/tint/sem/expression_test.cc
+++ b/src/tint/sem/expression_test.cc
@@ -43,7 +43,8 @@
 
 TEST_F(ExpressionTest, UnwrapMaterialize) {
     MockConstant c(create<I32>());
-    auto* a = create<Expression>(/* declaration */ nullptr, create<I32>(), /* statement */ nullptr,
+    auto* a = create<Expression>(/* declaration */ nullptr, create<I32>(),
+                                 sem::EvaluationStage::kRuntime, /* statement */ nullptr,
                                  /* constant_value */ nullptr,
                                  /* has_side_effects */ false, /* source_var */ nullptr);
     auto* b = create<Materialize>(a, /* statement */ nullptr, &c);
diff --git a/src/tint/sem/function.cc b/src/tint/sem/function.cc
index cd34eb7..dcc80b0 100644
--- a/src/tint/sem/function.cc
+++ b/src/tint/sem/function.cc
@@ -30,7 +30,7 @@
 Function::Function(const ast::Function* declaration,
                    Type* return_type,
                    std::vector<Parameter*> parameters)
-    : Base(return_type, utils::ToConstPtrVec(parameters)),
+    : Base(return_type, utils::ToConstPtrVec(parameters), EvaluationStage::kRuntime),
       declaration_(declaration),
       workgroup_size_{WorkgroupDimension{1}, WorkgroupDimension{1}, WorkgroupDimension{1}} {
     for (auto* parameter : parameters) {
diff --git a/src/tint/sem/index_accessor_expression.cc b/src/tint/sem/index_accessor_expression.cc
index cd74201..70d1d5f 100644
--- a/src/tint/sem/index_accessor_expression.cc
+++ b/src/tint/sem/index_accessor_expression.cc
@@ -24,13 +24,14 @@
 
 IndexAccessorExpression::IndexAccessorExpression(const ast::IndexAccessorExpression* declaration,
                                                  const sem::Type* type,
+                                                 EvaluationStage stage,
                                                  const Expression* object,
                                                  const Expression* index,
                                                  const Statement* statement,
                                                  const Constant* constant,
                                                  bool has_side_effects,
                                                  const Variable* source_var /* = nullptr */)
-    : Base(declaration, type, statement, constant, has_side_effects, source_var),
+    : Base(declaration, type, stage, statement, constant, has_side_effects, source_var),
       object_(object),
       index_(index) {}
 
diff --git a/src/tint/sem/index_accessor_expression.h b/src/tint/sem/index_accessor_expression.h
index 3ba10ea..ea93df7 100644
--- a/src/tint/sem/index_accessor_expression.h
+++ b/src/tint/sem/index_accessor_expression.h
@@ -32,6 +32,7 @@
     /// Constructor
     /// @param declaration the AST node
     /// @param type the resolved type of the expression
+    /// @param stage the earliest evaluation stage for the expression
     /// @param object the object expression that is being indexed
     /// @param index the index expression
     /// @param statement the statement that owns this expression
@@ -40,6 +41,7 @@
     /// @param source_var the (optional) source variable for this expression
     IndexAccessorExpression(const ast::IndexAccessorExpression* declaration,
                             const sem::Type* type,
+                            EvaluationStage stage,
                             const Expression* object,
                             const Expression* index,
                             const Statement* statement,
diff --git a/src/tint/sem/materialize.cc b/src/tint/sem/materialize.cc
index 739b0ce..15c31ae 100644
--- a/src/tint/sem/materialize.cc
+++ b/src/tint/sem/materialize.cc
@@ -22,6 +22,7 @@
                          const Constant* constant)
     : Base(/* declaration */ expr->Declaration(),
            /* type */ constant->Type(),
+           /* stage */ EvaluationStage::kConstant,  // Abstract can only be const-expr
            /* statement */ statement,
            /* constant */ constant,
            /* has_side_effects */ false,
diff --git a/src/tint/sem/member_accessor_expression.cc b/src/tint/sem/member_accessor_expression.cc
index 7c31b0e..74b4833 100644
--- a/src/tint/sem/member_accessor_expression.cc
+++ b/src/tint/sem/member_accessor_expression.cc
@@ -25,12 +25,14 @@
 
 MemberAccessorExpression::MemberAccessorExpression(const ast::MemberAccessorExpression* declaration,
                                                    const sem::Type* type,
+                                                   EvaluationStage stage,
                                                    const Statement* statement,
                                                    const Constant* constant,
                                                    const Expression* object,
                                                    bool has_side_effects,
                                                    const Variable* source_var /* = nullptr */)
-    : Base(declaration, type, statement, constant, has_side_effects, source_var), object_(object) {}
+    : Base(declaration, type, stage, statement, constant, has_side_effects, source_var),
+      object_(object) {}
 
 MemberAccessorExpression::~MemberAccessorExpression() = default;
 
@@ -42,7 +44,14 @@
                                        const StructMember* member,
                                        bool has_side_effects,
                                        const Variable* source_var /* = nullptr */)
-    : Base(declaration, type, statement, constant, object, has_side_effects, source_var),
+    : Base(declaration,
+           type,
+           object->Stage(),
+           statement,
+           constant,
+           object,
+           has_side_effects,
+           source_var),
       member_(member) {}
 
 StructMemberAccess::~StructMemberAccess() = default;
@@ -55,7 +64,14 @@
                  std::vector<uint32_t> indices,
                  bool has_side_effects,
                  const Variable* source_var /* = nullptr */)
-    : Base(declaration, type, statement, constant, object, has_side_effects, source_var),
+    : Base(declaration,
+           type,
+           object->Stage(),
+           statement,
+           constant,
+           object,
+           has_side_effects,
+           source_var),
       indices_(std::move(indices)) {}
 
 Swizzle::~Swizzle() = default;
diff --git a/src/tint/sem/member_accessor_expression.h b/src/tint/sem/member_accessor_expression.h
index d8484a8..43e1466 100644
--- a/src/tint/sem/member_accessor_expression.h
+++ b/src/tint/sem/member_accessor_expression.h
@@ -37,6 +37,7 @@
     /// Constructor
     /// @param declaration the AST node
     /// @param type the resolved type of the expression
+    /// @param stage the earliest evaluation stage for the expression
     /// @param statement the statement that owns this expression
     /// @param constant the constant value of the expression. May be null.
     /// @param object the object that holds the member being accessed
@@ -44,6 +45,7 @@
     /// @param source_var the (optional) source variable for this expression
     MemberAccessorExpression(const ast::MemberAccessorExpression* declaration,
                              const sem::Type* type,
+                             EvaluationStage stage,
                              const Statement* statement,
                              const Constant* constant,
                              const Expression* object,
diff --git a/src/tint/sem/type_constructor.cc b/src/tint/sem/type_constructor.cc
index 34f6e2a..d85e1be 100644
--- a/src/tint/sem/type_constructor.cc
+++ b/src/tint/sem/type_constructor.cc
@@ -18,8 +18,10 @@
 
 namespace tint::sem {
 
-TypeConstructor::TypeConstructor(const sem::Type* type, const ParameterList& parameters)
-    : Base(type, parameters) {}
+TypeConstructor::TypeConstructor(const sem::Type* type,
+                                 const ParameterList& parameters,
+                                 EvaluationStage stage)
+    : Base(type, parameters, stage) {}
 
 TypeConstructor::~TypeConstructor() = default;
 
diff --git a/src/tint/sem/type_constructor.h b/src/tint/sem/type_constructor.h
index f3d4221..74b7858 100644
--- a/src/tint/sem/type_constructor.h
+++ b/src/tint/sem/type_constructor.h
@@ -25,7 +25,8 @@
     /// Constructor
     /// @param type the type that's being constructed
     /// @param parameters the type constructor parameters
-    TypeConstructor(const sem::Type* type, const ParameterList& parameters);
+    /// @param stage the earliest evaluation stage for the expression
+    TypeConstructor(const sem::Type* type, const ParameterList& parameters, EvaluationStage stage);
 
     /// Destructor
     ~TypeConstructor() override;
diff --git a/src/tint/sem/type_conversion.cc b/src/tint/sem/type_conversion.cc
index 5da2928..262e3a0 100644
--- a/src/tint/sem/type_conversion.cc
+++ b/src/tint/sem/type_conversion.cc
@@ -18,8 +18,10 @@
 
 namespace tint::sem {
 
-TypeConversion::TypeConversion(const sem::Type* type, const sem::Parameter* parameter)
-    : Base(type, ParameterList{parameter}) {}
+TypeConversion::TypeConversion(const sem::Type* type,
+                               const sem::Parameter* parameter,
+                               EvaluationStage stage)
+    : Base(type, ParameterList{parameter}, stage) {}
 
 TypeConversion::~TypeConversion() = default;
 
diff --git a/src/tint/sem/type_conversion.h b/src/tint/sem/type_conversion.h
index e400565..5584b36 100644
--- a/src/tint/sem/type_conversion.h
+++ b/src/tint/sem/type_conversion.h
@@ -25,7 +25,8 @@
     /// Constructor
     /// @param type the target type of the cast
     /// @param parameter the type cast parameter
-    TypeConversion(const sem::Type* type, const sem::Parameter* parameter);
+    /// @param stage the earliest evaluation stage for the expression
+    TypeConversion(const sem::Type* type, const sem::Parameter* parameter, EvaluationStage stage);
 
     /// Destructor
     ~TypeConversion() override;
diff --git a/src/tint/sem/variable.cc b/src/tint/sem/variable.cc
index 3849807..28a373b 100644
--- a/src/tint/sem/variable.cc
+++ b/src/tint/sem/variable.cc
@@ -28,14 +28,15 @@
 TINT_INSTANTIATE_TYPEINFO(tint::sem::VariableUser);
 
 namespace tint::sem {
-
 Variable::Variable(const ast::Variable* declaration,
                    const sem::Type* type,
+                   EvaluationStage stage,
                    ast::StorageClass storage_class,
                    ast::Access access,
                    const Constant* constant_value)
     : declaration_(declaration),
       type_(type),
+      stage_(stage),
       storage_class_(storage_class),
       access_(access),
       constant_value_(constant_value) {}
@@ -44,21 +45,24 @@
 
 LocalVariable::LocalVariable(const ast::Variable* declaration,
                              const sem::Type* type,
+                             EvaluationStage stage,
                              ast::StorageClass storage_class,
                              ast::Access access,
                              const sem::Statement* statement,
                              const Constant* constant_value)
-    : Base(declaration, type, storage_class, access, constant_value), statement_(statement) {}
+    : Base(declaration, type, stage, storage_class, access, constant_value),
+      statement_(statement) {}
 
 LocalVariable::~LocalVariable() = default;
 
 GlobalVariable::GlobalVariable(const ast::Variable* declaration,
                                const sem::Type* type,
+                               EvaluationStage stage,
                                ast::StorageClass storage_class,
                                ast::Access access,
                                const Constant* constant_value,
                                sem::BindingPoint binding_point)
-    : Base(declaration, type, storage_class, access, constant_value),
+    : Base(declaration, type, stage, storage_class, access, constant_value),
       binding_point_(binding_point) {}
 
 GlobalVariable::~GlobalVariable() = default;
@@ -69,7 +73,9 @@
                      ast::StorageClass storage_class,
                      ast::Access access,
                      const ParameterUsage usage /* = ParameterUsage::kNone */)
-    : Base(declaration, type, storage_class, access, nullptr), index_(index), usage_(usage) {}
+    : Base(declaration, type, EvaluationStage::kRuntime, storage_class, access, nullptr),
+      index_(index),
+      usage_(usage) {}
 
 Parameter::~Parameter() = default;
 
@@ -78,6 +84,7 @@
                            sem::Variable* variable)
     : Base(declaration,
            variable->Type(),
+           variable->Stage(),
            statement,
            variable->ConstantValue(),
            /* has_side_effects */ false),
diff --git a/src/tint/sem/variable.h b/src/tint/sem/variable.h
index e5a5cec..fe54f50 100644
--- a/src/tint/sem/variable.h
+++ b/src/tint/sem/variable.h
@@ -45,11 +45,13 @@
     /// Constructor
     /// @param declaration the AST declaration node
     /// @param type the variable type
+    /// @param stage the evaluation stage for an expression of this variable type
     /// @param storage_class the variable storage class
     /// @param access the variable access control type
     /// @param constant_value the constant value for the variable. May be null
     Variable(const ast::Variable* declaration,
              const sem::Type* type,
+             EvaluationStage stage,
              ast::StorageClass storage_class,
              ast::Access access,
              const Constant* constant_value);
@@ -63,6 +65,9 @@
     /// @returns the canonical type for the variable
     const sem::Type* Type() const { return type_; }
 
+    /// @returns the evaluation stage for an expression of this variable type
+    EvaluationStage Stage() const { return stage_; }
+
     /// @returns the storage class for the variable
     ast::StorageClass StorageClass() const { return storage_class_; }
 
@@ -89,6 +94,7 @@
   private:
     const ast::Variable* const declaration_;
     const sem::Type* const type_;
+    const EvaluationStage stage_;
     const ast::StorageClass storage_class_;
     const ast::Access access_;
     const Constant* constant_value_;
@@ -102,12 +108,14 @@
     /// Constructor
     /// @param declaration the AST declaration node
     /// @param type the variable type
+    /// @param stage the evaluation stage for an expression of this variable type
     /// @param storage_class the variable storage class
     /// @param access the variable access control type
     /// @param statement the statement that declared this local variable
     /// @param constant_value the constant value for the variable. May be null
     LocalVariable(const ast::Variable* declaration,
                   const sem::Type* type,
+                  EvaluationStage stage,
                   ast::StorageClass storage_class,
                   ast::Access access,
                   const sem::Statement* statement,
@@ -137,12 +145,14 @@
     /// Constructor
     /// @param declaration the AST declaration node
     /// @param type the variable type
+    /// @param stage the evaluation stage for an expression of this variable type
     /// @param storage_class the variable storage class
     /// @param access the variable access control type
     /// @param constant_value the constant value for the variable. May be null
     /// @param binding_point the optional resource binding point of the variable
     GlobalVariable(const ast::Variable* declaration,
                    const sem::Type* type,
+                   EvaluationStage stage,
                    ast::StorageClass storage_class,
                    ast::Access access,
                    const Constant* constant_value,
diff --git a/src/tint/writer/append_vector.cc b/src/tint/writer/append_vector.cc
index c5f184d..28845d9 100644
--- a/src/tint/writer/append_vector.cc
+++ b/src/tint/writer/append_vector.cc
@@ -59,7 +59,8 @@
             << "unsupported vector element type: " << ty->TypeInfo().name;
         return nullptr;
     }
-    auto* sem = b.create<sem::Expression>(expr, ty, stmt, /* constant_value */ nullptr,
+    auto* sem = b.create<sem::Expression>(expr, ty, sem::EvaluationStage::kRuntime, stmt,
+                                          /* constant_value */ nullptr,
                                           /* has_side_effects */ false);
     b.Sem().Add(expr, sem);
     return sem;
@@ -136,10 +137,12 @@
         auto* scalar_cast_target = b->create<sem::TypeConversion>(
             packed_el_sem_ty,
             b->create<sem::Parameter>(nullptr, 0u, scalar_sem->Type()->UnwrapRef(),
-                                      ast::StorageClass::kNone, ast::Access::kUndefined));
+                                      ast::StorageClass::kNone, ast::Access::kUndefined),
+            sem::EvaluationStage::kRuntime);
         auto* scalar_cast_sem = b->create<sem::Call>(
-            scalar_cast_ast, scalar_cast_target, std::vector<const sem::Expression*>{scalar_sem},
-            statement, /* constant_value */ nullptr, /* has_side_effects */ false);
+            scalar_cast_ast, scalar_cast_target, sem::EvaluationStage::kRuntime,
+            std::vector<const sem::Expression*>{scalar_sem}, statement,
+            /* constant_value */ nullptr, /* has_side_effects */ false);
         b->Sem().Add(scalar_cast_ast, scalar_cast_sem);
         packed.emplace_back(scalar_cast_sem);
     } else {
@@ -151,15 +154,17 @@
         utils::Transform(packed, [&](const sem::Expression* expr) { return expr->Declaration(); }));
     auto* constructor_target = b->create<sem::TypeConstructor>(
         packed_sem_ty,
-        utils::Transform(
-            packed, [&](const tint::sem::Expression* arg, size_t i) -> const sem::Parameter* {
-                return b->create<sem::Parameter>(nullptr, static_cast<uint32_t>(i),
-                                                 arg->Type()->UnwrapRef(), ast::StorageClass::kNone,
-                                                 ast::Access::kUndefined);
-            }));
-    auto* constructor_sem = b->create<sem::Call>(constructor_ast, constructor_target, packed,
-                                                 statement, /* constant_value */ nullptr,
-                                                 /* has_side_effects */ false);
+        utils::Transform(packed,
+                         [&](const tint::sem::Expression* arg, size_t i) -> const sem::Parameter* {
+                             return b->create<sem::Parameter>(
+                                 nullptr, static_cast<uint32_t>(i), arg->Type()->UnwrapRef(),
+                                 ast::StorageClass::kNone, ast::Access::kUndefined);
+                         }),
+        sem::EvaluationStage::kRuntime);
+    auto* constructor_sem =
+        b->create<sem::Call>(constructor_ast, constructor_target, sem::EvaluationStage::kRuntime,
+                             packed, statement, /* constant_value */ nullptr,
+                             /* has_side_effects */ false);
     b->Sem().Add(constructor_ast, constructor_sem);
     return constructor_sem;
 }
diff --git a/src/tint/writer/glsl/generator_impl.cc b/src/tint/writer/glsl/generator_impl.cc
index 1a2b5dc..d28f3f4 100644
--- a/src/tint/writer/glsl/generator_impl.cc
+++ b/src/tint/writer/glsl/generator_impl.cc
@@ -1338,7 +1338,8 @@
 const ast::Expression* GeneratorImpl::CreateF32Zero(const sem::Statement* stmt) {
     auto* zero = builder_.Expr(0_f);
     auto* f32 = builder_.create<sem::F32>();
-    auto* sem_zero = builder_.create<sem::Expression>(zero, f32, stmt, /* constant_value */ nullptr,
+    auto* sem_zero = builder_.create<sem::Expression>(zero, f32, sem::EvaluationStage::kRuntime,
+                                                      stmt, /* constant_value */ nullptr,
                                                       /* has_side_effects */ false);
     builder_.Sem().Add(zero, sem_zero);
     return zero;
diff --git a/src/tint/writer/hlsl/generator_impl.cc b/src/tint/writer/hlsl/generator_impl.cc
index 0a6a196..e8ab2c7 100644
--- a/src/tint/writer/hlsl/generator_impl.cc
+++ b/src/tint/writer/hlsl/generator_impl.cc
@@ -2392,7 +2392,8 @@
         auto* zero = builder_.Expr(0_i);
         auto* stmt = builder_.Sem().Get(vector)->Stmt();
         builder_.Sem().Add(
-            zero, builder_.create<sem::Expression>(zero, i32, stmt, /* constant_value */ nullptr,
+            zero, builder_.create<sem::Expression>(zero, i32, sem::EvaluationStage::kRuntime, stmt,
+                                                   /* constant_value */ nullptr,
                                                    /* has_side_effects */ false));
         auto* packed = AppendVector(&builder_, vector, zero);
         return EmitExpression(out, packed->Declaration());