tint/resolver: Improve errors for expr eval-stages

Raise the error on the inner-most expression that violates the required evaluation stage.

Fixed: tint:1655
Change-Id: I82186e72ed6efa1cd6d4456c04446da18e9f1850
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/105640
Commit-Queue: Ben Clayton <bclayton@google.com>
Auto-Submit: Ben Clayton <bclayton@google.com>
Reviewed-by: Dan Sinclair <dsinclair@chromium.org>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/src/tint/reader/wgsl/parser_impl_texture_sampler_test.cc b/src/tint/reader/wgsl/parser_impl_texture_sampler_test.cc
index ce31276..a13998a 100644
--- a/src/tint/reader/wgsl/parser_impl_texture_sampler_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_texture_sampler_test.cc
@@ -239,7 +239,8 @@
     EXPECT_EQ(t.value, nullptr);
     EXPECT_FALSE(t.matched);
     EXPECT_TRUE(t.errored);
-    EXPECT_EQ(p->error(), R"(1:30: expected access control for storage texture type. Did you mean 'read'?
+    EXPECT_EQ(p->error(),
+              R"(1:30: expected access control for storage texture type. Did you mean 'read'?
 Possible values: 'read', 'read_write', 'write')");
 }
 
diff --git a/src/tint/resolver/attribute_validation_test.cc b/src/tint/resolver/attribute_validation_test.cc
index bd965f7..30615bb 100644
--- a/src/tint/resolver/attribute_validation_test.cc
+++ b/src/tint/resolver/attribute_validation_test.cc
@@ -738,7 +738,9 @@
     Structure("mystruct", utils::Vector{Member(
                               "a", ty.f32(), utils::Vector{MemberAlign(Source{{12, 34}}, "val")})});
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: 'align' must be an i32 or u32 value)");
+    EXPECT_EQ(
+        r()->error(),
+        R"(error: @align requires a const-expression, but expression is an override-expression)");
 }
 
 }  // namespace StructAndStructMemberTests
diff --git a/src/tint/resolver/builtin_test.cc b/src/tint/resolver/builtin_test.cc
index 2917f4d..b28521d 100644
--- a/src/tint/resolver/builtin_test.cc
+++ b/src/tint/resolver/builtin_test.cc
@@ -67,7 +67,9 @@
 
     // TODO(crbug.com/tint/1581): Once 'abs' is implemented as @const, this will no longer be an
     // error.
-    EXPECT_EQ(r()->error(), R"(12:34 error: 'const' initializer must be const-expression)");
+    EXPECT_EQ(
+        r()->error(),
+        R"(12:34 error: const initializer requires a const-expression, but expression is a runtime-expression)");
 }
 
 // Tests for Logical builtins
diff --git a/src/tint/resolver/resolver.cc b/src/tint/resolver/resolver.cc
index 85adaf8..b8ba0fe 100644
--- a/src/tint/resolver/resolver.cc
+++ b/src/tint/resolver/resolver.cc
@@ -416,6 +416,8 @@
 
     // Does the variable have a constructor?
     if (v->constructor) {
+        ExprEvalStageConstraint constraint{sem::EvaluationStage::kOverride, "override initializer"};
+        TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
         rhs = Materialize(Expression(v->constructor), ty);
         if (!rhs) {
             return nullptr;
@@ -453,8 +455,8 @@
         }
         auto* c = materialize->ConstantValue();
         if (!c) {
-            // TODO(crbug.com/tint/1633): Handle invalid materialization when expressions
-            // are supported.
+            // TODO(crbug.com/tint/1633): Handle invalid materialization when expressions are
+            // supported.
             return nullptr;
         }
 
@@ -493,9 +495,14 @@
         return nullptr;
     }
 
-    const auto* rhs = Expression(c->constructor);
-    if (!rhs) {
-        return nullptr;
+    const sem::Expression* rhs = nullptr;
+    {
+        ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant, "const initializer"};
+        TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
+        rhs = Expression(c->constructor);
+        if (!rhs) {
+            return nullptr;
+        }
     }
 
     if (ty) {
@@ -509,12 +516,6 @@
         ty = rhs->Type();
     }
 
-    const auto value = rhs->ConstantValue();
-    if (!value) {
-        AddError("'const' initializer must be const-expression", c->constructor->source);
-        return nullptr;
-    }
-
     if (!validator_.VariableInitializer(c, ast::AddressSpace::kNone, ty, rhs)) {
         return nullptr;
     }
@@ -525,6 +526,7 @@
         return nullptr;
     }
 
+    const auto value = rhs->ConstantValue();
     auto* sem = is_global ? static_cast<sem::Variable*>(builder_->create<sem::GlobalVariable>(
                                 c, ty, sem::EvaluationStage::kConstant, ast::AddressSpace::kNone,
                                 ast::Access::kUndefined, value, sem::BindingPoint{}, std::nullopt))
@@ -552,6 +554,12 @@
 
     // Does the variable have a constructor?
     if (var->constructor) {
+        ExprEvalStageConstraint constraint{
+            is_global ? sem::EvaluationStage::kOverride : sem::EvaluationStage::kRuntime,
+            "var initializer",
+        };
+        TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
+
         rhs = Materialize(Expression(var->constructor), storage_ty);
         if (!rhs) {
             return nullptr;
@@ -858,16 +866,13 @@
 }
 
 sem::Statement* Resolver::StaticAssert(const ast::StaticAssert* assertion) {
+    ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant, "static assertion"};
+    TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
     auto* expr = Expression(assertion->condition);
     if (!expr) {
         return nullptr;
     }
     auto* cond = expr->ConstantValue();
-    if (!cond) {
-        AddError("static assertion condition must be a const-expression",
-                 assertion->condition->source);
-        return nullptr;
-    }
     if (auto* ty = cond->Type(); !ty->Is<sem::Bool>()) {
         AddError(
             "static assertion condition must be a bool, got '" + builder_->FriendlyName(ty) + "'",
@@ -1458,6 +1463,13 @@
             return nullptr;
         }
 
+        if (auto* constraint = expr_eval_stage_constraint_.constraint) {
+            if (!validator_.EvaluationStage(sem_expr, expr_eval_stage_constraint_.stage,
+                                            constraint)) {
+                return nullptr;
+            }
+        }
+
         builder_->Sem().Add(expr, sem_expr);
         if (expr == root) {
             return sem_expr;
@@ -2818,13 +2830,17 @@
                 // Offset attributes are not part of the WGSL spec, but are emitted
                 // by the SPIR-V reader.
 
+                ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant,
+                                                   "@offset value"};
+                TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
+
                 auto* materialized = Materialize(Expression(o->expr));
                 if (!materialized) {
                     return nullptr;
                 }
                 auto const_value = materialized->ConstantValue();
                 if (!const_value) {
-                    AddError("'offset' must be const-expression", o->expr->source);
+                    AddError("'offset' must be constant expression", o->expr->source);
                     return nullptr;
                 }
                 offset = const_value->As<uint64_t>();
@@ -2836,6 +2852,9 @@
                 align = 1;
                 has_offset_attr = true;
             } else if (auto* a = attr->As<ast::StructMemberAlignAttribute>()) {
+                ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant, "@align"};
+                TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
+
                 auto* materialized = Materialize(Expression(a->expr));
                 if (!materialized) {
                     return nullptr;
@@ -2847,7 +2866,7 @@
 
                 auto const_value = materialized->ConstantValue();
                 if (!const_value) {
-                    AddError("'align' must be const-expression", a->source);
+                    AddError("'align' must be constant expression", a->source);
                     return nullptr;
                 }
                 auto value = const_value->As<AInt>();
@@ -2856,16 +2875,19 @@
                     AddError("'align' value must be a positive, power-of-two integer", a->source);
                     return nullptr;
                 }
-                align = const_value->As<u32>();
+                align = u32(value);
                 has_align_attr = true;
             } else if (auto* s = attr->As<ast::StructMemberSizeAttribute>()) {
+                ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant, "@size"};
+                TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
+
                 auto* materialized = Materialize(Expression(s->expr));
                 if (!materialized) {
                     return nullptr;
                 }
                 auto const_value = materialized->ConstantValue();
                 if (!const_value) {
-                    AddError("'size' must be const-expression", s->expr->source);
+                    AddError("'size' must be constant expression", s->expr->source);
                     return nullptr;
                 }
                 auto value = const_value->As<uint64_t>();
@@ -2876,9 +2898,12 @@
                              s->source);
                     return nullptr;
                 }
-                size = const_value->As<u32>();
+                size = u32(value);
                 has_size_attr = true;
             } else if (auto* l = attr->As<ast::LocationAttribute>()) {
+                ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant, "@location"};
+                TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
+
                 auto* materialize = Materialize(Expression(l->expr));
                 if (!materialize) {
                     return nullptr;
diff --git a/src/tint/resolver/resolver.h b/src/tint/resolver/resolver.h
index 5c413f9..f699996 100644
--- a/src/tint/resolver/resolver.h
+++ b/src/tint/resolver/resolver.h
@@ -414,6 +414,15 @@
     using StructConstructorSig =
         utils::UnorderedKeyWrapper<std::tuple<const sem::Struct*, size_t, sem::EvaluationStage>>;
 
+    /// ExprEvalStageConstraint describes a constraint on when expressions can be evaluated.
+    struct ExprEvalStageConstraint {
+        /// The latest stage that the expression can be evaluated
+        sem::EvaluationStage stage = sem::EvaluationStage::kRuntime;
+        /// The 'thing' that is imposing the contraint. e.g. "var declaration"
+        /// If nullptr, then there is no constraint
+        const char* constraint = nullptr;
+    };
+
     ProgramBuilder* const builder_;
     diag::List& diagnostics_;
     ConstEval const_eval_;
@@ -425,10 +434,10 @@
     std::vector<sem::Function*> entry_points_;
     std::unordered_map<const sem::Type*, const Source&> atomic_composite_info_;
     utils::Bitset<0> marked_;
+    ExprEvalStageConstraint expr_eval_stage_constraint_;
     std::unordered_map<OverrideId, const sem::Variable*> override_ids_;
     std::unordered_map<ArrayConstructorSig, sem::CallTarget*> array_ctors_;
     std::unordered_map<StructConstructorSig, sem::CallTarget*> struct_ctors_;
-
     sem::Function* current_function_ = nullptr;
     sem::Statement* current_statement_ = nullptr;
     sem::CompoundStatement* current_compound_statement_ = nullptr;
diff --git a/src/tint/resolver/static_assert_test.cc b/src/tint/resolver/static_assert_test.cc
index 6d8423d..6e8ecb7 100644
--- a/src/tint/resolver/static_assert_test.cc
+++ b/src/tint/resolver/static_assert_test.cc
@@ -88,7 +88,8 @@
     WrapInFunction(StaticAssert(Expr(Source{{12, 34}}, "V")));
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              "12:34 error: static assertion condition must be a const-expression");
+              "12:34 error: static assertion requires a const-expression, but expression is a "
+              "runtime-expression");
 }
 
 TEST_F(ResolverStaticAssertTest, Local_LessThan_Pass) {
diff --git a/src/tint/resolver/validator.cc b/src/tint/resolver/validator.cc
index e8128bb..3066315 100644
--- a/src/tint/resolver/validator.cc
+++ b/src/tint/resolver/validator.cc
@@ -1391,6 +1391,30 @@
     return true;
 }
 
+bool Validator::EvaluationStage(const sem::Expression* expr,
+                                sem::EvaluationStage latest_stage,
+                                std::string_view constraint) const {
+    if (expr->Stage() > latest_stage) {
+        auto stage_name = [](sem::EvaluationStage stage) -> std::string {
+            switch (stage) {
+                case sem::EvaluationStage::kRuntime:
+                    return "a runtime-expression";
+                case sem::EvaluationStage::kOverride:
+                    return "an override-expression";
+                case sem::EvaluationStage::kConstant:
+                    return "a const-expression";
+            }
+            return "<unknown>";
+        };
+
+        AddError(std::string(constraint) + " requires " + stage_name(latest_stage) +
+                     ", but expression is " + stage_name(expr->Stage()),
+                 expr->Declaration()->source);
+        return false;
+    }
+    return true;
+}
+
 bool Validator::Statements(utils::VectorRef<const ast::Statement*> stmts) const {
     for (auto* stmt : stmts) {
         if (!sem_.Get(stmt)->IsReachable()) {
diff --git a/src/tint/resolver/validator.h b/src/tint/resolver/validator.h
index 2b84ad1..1297a9a 100644
--- a/src/tint/resolver/validator.h
+++ b/src/tint/resolver/validator.h
@@ -25,6 +25,7 @@
 #include "src/tint/ast/pipeline_stage.h"
 #include "src/tint/program_builder.h"
 #include "src/tint/resolver/sem_helper.h"
+#include "src/tint/sem/evaluation_stage.h"
 #include "src/tint/source.h"
 
 // Forward declarations
@@ -209,6 +210,15 @@
     /// @returns true on success, false otherwise
     bool EntryPoint(const sem::Function* func, ast::PipelineStage stage) const;
 
+    /// Validates that the expression must not be evaluated any later than @p latest_stage
+    /// @param expr the expression to check
+    /// @param latest_stage the latest evaluation stage that the expression can be evaluated
+    /// @param constraint the 'thing' that is imposing the contraint. e.g. "var declaration"
+    /// @returns true if @p expr is evaluated in or before @p latest_stage, false otherwise
+    bool EvaluationStage(const sem::Expression* expr,
+                         sem::EvaluationStage latest_stage,
+                         std::string_view constraint) const;
+
     /// Validates a for loop
     /// @param stmt the for loop statement to validate
     /// @returns true on success, false otherwise
diff --git a/src/tint/resolver/variable_validation_test.cc b/src/tint/resolver/variable_validation_test.cc
index b1c7bac..65cfd51 100644
--- a/src/tint/resolver/variable_validation_test.cc
+++ b/src/tint/resolver/variable_validation_test.cc
@@ -423,7 +423,9 @@
     WrapInFunction(v, c);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: 'const' initializer must be const-expression)");
+    EXPECT_EQ(
+        r()->error(),
+        R"(12:34 error: const initializer requires a const-expression, but expression is a runtime-expression)");
 }
 
 TEST_F(ResolverVariableValidationTest, ConstInitWithOverride) {
@@ -432,7 +434,9 @@
     WrapInFunction(c);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: 'const' initializer must be const-expression)");
+    EXPECT_EQ(
+        r()->error(),
+        R"(12:34 error: const initializer requires a const-expression, but expression is an override-expression)");
 }
 
 TEST_F(ResolverVariableValidationTest, ConstInitWithLet) {
@@ -441,7 +445,30 @@
     WrapInFunction(l, c);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: 'const' initializer must be const-expression)");
+    EXPECT_EQ(
+        r()->error(),
+        R"(12:34 error: const initializer requires a const-expression, but expression is a runtime-expression)");
+}
+
+TEST_F(ResolverVariableValidationTest, ConstInitWithRuntimeExpr) {
+    // const c = clamp(2, dpdx(0.5), 3);
+    WrapInFunction(Const("c", Call("clamp", 2_a, Call(Source{{12, 34}}, "dpdx", 0.5_a), 3_a)));
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(
+        r()->error(),
+        R"(12:34 error: const initializer requires a const-expression, but expression is a runtime-expression)");
+}
+
+TEST_F(ResolverVariableValidationTest, ConstInitWithOverrideExpr) {
+    auto* o = Override("v", Expr(1_i));
+    auto* c = Const("c", Add(10_a, Expr(Source{{12, 34}}, o)));
+    WrapInFunction(c);
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(
+        r()->error(),
+        R"(12:34 error: const initializer requires a const-expression, but expression is an override-expression)");
 }
 
 }  // namespace
diff --git a/src/tint/transform/robustness_test.cc b/src/tint/transform/robustness_test.cc
index fa04e47..fe2f343 100644
--- a/src/tint/transform/robustness_test.cc
+++ b/src/tint/transform/robustness_test.cc
@@ -1311,7 +1311,8 @@
 }
 )";
 
-    auto* expect = R"(error: array size is an override-expression, when expected a constant-expression.
+    auto* expect =
+        R"(error: array size is an override-expression, when expected a constant-expression.
 Was the SubstituteOverride transform run?)";
 
     auto got = Run<Robustness>(src);