[tint][utils] Add more string helpers

Add utils::Split(), utils::Join() for splitting and joining strings with
delimiters.

Add options to SuggestAlternatives().

Add Quote() for surrounding a string with '.

Requires some include shuffling and minor tweaks to other utilities.

Change-Id: If75058e68cd986450fecc0b27bcc9a94c765a665
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/134580
Commit-Queue: Ben Clayton <bclayton@google.com>
Reviewed-by: James Price <jrprice@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
Auto-Submit: Ben Clayton <bclayton@google.com>
diff --git a/src/tint/ast/transform/renamer_test.cc b/src/tint/ast/transform/renamer_test.cc
index 4e97c6f..c93340d 100644
--- a/src/tint/ast/transform/renamer_test.cc
+++ b/src/tint/ast/transform/renamer_test.cc
@@ -22,6 +22,7 @@
 #include "src/tint/ast/transform/test_helper.h"
 #include "src/tint/builtin/builtin.h"
 #include "src/tint/builtin/texel_format.h"
+#include "src/tint/utils/string.h"
 
 namespace tint::ast::transform {
 namespace {
diff --git a/src/tint/bench/benchmark.cc b/src/tint/bench/benchmark.cc
index 4804a71..115d0b2 100644
--- a/src/tint/bench/benchmark.cc
+++ b/src/tint/bench/benchmark.cc
@@ -19,6 +19,7 @@
 #include <utility>
 #include <vector>
 
+#include "src/tint/utils/string.h"
 #include "src/tint/utils/string_stream.h"
 
 namespace tint::bench {
diff --git a/src/tint/resolver/dependency_graph.cc b/src/tint/resolver/dependency_graph.cc
index 5ba9ee2..504e8ac 100644
--- a/src/tint/resolver/dependency_graph.cc
+++ b/src/tint/resolver/dependency_graph.cc
@@ -66,6 +66,7 @@
 #include "src/tint/utils/defer.h"
 #include "src/tint/utils/map.h"
 #include "src/tint/utils/scoped_assignment.h"
+#include "src/tint/utils/string.h"
 #include "src/tint/utils/string_stream.h"
 #include "src/tint/utils/unique_vector.h"
 
diff --git a/src/tint/resolver/resolver.cc b/src/tint/resolver/resolver.cc
index 09d8464..fb06435 100644
--- a/src/tint/resolver/resolver.cc
+++ b/src/tint/resolver/resolver.cc
@@ -3761,8 +3761,9 @@
             } else {
                 utils::StringStream ss;
                 ss << "unrecognized diagnostic rule 'chromium." << name << "'\n";
-                utils::SuggestAlternatives(name, builtin::kChromiumDiagnosticRuleStrings, ss,
-                                           "chromium.");
+                utils::SuggestAlternativeOptions opts;
+                opts.prefix = "chromium.";
+                utils::SuggestAlternatives(name, builtin::kChromiumDiagnosticRuleStrings, ss, opts);
                 AddWarning(ss.str(), control.rule_name->source);
             }
         }
diff --git a/src/tint/utils/string.cc b/src/tint/utils/string.cc
index 67eaf2b..00b51a8 100644
--- a/src/tint/utils/string.cc
+++ b/src/tint/utils/string.cc
@@ -15,6 +15,7 @@
 #include <algorithm>
 
 #include "src/tint/utils/string.h"
+#include "src/tint/utils/transform.h"
 #include "src/tint/utils/vector.h"
 
 namespace tint::utils {
@@ -51,33 +52,46 @@
 void SuggestAlternatives(std::string_view got,
                          Slice<char const* const> strings,
                          utils::StringStream& ss,
-                         std::string_view prefix /* = "" */) {
+                         const SuggestAlternativeOptions& options /* = {} */) {
+    auto views = Transform<8>(strings, [](char const* const str) { return std::string_view(str); });
+    SuggestAlternatives(got, views.Slice(), ss, options);
+}
+
+void SuggestAlternatives(std::string_view got,
+                         Slice<std::string_view> strings,
+                         utils::StringStream& ss,
+                         const SuggestAlternativeOptions& options /* = {} */) {
     // If the string typed was within kSuggestionDistance of one of the possible enum values,
     // suggest that. Don't bother with suggestions if the string was extremely long.
     constexpr size_t kSuggestionDistance = 5;
     constexpr size_t kSuggestionMaxLength = 64;
     if (!got.empty() && got.size() < kSuggestionMaxLength) {
         size_t candidate_dist = kSuggestionDistance;
-        const char* candidate = nullptr;
-        for (auto* str : strings) {
+        std::string_view candidate;
+        for (auto str : strings) {
             auto dist = utils::Distance(str, got);
             if (dist < candidate_dist) {
                 candidate = str;
                 candidate_dist = dist;
             }
         }
-        if (candidate) {
-            ss << "Did you mean '" << prefix << candidate << "'?\n";
+        if (!candidate.empty()) {
+            ss << "Did you mean '" << options.prefix << candidate << "'?";
+            if (options.list_possible_values) {
+                ss << "\n";
+            }
         }
     }
 
-    // List all the possible enumerator values
-    ss << "Possible values: ";
-    for (auto* str : strings) {
-        if (str != strings[0]) {
-            ss << ", ";
+    if (options.list_possible_values) {
+        // List all the possible enumerator values
+        ss << "Possible values: ";
+        for (auto str : strings) {
+            if (str != strings[0]) {
+                ss << ", ";
+            }
+            ss << "'" << options.prefix << str << "'";
         }
-        ss << "'" << prefix << str << "'";
     }
 }
 
diff --git a/src/tint/utils/string.h b/src/tint/utils/string.h
index e731365..99c930a 100644
--- a/src/tint/utils/string.h
+++ b/src/tint/utils/string.h
@@ -20,6 +20,7 @@
 
 #include "src/tint/utils/slice.h"
 #include "src/tint/utils/string_stream.h"
+#include "src/tint/utils/vector.h"
 
 namespace tint::utils {
 
@@ -38,6 +39,12 @@
     return str;
 }
 
+/// @param value the boolean value to be printed as a string
+/// @returns value printed as a string via the stream `<<` operator
+inline std::string ToString(bool value) {
+    return value ? "true" : "false";
+}
+
 /// @param value the value to be printed as a string
 /// @returns value printed as a string via the stream `<<` operator
 template <typename T>
@@ -75,15 +82,33 @@
 /// @returns the Levenshtein distance between @p a and @p b
 size_t Distance(std::string_view a, std::string_view b);
 
+/// Options for SuggestAlternatives()
+struct SuggestAlternativeOptions {
+    /// The prefix to apply to the strings when printing
+    std::string_view prefix;
+    /// List all the possible values
+    bool list_possible_values = true;
+};
+
 /// Suggest alternatives for an unrecognized string from a list of possible values.
 /// @param got the unrecognized string
 /// @param strings the list of possible values
 /// @param ss the stream to write the suggest and list of possible values to
-/// @param prefix the prefix to apply to the strings when printing (optional)
+/// @param options options for the suggestion
 void SuggestAlternatives(std::string_view got,
                          Slice<char const* const> strings,
                          utils::StringStream& ss,
-                         std::string_view prefix = "");
+                         const SuggestAlternativeOptions& options = {});
+
+/// Suggest alternatives for an unrecognized string from a list of possible values.
+/// @param got the unrecognized string
+/// @param strings the list of possible values
+/// @param ss the stream to write the suggest and list of possible values to
+/// @param options options for the suggestion
+void SuggestAlternatives(std::string_view got,
+                         Slice<std::string_view> strings,
+                         utils::StringStream& ss,
+                         const SuggestAlternativeOptions& options = {});
 
 /// @param str the input string
 /// @param pred the predicate function
@@ -150,6 +175,51 @@
     return Trim(str, IsSpace);
 }
 
+/// @param str the input string
+/// @param delimiter the delimiter
+/// @return @p str split at each occurrence of @p delimiter
+inline utils::Vector<std::string_view, 8> Split(std::string_view str, std::string_view delimiter) {
+    utils::Vector<std::string_view, 8> out;
+    while (str.length() > delimiter.length()) {
+        auto pos = str.find(delimiter);
+        if (pos == std::string_view::npos) {
+            break;
+        }
+        out.Push(str.substr(0, pos));
+        str = str.substr(pos + delimiter.length());
+    }
+    out.Push(str);
+    return out;
+}
+
+/// @returns @p str quoted with <code>'</code>
+inline std::string Quote(std::string_view str) {
+    return "'" + std::string(str) + "'";
+}
+
+/// @param parts the input parts
+/// @param delimiter the delimiter
+/// @return @p parts joined as a string, delimited with @p delimiter
+template <typename T>
+inline std::string Join(utils::VectorRef<T> parts, std::string_view delimiter) {
+    utils::StringStream s;
+    for (auto& part : parts) {
+        if (part != parts.Front()) {
+            s << delimiter;
+        }
+        s << part;
+    }
+    return s.str();
+}
+
+/// @param parts the input parts
+/// @param delimiter the delimiter
+/// @return @p parts joined as a string, delimited with @p delimiter
+template <typename T, size_t N>
+inline std::string Join(const utils::Vector<T, N>& parts, std::string_view delimiter) {
+    return Join(utils::VectorRef<T>(parts), delimiter);
+}
+
 }  // namespace tint::utils
 
 #endif  // SRC_TINT_UTILS_STRING_H_
diff --git a/src/tint/utils/string_stream.h b/src/tint/utils/string_stream.h
index 7cbce09..ecb88f7 100644
--- a/src/tint/utils/string_stream.h
+++ b/src/tint/utils/string_stream.h
@@ -24,6 +24,7 @@
 #include <utility>
 
 #include "src/tint/utils/unicode.h"
+#include "src/tint/utils/vector.h"
 
 namespace tint::utils {
 
@@ -189,6 +190,44 @@
 /// @returns out so calls can be chained
 utils::StringStream& operator<<(utils::StringStream& out, CodePoint codepoint);
 
+/// Prints the vector @p vec to @p o
+/// @param o the stream to write to
+/// @param vec the vector
+/// @return the stream so calls can be chained
+template <typename T, size_t N>
+inline utils::StringStream& operator<<(utils::StringStream& o, const utils::Vector<T, N>& vec) {
+    o << "[";
+    bool first = true;
+    for (auto& el : vec) {
+        if (!first) {
+            o << ", ";
+        }
+        first = false;
+        o << el;
+    }
+    o << "]";
+    return o;
+}
+
+/// Prints the vector @p vec to @p o
+/// @param o the stream to write to
+/// @param vec the vector reference
+/// @return the stream so calls can be chained
+template <typename T>
+inline utils::StringStream& operator<<(utils::StringStream& o, utils::VectorRef<T> vec) {
+    o << "[";
+    bool first = true;
+    for (auto& el : vec) {
+        if (!first) {
+            o << ", ";
+        }
+        first = false;
+        o << el;
+    }
+    o << "]";
+    return o;
+}
+
 }  // namespace tint::utils
 
 #endif  // SRC_TINT_UTILS_STRING_STREAM_H_
diff --git a/src/tint/utils/string_test.cc b/src/tint/utils/string_test.cc
index 9cf8370..676c341 100644
--- a/src/tint/utils/string_test.cc
+++ b/src/tint/utils/string_test.cc
@@ -14,7 +14,7 @@
 
 #include "src/tint/utils/string.h"
 
-#include "gtest/gtest.h"
+#include "gmock/gmock.h"
 #include "src/tint/utils/string_stream.h"
 
 namespace tint::utils {
@@ -34,6 +34,8 @@
 }
 
 TEST(StringTest, ToString) {
+    EXPECT_EQ("true", ToString(true));
+    EXPECT_EQ("false", ToString(false));
     EXPECT_EQ("123", ToString(123));
     EXPECT_EQ("hello", ToString("hello"));
 }
@@ -82,6 +84,23 @@
         SuggestAlternatives("hello world", alternatives, ss);
         EXPECT_EQ(ss.str(), R"(Possible values: 'foobar', 'something else')");
     }
+    {
+        const char* alternatives[] = {"hello world", "Hello World"};
+        utils::StringStream ss;
+        SuggestAlternativeOptions opts;
+        opts.prefix = "$";
+        SuggestAlternatives("hello wordl", alternatives, ss, opts);
+        EXPECT_EQ(ss.str(), R"(Did you mean '$hello world'?
+Possible values: '$hello world', '$Hello World')");
+    }
+    {
+        const char* alternatives[] = {"hello world", "Hello World"};
+        utils::StringStream ss;
+        SuggestAlternativeOptions opts;
+        opts.list_possible_values = false;
+        SuggestAlternatives("hello world", alternatives, ss, opts);
+        EXPECT_EQ(ss.str(), R"(Did you mean 'hello world'?)");
+    }
 }
 
 TEST(StringTest, TrimLeft) {
@@ -153,5 +172,27 @@
     EXPECT_EQ(TrimSpace(""), "");
 }
 
+TEST(StringTest, Quote) {
+    EXPECT_EQ("'meow'", Quote("meow"));
+}
+
+#if 0  // Enable when moved to C++20 (https://github.com/google/googletest/issues/3081)
+TEST(StringTest, Split) {
+    EXPECT_THAT(Split("", ","), testing::ElementsAre(""));
+    EXPECT_THAT(Split("cat", ","), testing::ElementsAre("cat"));
+    EXPECT_THAT(Split("cat,", ","), testing::ElementsAre("cat", ""));
+    EXPECT_THAT(Split(",cat", ","), testing::ElementsAre("", "cat"));
+    EXPECT_THAT(Split("cat,dog,fish", ","), testing::ElementsAre("cat", "dog", "fish"));
+    EXPECT_THAT(Split("catdogfish", "dog"), testing::ElementsAre("cat", "fish"));
+}
+#endif
+
+TEST(StringTest, Join) {
+    EXPECT_EQ(Join(utils::Vector<int, 1>{}, ","), "");
+    EXPECT_EQ(Join(utils::Vector{1, 2, 3}, ","), "1,2,3");
+    EXPECT_EQ(Join(utils::Vector{"cat"}, ","), "cat");
+    EXPECT_EQ(Join(utils::Vector{"cat", "dog"}, ","), "cat,dog");
+}
+
 }  // namespace
 }  // namespace tint::utils
diff --git a/src/tint/utils/transform.h b/src/tint/utils/transform.h
index 9615471..7ecd941 100644
--- a/src/tint/utils/transform.h
+++ b/src/tint/utils/transform.h
@@ -85,6 +85,36 @@
     return result;
 }
 
+/// Transform performs an element-wise transformation of a slice.
+/// @param in the input slice.
+/// @param transform the transformation function with signature: `OUT(IN)`
+/// @tparam N the small-array size of the returned Vector
+/// @returns a new vector with each element of the source vector transformed by `transform`.
+template <size_t N, typename IN, typename TRANSFORMER>
+auto Transform(Slice<IN> in, TRANSFORMER&& transform) -> Vector<decltype(transform(in[0])), N> {
+    Vector<decltype(transform(in[0])), N> result;
+    result.Reserve(in.len);
+    for (size_t i = 0; i < in.len; ++i) {
+        result.Push(transform(in[i]));
+    }
+    return result;
+}
+
+/// Transform performs an element-wise transformation of a slice.
+/// @param in the input slice.
+/// @param transform the transformation function with signature: `OUT(IN, size_t)`
+/// @tparam N the small-array size of the returned Vector
+/// @returns a new vector with each element of the source vector transformed by `transform`.
+template <size_t N, typename IN, typename TRANSFORMER>
+auto Transform(Slice<IN> in, TRANSFORMER&& transform) -> Vector<decltype(transform(in[0], 1u)), N> {
+    Vector<decltype(transform(in[0], 1u)), N> result;
+    result.Reserve(in.len);
+    for (size_t i = 0; i < in.len; ++i) {
+        result.Push(transform(in[i], i));
+    }
+    return result;
+}
+
 /// Transform performs an element-wise transformation of a vector reference.
 /// @param in the input vector.
 /// @param transform the transformation function with signature: `OUT(IN)`
@@ -92,13 +122,7 @@
 /// @returns a new vector with each element of the source vector transformed by `transform`.
 template <size_t N, typename IN, typename TRANSFORMER>
 auto Transform(VectorRef<IN> in, TRANSFORMER&& transform) -> Vector<decltype(transform(in[0])), N> {
-    const auto count = in.Length();
-    Vector<decltype(transform(in[0])), N> result;
-    result.Reserve(count);
-    for (size_t i = 0; i < count; ++i) {
-        result.Push(transform(in[i]));
-    }
-    return result;
+    return Transform<N>(in.Slice(), std::forward<TRANSFORMER>(transform));
 }
 
 /// Transform performs an element-wise transformation of a vector reference.
@@ -109,13 +133,7 @@
 template <size_t N, typename IN, typename TRANSFORMER>
 auto Transform(VectorRef<IN> in, TRANSFORMER&& transform)
     -> Vector<decltype(transform(in[0], 1u)), N> {
-    const auto count = in.Length();
-    Vector<decltype(transform(in[0], 1u)), N> result;
-    result.Reserve(count);
-    for (size_t i = 0; i < count; ++i) {
-        result.Push(transform(in[i], i));
-    }
-    return result;
+    return Transform<N>(in.Slice(), std::forward<TRANSFORMER>(transform));
 }
 
 /// TransformN performs an element-wise transformation of a vector, transforming and returning at
diff --git a/src/tint/utils/vector.h b/src/tint/utils/vector.h
index 65595e7..acb0e39 100644
--- a/src/tint/utils/vector.h
+++ b/src/tint/utils/vector.h
@@ -26,8 +26,6 @@
 #include "src/tint/utils/bitcast.h"
 #include "src/tint/utils/compiler_macros.h"
 #include "src/tint/utils/slice.h"
-#include "src/tint/utils/string.h"
-#include "src/tint/utils/string_stream.h"
 
 namespace tint::utils {
 
@@ -587,12 +585,9 @@
 /// Aside from this move pattern, a VectorRef provides an immutable reference to the Vector.
 template <typename T>
 class VectorRef {
-    /// The slice type used by this vector reference
-    using Slice = utils::Slice<T>;
-
     /// @returns an empty slice.
-    static Slice& EmptySlice() {
-        static Slice empty;
+    static utils::Slice<T>& EmptySlice() {
+        static utils::Slice<T> empty;
         return empty;
     }
 
@@ -608,7 +603,7 @@
 
     /// Constructor from a Slice
     /// @param slice the slice
-    VectorRef(Slice& slice)  // NOLINT(runtime/explicit)
+    VectorRef(utils::Slice<T>& slice)  // NOLINT(runtime/explicit)
         : slice_(slice) {}
 
     /// Constructor from a Vector
@@ -621,7 +616,7 @@
     /// @param vector the vector to create a reference of
     template <size_t N>
     VectorRef(const Vector<T, N>& vector)  // NOLINT(runtime/explicit)
-        : slice_(const_cast<Slice&>(vector.impl_.slice)) {}
+        : slice_(const_cast<utils::Slice<T>&>(vector.impl_.slice)) {}
 
     /// Constructor from a moved Vector
     /// @param vector the vector being moved
@@ -689,6 +684,9 @@
         return {slice_.template Reinterpret<U, ReinterpretMode::kUnsafe>()};
     }
 
+    /// @returns the internal slice of the vector
+    utils::Slice<T> Slice() { return slice_; }
+
     /// @returns true if the vector is empty.
     bool IsEmpty() const { return slice_.len == 0; }
 
@@ -724,7 +722,7 @@
     friend class VectorRef;
 
     /// The slice of the vector being referenced.
-    Slice& slice_;
+    utils::Slice<T>& slice_;
     /// Whether the slice data is passed by r-value reference, and can be moved.
     bool can_move_ = false;
 };
@@ -753,44 +751,6 @@
     return out;
 }
 
-/// Prints the vector @p vec to @p o
-/// @param o the stream to write to
-/// @param vec the vector
-/// @return the stream so calls can be chained
-template <typename T, size_t N>
-inline utils::StringStream& operator<<(utils::StringStream& o, const utils::Vector<T, N>& vec) {
-    o << "[";
-    bool first = true;
-    for (auto& el : vec) {
-        if (!first) {
-            o << ", ";
-        }
-        first = false;
-        o << ToString(el);
-    }
-    o << "]";
-    return o;
-}
-
-/// Prints the vector @p vec to @p o
-/// @param o the stream to write to
-/// @param vec the vector reference
-/// @return the stream so calls can be chained
-template <typename T>
-inline utils::StringStream& operator<<(utils::StringStream& o, utils::VectorRef<T> vec) {
-    o << "[";
-    bool first = true;
-    for (auto& el : vec) {
-        if (!first) {
-            o << ", ";
-        }
-        first = false;
-        o << ToString(el);
-    }
-    o << "]";
-    return o;
-}
-
 }  // namespace tint::utils
 
 #endif  // SRC_TINT_UTILS_VECTOR_H_