tint: clean up const eval test framework

- Remove Types variant, and replace with a type-erasing Value class
  instead. This is not only better for compile times, but makes the code
  much easier to understand.
- Value wraps an internal shared_ptr to a const detail::ValueBase,
  allowing it to be used as a value-type (i.e. copyable), while behaving
  polymorphically.
- Add static_asserts to Val, Vec, and Mat creation helpers to emit a
  more useful error message when the wrong type is passed in.

Bug: tint:1581
Change-Id: Icd0d08522bedb3eab12c44efa0d1555ed6e96458
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/111700
Commit-Queue: Antonio Maiorano <amaiorano@google.com>
Reviewed-by: Dan Sinclair <dsinclair@chromium.org>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/src/tint/resolver/const_eval_binary_op_test.cc b/src/tint/resolver/const_eval_binary_op_test.cc
index 70176c7..a35214a 100644
--- a/src/tint/resolver/const_eval_binary_op_test.cc
+++ b/src/tint/resolver/const_eval_binary_op_test.cc
@@ -22,30 +22,26 @@
 namespace tint::resolver {
 namespace {
 
-// Bring in std::ostream& operator<<(std::ostream& o, const Types& types)
-using resolver::operator<<;
-
 struct Case {
     struct Success {
-        Types value;
+        Value value;
     };
     struct Failure {
         std::string error;
     };
 
-    Types lhs;
-    Types rhs;
+    Value lhs;
+    Value rhs;
     utils::Result<Success, Failure> expected;
 };
 
 struct ErrorCase {
-    Types lhs;
-    Types rhs;
+    Value lhs;
+    Value rhs;
 };
 
 /// Creates a Case with Values of any type
-template <typename T, typename U, typename V>
-Case C(Value<T> lhs, Value<U> rhs, Value<V> expected) {
+Case C(Value lhs, Value rhs, Value expected) {
     return Case{std::move(lhs), std::move(rhs), Case::Success{std::move(expected)}};
 }
 
@@ -56,8 +52,7 @@
 }
 
 /// Creates an failure Case with Values of any type
-template <typename T, typename U>
-Case E(Value<T> lhs, Value<U> rhs, std::string error) {
+Case E(Value lhs, Value rhs, std::string error) {
     return Case{std::move(lhs), std::move(rhs), Case::Failure{std::move(error)}};
 }
 
@@ -71,7 +66,7 @@
 static std::ostream& operator<<(std::ostream& o, const Case& c) {
     o << "lhs: " << c.lhs << ", rhs: " << c.rhs << ", expected: ";
     if (c.expected) {
-        auto s = c.expected.Get();
+        auto& s = c.expected.Get();
         o << s.value;
     } else {
         o << "[ERROR: " << c.expected.Failure().error << "]";
@@ -91,15 +86,16 @@
     auto op = std::get<0>(GetParam());
     auto& c = std::get<1>(GetParam());
 
-    auto* lhs_expr = ToValueBase(c.lhs)->Expr(*this);
-    auto* rhs_expr = ToValueBase(c.rhs)->Expr(*this);
+    auto* lhs_expr = c.lhs.Expr(*this);
+    auto* rhs_expr = c.rhs.Expr(*this);
+
     auto* expr = create<ast::BinaryExpression>(Source{{12, 34}}, op, lhs_expr, rhs_expr);
     GlobalConst("C", expr);
 
     if (c.expected) {
         ASSERT_TRUE(r()->Resolve()) << r()->error();
         auto expected_case = c.expected.Get();
-        auto* expected = ToValueBase(expected_case.value);
+        auto& expected = expected_case.value;
 
         auto* sem = Sem().Get(expr);
         const sem::Constant* value = sem->ConstantValue();
@@ -707,7 +703,6 @@
                                                       OpOrIntCases<u32>()))));
 
 TEST_F(ResolverConstEvalTest, NotAndOrOfVecs) {
-    // const C = !((vec2(true, true) & vec2(true, false)) | vec2(false, true));
     auto v1 = Vec(true, true).Expr(*this);
     auto v2 = Vec(true, false).Expr(*this);
     auto v3 = Vec(false, true).Expr(*this);
@@ -978,8 +973,8 @@
 // i32/u32 left shift by >= 32 -> error
 using ResolverConstEvalShiftLeftConcreteGeqBitWidthError = ResolverTestWithParam<ErrorCase>;
 TEST_P(ResolverConstEvalShiftLeftConcreteGeqBitWidthError, Test) {
-    auto* lhs_expr = ToValueBase(GetParam().lhs)->Expr(*this);
-    auto* rhs_expr = ToValueBase(GetParam().rhs)->Expr(*this);
+    auto* lhs_expr = GetParam().lhs.Expr(*this);
+    auto* rhs_expr = GetParam().rhs.Expr(*this);
     GlobalConst("c", Shl(Source{{1, 1}}, lhs_expr, rhs_expr));
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(
@@ -1024,8 +1019,8 @@
 // AInt left shift results in sign change error
 using ResolverConstEvalShiftLeftSignChangeError = ResolverTestWithParam<ErrorCase>;
 TEST_P(ResolverConstEvalShiftLeftSignChangeError, Test) {
-    auto* lhs_expr = ToValueBase(GetParam().lhs)->Expr(*this);
-    auto* rhs_expr = ToValueBase(GetParam().rhs)->Expr(*this);
+    auto* lhs_expr = GetParam().lhs.Expr(*this);
+    auto* rhs_expr = GetParam().rhs.Expr(*this);
     GlobalConst("c", Shl(Source{{1, 1}}, lhs_expr, rhs_expr));
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(), "1:1 error: shift left operation results in sign change");
diff --git a/src/tint/resolver/const_eval_builtin_test.cc b/src/tint/resolver/const_eval_builtin_test.cc
index 112d4e2..dabe7d9 100644
--- a/src/tint/resolver/const_eval_builtin_test.cc
+++ b/src/tint/resolver/const_eval_builtin_test.cc
@@ -22,15 +22,12 @@
 namespace tint::resolver {
 namespace {
 
-// Bring in std::ostream& operator<<(std::ostream& o, const Types& types)
-using resolver::operator<<;
-
 struct Case {
-    Case(utils::VectorRef<Types> in_args, utils::VectorRef<Types> expected_values)
+    Case(utils::VectorRef<Value> in_args, utils::VectorRef<Value> expected_values)
         : args(std::move(in_args)),
           expected(Success{std::move(expected_values), CheckConstantFlags{}}) {}
 
-    Case(utils::VectorRef<Types> in_args, std::string expected_err)
+    Case(utils::VectorRef<Value> in_args, std::string expected_err)
         : args(std::move(in_args)), expected(Failure{std::move(expected_err)}) {}
 
     /// Expected value may be positive or negative
@@ -52,14 +49,14 @@
     }
 
     struct Success {
-        utils::Vector<Types, 2> values;
+        utils::Vector<Value, 2> values;
         CheckConstantFlags flags;
     };
     struct Failure {
         std::string error;
     };
 
-    utils::Vector<Types, 8> args;
+    utils::Vector<Value, 8> args;
     utils::Result<Success, Failure> expected;
 };
 
@@ -94,34 +91,34 @@
 using ScalarTypes = std::variant<AInt, AFloat, u32, i32, f32, f16>;
 
 /// Creates a Case with Values for args and result
-static Case C(std::initializer_list<Types> args, Types result) {
-    return Case{utils::Vector<Types, 8>{args}, utils::Vector<Types, 2>{std::move(result)}};
+static Case C(std::initializer_list<Value> args, Value result) {
+    return Case{utils::Vector<Value, 8>{args}, utils::Vector<Value, 2>{std::move(result)}};
 }
 
 /// Creates a Case with Values for args and result
-static Case C(std::initializer_list<Types> args, std::initializer_list<Types> results) {
-    return Case{utils::Vector<Types, 8>{args}, utils::Vector<Types, 2>{results}};
+static Case C(std::initializer_list<Value> args, std::initializer_list<Value> results) {
+    return Case{utils::Vector<Value, 8>{args}, utils::Vector<Value, 2>{results}};
 }
 
 /// Convenience overload that creates a Case with just scalars
 static Case C(std::initializer_list<ScalarTypes> sargs, ScalarTypes sresult) {
-    utils::Vector<Types, 8> args;
+    utils::Vector<Value, 8> args;
     for (auto& sa : sargs) {
         std::visit([&](auto&& v) { return args.Push(Val(v)); }, sa);
     }
-    Types result = Val(0_a);
+    Value result = Val(0_a);
     std::visit([&](auto&& v) { result = Val(v); }, sresult);
-    return Case{std::move(args), utils::Vector<Types, 2>{std::move(result)}};
+    return Case{std::move(args), utils::Vector<Value, 2>{std::move(result)}};
 }
 
 /// Creates a Case with Values for args and result
 static Case C(std::initializer_list<ScalarTypes> sargs,
               std::initializer_list<ScalarTypes> sresults) {
-    utils::Vector<Types, 8> args;
+    utils::Vector<Value, 8> args;
     for (auto& sa : sargs) {
         std::visit([&](auto&& v) { return args.Push(Val(v)); }, sa);
     }
-    utils::Vector<Types, 2> results;
+    utils::Vector<Value, 2> results;
     for (auto& sa : sresults) {
         std::visit([&](auto&& v) { return results.Push(Val(v)); }, sa);
     }
@@ -129,13 +126,13 @@
 }
 
 /// Creates a Case with Values for args and expected error
-static Case E(std::initializer_list<Types> args, std::string err) {
-    return Case{utils::Vector<Types, 8>{args}, std::move(err)};
+static Case E(std::initializer_list<Value> args, std::string err) {
+    return Case{utils::Vector<Value, 8>{args}, std::move(err)};
 }
 
 /// Convenience overload that creates an expected-error Case with just scalars
 static Case E(std::initializer_list<ScalarTypes> sargs, std::string err) {
-    utils::Vector<Types, 8> args;
+    utils::Vector<Value, 8> args;
     for (auto& sa : sargs) {
         std::visit([&](auto&& v) { return args.Push(Val(v)); }, sa);
     }
@@ -152,7 +149,7 @@
 
     utils::Vector<const ast::Expression*, 8> args;
     for (auto& a : c.args) {
-        std::visit([&](auto&& v) { args.Push(v.Expr(*this)); }, a);
+        args.Push(a.Expr(*this));
     }
 
     auto* expr = Call(Source{{12, 34}}, sem::str(builtin), std::move(args));
@@ -173,14 +170,13 @@
             // The result type of the constant-evaluated expression is a structure.
             // Compare each of the fields individually.
             for (size_t i = 0; i < expected_case.values.Length(); i++) {
-                CheckConstant(value->Index(i), ToValueBase(expected_case.values[i]),
-                              expected_case.flags);
+                CheckConstant(value->Index(i), expected_case.values[i], expected_case.flags);
             }
         } else {
             // Return type is not a structure. Just compare the single value
             ASSERT_EQ(expected_case.values.Length(), 1u)
                 << "const-eval returned non-struct, but Case expected multiple values";
-            CheckConstant(value, ToValueBase(expected_case.values[0]), expected_case.flags);
+            CheckConstant(value, expected_case.values[0], expected_case.flags);
         }
     } else {
         EXPECT_FALSE(r()->Resolve());
diff --git a/src/tint/resolver/const_eval_conversion_test.cc b/src/tint/resolver/const_eval_conversion_test.cc
index ed68725..da37f3b 100644
--- a/src/tint/resolver/const_eval_conversion_test.cc
+++ b/src/tint/resolver/const_eval_conversion_test.cc
@@ -19,19 +19,6 @@
 namespace tint::resolver {
 namespace {
 
-using Scalar = std::variant<  //
-    builder::Value<AInt>,
-    builder::Value<AFloat>,
-    builder::Value<u32>,
-    builder::Value<i32>,
-    builder::Value<f32>,
-    builder::Value<f16>,
-    builder::Value<bool>>;
-
-static std::ostream& operator<<(std::ostream& o, const Scalar& scalar) {
-    return ToValueBase(scalar)->Print(o);
-}
-
 enum class Kind {
     kScalar,
     kVector,
@@ -48,8 +35,8 @@
 }
 
 struct Case {
-    Scalar input;
-    Scalar expected;
+    Value input;
+    Value expected;
     builder::CreatePtrs type;
     bool unrepresentable = false;
 };
@@ -65,7 +52,7 @@
 
 template <typename TO, typename FROM>
 Case Success(FROM input, TO expected) {
-    return {builder::Val(input), builder::Val(expected), builder::CreatePtrsFor<TO>()};
+    return {Val(input), Val(expected), builder::CreatePtrsFor<TO>()};
 }
 
 template <typename TO, typename FROM>
@@ -83,7 +70,7 @@
     const auto& type = std::get<1>(GetParam()).type;
     const auto unrepresentable = std::get<1>(GetParam()).unrepresentable;
 
-    auto* input_val = ToValueBase(input)->Expr(*this);
+    auto* input_val = input.Expr(*this);
     auto* expr = Construct(type.ast(*this), input_val);
     if (kind == Kind::kVector) {
         expr = Construct(ty.vec(nullptr, 3), expr);
@@ -107,7 +94,7 @@
         ASSERT_NE(sem->ConstantValue(), nullptr);
         EXPECT_TYPE(sem->ConstantValue()->Type(), target_sem_ty);
 
-        auto expected_values = ToValueBase(expected)->Args();
+        auto expected_values = expected.Args();
         if (kind == Kind::kVector) {
             expected_values.values.Push(expected_values.values[0]);
             expected_values.values.Push(expected_values.values[0]);
diff --git a/src/tint/resolver/const_eval_test.h b/src/tint/resolver/const_eval_test.h
index 1a5ff8c..dcb91d0 100644
--- a/src/tint/resolver/const_eval_test.h
+++ b/src/tint/resolver/const_eval_test.h
@@ -88,10 +88,10 @@
 /// @param expected_value the expected value for the test
 /// @param flags optional flags for controlling the comparisons
 inline void CheckConstant(const sem::Constant* got_constant,
-                          const builder::ValueBase* expected_value,
+                          const builder::Value& expected_value,
                           CheckConstantFlags flags = {}) {
     auto values_flat = ScalarArgsFrom(got_constant);
-    auto expected_values_flat = expected_value->Args();
+    auto expected_values_flat = expected_value.Args();
     ASSERT_EQ(values_flat.values.Length(), expected_values_flat.values.Length());
     for (size_t i = 0; i < values_flat.values.Length(); ++i) {
         auto& got_scalar = values_flat.values[i];
@@ -247,93 +247,8 @@
 using builder::Mat;
 using builder::Val;
 using builder::Value;
-using builder::ValueBase;
 using builder::Vec;
 
-using Types = std::variant<  //
-    Value<AInt>,
-    Value<AFloat>,
-    Value<u32>,
-    Value<i32>,
-    Value<f32>,
-    Value<f16>,
-    Value<bool>,
-
-    Value<builder::vec2<AInt>>,
-    Value<builder::vec2<AFloat>>,
-    Value<builder::vec2<u32>>,
-    Value<builder::vec2<i32>>,
-    Value<builder::vec2<f32>>,
-    Value<builder::vec2<f16>>,
-    Value<builder::vec2<bool>>,
-
-    Value<builder::vec3<AInt>>,
-    Value<builder::vec3<AFloat>>,
-    Value<builder::vec3<u32>>,
-    Value<builder::vec3<i32>>,
-    Value<builder::vec3<f32>>,
-    Value<builder::vec3<f16>>,
-    Value<builder::vec3<bool>>,
-
-    Value<builder::vec4<AInt>>,
-    Value<builder::vec4<AFloat>>,
-    Value<builder::vec4<u32>>,
-    Value<builder::vec4<i32>>,
-    Value<builder::vec4<f32>>,
-    Value<builder::vec4<f16>>,
-    Value<builder::vec4<bool>>,
-
-    Value<builder::mat2x2<AInt>>,
-    Value<builder::mat2x2<AFloat>>,
-    Value<builder::mat2x2<f32>>,
-    Value<builder::mat2x2<f16>>,
-
-    Value<builder::mat3x3<AInt>>,
-    Value<builder::mat3x3<AFloat>>,
-    Value<builder::mat3x3<f32>>,
-    Value<builder::mat3x3<f16>>,
-
-    Value<builder::mat4x4<AInt>>,
-    Value<builder::mat4x4<AFloat>>,
-    Value<builder::mat4x4<f32>>,
-    Value<builder::mat4x4<f16>>,
-
-    Value<builder::mat2x3<AInt>>,
-    Value<builder::mat2x3<AFloat>>,
-    Value<builder::mat2x3<f32>>,
-    Value<builder::mat2x3<f16>>,
-
-    Value<builder::mat3x2<AInt>>,
-    Value<builder::mat3x2<AFloat>>,
-    Value<builder::mat3x2<f32>>,
-    Value<builder::mat3x2<f16>>,
-
-    Value<builder::mat2x4<AInt>>,
-    Value<builder::mat2x4<AFloat>>,
-    Value<builder::mat2x4<f32>>,
-    Value<builder::mat2x4<f16>>,
-
-    Value<builder::mat4x2<AInt>>,
-    Value<builder::mat4x2<AFloat>>,
-    Value<builder::mat4x2<f32>>,
-    Value<builder::mat4x2<f16>>
-    //
-    >;
-
-/// Returns the current Value<T> in the `types` variant as a `ValueBase` pointer to use the
-/// polymorphic API. This trades longer compile times using std::variant for longer runtime via
-/// virtual function calls.
-template <typename ValueVariant>
-inline const ValueBase* ToValueBase(const ValueVariant& types) {
-    return std::visit(
-        [](auto&& t) -> const ValueBase* { return static_cast<const ValueBase*>(&t); }, types);
-}
-
-/// Prints Types to ostream
-inline std::ostream& operator<<(std::ostream& o, const Types& types) {
-    return ToValueBase(types)->Print(o);
-}
-
 // Calls `f` on deepest elements of both `a` and `b`. If function returns Action::kStop, it stops
 // traversing, and return Action::kStop; if the function returns Action::kContinue, it continues and
 // returns Action::kContinue when done.
diff --git a/src/tint/resolver/const_eval_unary_op_test.cc b/src/tint/resolver/const_eval_unary_op_test.cc
index fced490..d24c27b 100644
--- a/src/tint/resolver/const_eval_unary_op_test.cc
+++ b/src/tint/resolver/const_eval_unary_op_test.cc
@@ -19,22 +19,17 @@
 namespace tint::resolver {
 namespace {
 
-// Bring in std::ostream& operator<<(std::ostream& o, const Types& types)
-using resolver::operator<<;
-
 struct Case {
-    Types input;
-    Types expected;
+    Value input;
+    Value expected;
 };
 
 static std::ostream& operator<<(std::ostream& o, const Case& c) {
     o << "input: " << c.input << ", expected: " << c.expected;
     return o;
 }
-
-/// Creates a Case with Values of any type
-template <typename T, typename U>
-Case C(Value<T> input, Value<U> expected) {
+// Creates a Case with Values of any type
+Case C(Value input, Value expected) {
     return Case{std::move(input), std::move(expected)};
 }
 
@@ -52,10 +47,10 @@
     auto op = std::get<0>(GetParam());
     auto& c = std::get<1>(GetParam());
 
-    auto* expected = ToValueBase(c.expected);
-    auto* input = ToValueBase(c.input);
+    auto& expected = c.expected;
+    auto& input = c.input;
 
-    auto* input_expr = input->Expr(*this);
+    auto* input_expr = input.Expr(*this);
     auto* expr = create<ast::UnaryOpExpression>(op, input_expr);
 
     GlobalConst("C", expr);
@@ -67,13 +62,13 @@
     EXPECT_TYPE(value->Type(), sem->Type());
 
     auto values_flat = ScalarArgsFrom(value);
-    auto expected_values_flat = expected->Args();
+    auto expected_values_flat = expected.Args();
     ASSERT_EQ(values_flat.values.Length(), expected_values_flat.values.Length());
     for (size_t i = 0; i < values_flat.values.Length(); ++i) {
         auto& a = values_flat.values[i];
         auto& b = expected_values_flat.values[i];
         EXPECT_EQ(a, b);
-        if (expected->IsIntegral()) {
+        if (expected.IsIntegral()) {
             // Check that the constant's integer doesn't contain unexpected
             // data in the MSBs that are outside of the bit-width of T.
             EXPECT_EQ(builder::As<AInt>(a), builder::As<AInt>(b));
diff --git a/src/tint/resolver/resolver_test_helper.h b/src/tint/resolver/resolver_test_helper.h
index 41b08b3..edbc456 100644
--- a/src/tint/resolver/resolver_test_helper.h
+++ b/src/tint/resolver/resolver_test_helper.h
@@ -241,12 +241,21 @@
 using sem_type_func_ptr = const sem::Type* (*)(ProgramBuilder& b);
 using type_name_func_ptr = std::string (*)();
 
+struct UnspecializedElementType {};
+
+/// Base template for DataType, specialized below.
 template <typename T>
-struct DataType {};
+struct DataType {
+    /// The element type
+    using ElementType = UnspecializedElementType;
+};
 
 /// Helper that represents no-type. Returns nullptr for all static methods.
 template <>
 struct DataType<void> {
+    /// The element type
+    using ElementType = void;
+
     /// @return nullptr
     static inline const ast::Type* AST(ProgramBuilder&) { return nullptr; }
     /// @return nullptr
@@ -762,7 +771,13 @@
             DataType<T>::Name};
 }
 
-/// Base class for Value<T>
+/// True if DataType<T> is specialized for T, false otherwise.
+template <typename T>
+const bool IsDataTypeSpecializedFor =
+    !std::is_same_v<typename DataType<T>::ElementType, UnspecializedElementType>;
+
+namespace detail {
+/// ValueBase is a base class of ConcreteValue<T>
 struct ValueBase {
     /// Constructor
     ValueBase() = default;
@@ -793,13 +808,12 @@
     virtual std::ostream& Print(std::ostream& o) const = 0;
 };
 
-/// Value<T> is an instance of a value of type DataType<T>. Useful for storing values to create
-/// expressions with.
+/// ConcreteValue<T> is used to create Values of type DataType<T> with a ScalarArgs initializer.
 template <typename T>
-struct Value : ValueBase {
+struct ConcreteValue : ValueBase {
     /// Constructor
-    /// @param a the scalar args
-    explicit Value(ScalarArgs a) : args(std::move(a)) {}
+    /// @param args the scalar args
+    explicit ConcreteValue(ScalarArgs args) : args_(std::move(args)) {}
 
     /// Alias to T
     using Type = T;
@@ -808,21 +822,16 @@
     /// Alias to DataType::ElementType
     using ElementType = typename DataType::ElementType;
 
-    /// Creates a Value<T> with `args`
-    /// @param args the args that will be passed to the expression
-    /// @returns a Value<T>
-    static Value Create(ScalarArgs args) { return Value{std::move(args)}; }
-
     /// Creates an `ast::Expression` for the type T passing in previously stored args
     /// @param b the ProgramBuilder
     /// @returns an expression node
     const ast::Expression* Expr(ProgramBuilder& b) const override {
         auto create = CreatePtrsFor<T>();
-        return (*create.expr)(b, args);
+        return (*create.expr)(b, args_);
     }
 
     /// @returns args used to create expression via `Expr`
-    const ScalarArgs& Args() const override { return args; }
+    const ScalarArgs& Args() const override { return args_; }
 
     /// @returns true if element type is abstract
     bool IsAbstract() const override { return tint::IsAbstract<ElementType>; }
@@ -838,9 +847,9 @@
     /// @returns input argument `o`
     std::ostream& Print(std::ostream& o) const override {
         o << TypeName() << "(";
-        for (auto& a : args.values) {
+        for (auto& a : args_.values) {
             o << std::get<ElementType>(a);
-            if (&a != &args.values.Back()) {
+            if (&a != &args_.values.Back()) {
                 o << ", ";
             }
         }
@@ -848,60 +857,95 @@
         return o;
     }
 
+  private:
     /// args to create expression with
-    ScalarArgs args;
+    ScalarArgs args_;
 };
-
-namespace detail {
-/// Base template for IsValue
-template <typename T>
-struct IsValue : std::false_type {};
-/// Specialization for IsValue
-template <typename T>
-struct IsValue<Value<T>> : std::true_type {};
 }  // namespace detail
 
-/// True if T is of type Value
-template <typename T>
-constexpr bool IsValue = detail::IsValue<T>::value;
+/// A Value represents a value of type DataType<T> created with ScalarArgs. Useful for storing
+/// values for unit tests.
+class Value {
+  public:
+    /// Creates a Value for type T initialized with `args`
+    /// @param args the scalar args
+    /// @returns Value
+    template <typename T>
+    static Value Create(ScalarArgs args) {
+        static_assert(IsDataTypeSpecializedFor<T>, "No DataType<T> specialization exists");
+        return Value{std::make_shared<detail::ConcreteValue<T>>(std::move(args))};
+    }
 
-/// Returns the friendly name of ValueT
-template <typename ValueT, typename = traits::EnableIf<IsValue<ValueT>>>
-const char* FriendlyName() {
-    return tint::FriendlyName<typename ValueT::ElementType>();
+    /// Creates an `ast::Expression` for the type T passing in previously stored args
+    /// @param b the ProgramBuilder
+    /// @returns an expression node
+    const ast::Expression* Expr(ProgramBuilder& b) const { return value_->Expr(b); }
+
+    /// @returns args used to create expression via `Expr`
+    const ScalarArgs& Args() const { return value_->Args(); }
+
+    /// @returns true if element type is abstract
+    bool IsAbstract() const { return value_->IsAbstract(); }
+
+    /// @returns true if element type is an integral
+    bool IsIntegral() const { return value_->IsIntegral(); }
+
+    /// @returns element type name
+    std::string TypeName() const { return value_->TypeName(); }
+
+    /// Prints this value to the output stream
+    /// @param o the output stream
+    /// @returns input argument `o`
+    std::ostream& Print(std::ostream& o) const { return value_->Print(o); }
+
+  private:
+    /// Private constructor
+    explicit Value(std::shared_ptr<const detail::ValueBase> value) : value_(std::move(value)) {}
+
+    /// Shared pointer to an immutable value. This type-erasure pattern allows Value to wrap a
+    /// polymorphic type, while being used like a value-type (i.e. copyable).
+    std::shared_ptr<const detail::ValueBase> value_;
+};
+
+/// Prints Value to ostream
+inline std::ostream& operator<<(std::ostream& o, const Value& value) {
+    return value.Print(o);
 }
 
-/// Creates a `Value<T>` from a scalar `v`
+/// True if T is Value, false otherwise
 template <typename T>
-auto Val(T v) {
-    return Value<T>::Create(ScalarArgs{v});
+constexpr bool IsValue = std::is_same_v<T, Value>;
+
+/// Creates a Value of DataType<T> from a scalar `v`
+template <typename T>
+Value Val(T v) {
+    return Value::Create<T>(ScalarArgs{v});
 }
 
-/// Creates a `Value<vec<N, T>>` from N scalar `args`
+/// Creates a Value of DataType<vec<N, T>> from N scalar `args`
 template <typename... T>
-auto Vec(T... args) {
-    constexpr size_t N = sizeof...(args);
+Value Vec(T... args) {
     using FirstT = std::tuple_element_t<0, std::tuple<T...>>;
+    constexpr size_t N = sizeof...(args);
     utils::Vector v{args...};
-    using VT = vec<N, FirstT>;
-    return Value<VT>::Create(utils::VectorRef<FirstT>{v});
+    return Value::Create<vec<N, FirstT>>(utils::VectorRef<FirstT>{v});
 }
 
-/// Creates a `Value<mat<C,R,T>` from C*R scalar `args`
+/// Creates a Value of DataType<mat<C,R,T> from C*R scalar `args`
 template <size_t C, size_t R, typename T>
-auto Mat(const T (&m_in)[C][R]) {
+Value Mat(const T (&m_in)[C][R]) {
     utils::Vector<T, C * R> m;
     for (uint32_t i = 0; i < C; ++i) {
         for (size_t j = 0; j < R; ++j) {
             m.Push(m_in[i][j]);
         }
     }
-    return Value<mat<C, R, T>>::Create(utils::VectorRef<T>{m});
+    return Value::Create<mat<C, R, T>>(utils::VectorRef<T>{m});
 }
 
-/// Creates a `Value<mat<2,R,T>` from column vectors `c0` and `c1`
+/// Creates a Value of DataType<mat<2,R,T> from column vectors `c0` and `c1`
 template <typename T, size_t R>
-auto Mat(const T (&c0)[R], const T (&c1)[R]) {
+Value Mat(const T (&c0)[R], const T (&c1)[R]) {
     constexpr size_t C = 2;
     utils::Vector<T, C * R> m;
     for (auto v : c0) {
@@ -910,12 +954,12 @@
     for (auto v : c1) {
         m.Push(v);
     }
-    return Value<mat<C, R, T>>::Create(utils::VectorRef<T>{m});
+    return Value::Create<mat<C, R, T>>(utils::VectorRef<T>{m});
 }
 
-/// Creates a `Value<mat<3,R,T>` from column vectors `c0`, `c1`, and `c2`
+/// Creates a Value of DataType<mat<3,R,T> from column vectors `c0`, `c1`, and `c2`
 template <typename T, size_t R>
-auto Mat(const T (&c0)[R], const T (&c1)[R], const T (&c2)[R]) {
+Value Mat(const T (&c0)[R], const T (&c1)[R], const T (&c2)[R]) {
     constexpr size_t C = 3;
     utils::Vector<T, C * R> m;
     for (auto v : c0) {
@@ -927,12 +971,12 @@
     for (auto v : c2) {
         m.Push(v);
     }
-    return Value<mat<C, R, T>>::Create(utils::VectorRef<T>{m});
+    return Value::Create<mat<C, R, T>>(utils::VectorRef<T>{m});
 }
 
-/// Creates a `Value<mat<4,R,T>` from column vectors `c0`, `c1`, `c2`, and `c3`
+/// Creates a Value of DataType<mat<4,R,T> from column vectors `c0`, `c1`, `c2`, and `c3`
 template <typename T, size_t R>
-auto Mat(const T (&c0)[R], const T (&c1)[R], const T (&c2)[R], const T (&c3)[R]) {
+Value Mat(const T (&c0)[R], const T (&c1)[R], const T (&c2)[R], const T (&c3)[R]) {
     constexpr size_t C = 4;
     utils::Vector<T, C * R> m;
     for (auto v : c0) {
@@ -947,7 +991,7 @@
     for (auto v : c3) {
         m.Push(v);
     }
-    return Value<mat<C, R, T>>::Create(utils::VectorRef<T>{m});
+    return Value::Create<mat<C, R, T>>(utils::VectorRef<T>{m});
 }
 
 }  // namespace builder