Import Tint changes from Dawn

Changes:
  - 958a4642f14e66b9dec8e7b38f8ebe9b62a65297 tint/resolver: Use utils::Vector in a few places by Ben Clayton <bclayton@google.com>
  - d9a61cedaf0923473425249c0df7a73867b4f392 Remove source copies in the parser. by dan sinclair <dsinclair@chromium.org>
  - 4d1d143977dd4272dc61eb7c3f6fc96f539f77f8 tint/utils: More Vector polish by Ben Clayton <bclayton@google.com>
  - 0cbf5a922fe7b0b759db48ab07a329645b53e6bd Pre-parse token list. by dan sinclair <dsinclair@chromium.org>
  - 833ccab3842a72c302f84d9b5e4aefb64c8be66f Optimize the lexer match method. by dan sinclair <dsinclair@chromium.org>
  - 08482ec7ed7c45e27b02f4e8000eace9bb933cfa Expand ASCII short circuit. by dan sinclair <dsinclair@chromium.org>
  - b0499e446f858077061eda00181a80bcd50120de Add optional attribute trailing commas. by dan sinclair <dsinclair@chromium.org>
  - 659d58ceca0e07f4d1a2a292268397b58326c80c tint/resolver: Fix BuiltinCall validator nullptr deref by Zhaoming Jiang <zhaoming.jiang@intel.com>
GitOrigin-RevId: 958a4642f14e66b9dec8e7b38f8ebe9b62a65297
Change-Id: I28358e6be6d405ea3811edf79d0b8c1800285dd0
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/97183
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
diff --git a/src/tint/ast/traverse_expressions.h b/src/tint/ast/traverse_expressions.h
index 59b00e9..0974fd8 100644
--- a/src/tint/ast/traverse_expressions.h
+++ b/src/tint/ast/traverse_expressions.h
@@ -26,6 +26,7 @@
 #include "src/tint/ast/phony_expression.h"
 #include "src/tint/ast/unary_op_expression.h"
 #include "src/tint/utils/reverse.h"
+#include "src/tint/utils/vector.h"
 
 namespace tint::ast {
 
@@ -67,35 +68,34 @@
         size_t depth;
     };
 
-    std::vector<Pending> to_visit{{root, 0}};
+    utils::Vector<Pending, 64> to_visit{{root, 0}};
 
     auto push_single = [&](const ast::Expression* expr, size_t depth) {
-        to_visit.push_back({expr, depth});
+        to_visit.Push({expr, depth});
     };
     auto push_pair = [&](const ast::Expression* left, const ast::Expression* right, size_t depth) {
         if (ORDER == TraverseOrder::LeftToRight) {
-            to_visit.push_back({right, depth});
-            to_visit.push_back({left, depth});
+            to_visit.Push({right, depth});
+            to_visit.Push({left, depth});
         } else {
-            to_visit.push_back({left, depth});
-            to_visit.push_back({right, depth});
+            to_visit.Push({left, depth});
+            to_visit.Push({right, depth});
         }
     };
     auto push_list = [&](const std::vector<const ast::Expression*>& exprs, size_t depth) {
         if (ORDER == TraverseOrder::LeftToRight) {
             for (auto* expr : utils::Reverse(exprs)) {
-                to_visit.push_back({expr, depth});
+                to_visit.Push({expr, depth});
             }
         } else {
             for (auto* expr : exprs) {
-                to_visit.push_back({expr, depth});
+                to_visit.Push({expr, depth});
             }
         }
     };
 
-    while (!to_visit.empty()) {
-        auto p = to_visit.back();
-        to_visit.pop_back();
+    while (!to_visit.IsEmpty()) {
+        auto p = to_visit.Pop();
         const ast::Expression* expr = p.expr;
 
         if (auto* filtered = expr->template As<EXPR_TYPE>()) {
diff --git a/src/tint/castable.h b/src/tint/castable.h
index 806d0f0..d654fca 100644
--- a/src/tint/castable.h
+++ b/src/tint/castable.h
@@ -468,7 +468,7 @@
 
 /// Alias to typename CastableCommonBaseImpl<TYPES>::type
 template <typename... TYPES>
-using CastableCommonBase = typename detail::CastableCommonBaseImpl<TYPES...>::type;
+using CastableCommonBase = typename CastableCommonBaseImpl<TYPES...>::type;
 
 /// CastableCommonBaseImpl template specialization for a single type
 template <typename T>
diff --git a/src/tint/reader/wgsl/lexer.cc b/src/tint/reader/wgsl/lexer.cc
index 93392b6..c9056e0 100644
--- a/src/tint/reader/wgsl/lexer.cc
+++ b/src/tint/reader/wgsl/lexer.cc
@@ -37,6 +37,8 @@
               "tint::reader::wgsl requires the size of a std::string element "
               "to be a single byte");
 
+static constexpr size_t kDefaultListSize = 512;
+
 bool read_blankspace(std::string_view str, size_t i, bool* is_blankspace, size_t* blankspace_size) {
     // See https://www.w3.org/TR/WGSL/#blankspace
 
@@ -88,6 +90,27 @@
 
 Lexer::~Lexer() = default;
 
+std::vector<Token> Lexer::Lex() {
+    std::vector<Token> tokens;
+    tokens.reserve(kDefaultListSize);
+    while (true) {
+        tokens.emplace_back(next());
+
+        // If the token can be split, we insert a placeholder element into
+        // the stream to hold the split character.
+        if (tokens.back().IsSplittable()) {
+            auto src = tokens.back().source();
+            src.range.begin.column++;
+            tokens.emplace_back(Token(Token::Type::kPlaceholder, src));
+        }
+
+        if (tokens.back().IsEof() || tokens.back().IsError()) {
+            break;
+        }
+    }
+    return tokens;
+}
+
 const std::string_view Lexer::line() const {
     if (file_->content.lines.size() == 0) {
         static const char* empty_string = "";
@@ -204,6 +227,13 @@
     return substr(pos, sub_string.size()) == sub_string;
 }
 
+bool Lexer::matches(size_t pos, char ch) {
+    if (pos >= length()) {
+        return false;
+    }
+    return line()[pos] == ch;
+}
+
 Token Lexer::skip_blankspace_and_comments() {
     for (;;) {
         auto loc = location_;
@@ -298,7 +328,7 @@
     auto source = begin_source();
     bool has_mantissa_digits = false;
 
-    if (matches(end, "-")) {
+    if (matches(end, '-')) {
         end++;
     }
     while (end < length() && is_digit(at(end))) {
@@ -307,7 +337,7 @@
     }
 
     bool has_point = false;
-    if (end < length() && matches(end, ".")) {
+    if (end < length() && matches(end, '.')) {
         has_point = true;
         end++;
     }
@@ -323,9 +353,9 @@
 
     // Parse the exponent if one exists
     bool has_exponent = false;
-    if (end < length() && (matches(end, "e") || matches(end, "E"))) {
+    if (end < length() && (matches(end, 'e') || matches(end, 'E'))) {
         end++;
-        if (end < length() && (matches(end, "+") || matches(end, "-"))) {
+        if (end < length() && (matches(end, '+') || matches(end, '-'))) {
             end++;
         }
 
@@ -344,10 +374,10 @@
 
     bool has_f_suffix = false;
     bool has_h_suffix = false;
-    if (end < length() && matches(end, "f")) {
+    if (end < length() && matches(end, 'f')) {
         end++;
         has_f_suffix = true;
-    } else if (end < length() && matches(end, "h")) {
+    } else if (end < length() && matches(end, 'h')) {
         end++;
         has_h_suffix = true;
     }
@@ -410,12 +440,12 @@
 
     // -?
     uint64_t sign_bit = 0;
-    if (matches(end, "-")) {
+    if (matches(end, '-')) {
         sign_bit = 1;
         end++;
     }
     // 0[xX]
-    if (matches(end, "0x") || matches(end, "0X")) {
+    if (matches(end, '0') && (matches(end + 1, 'x') || matches(end + 1, 'X'))) {
         end += 2;
     } else {
         return {};
@@ -461,7 +491,7 @@
 
     // .?
     bool hex_point = false;
-    if (matches(end, ".")) {
+    if (matches(end, '.')) {
         hex_point = true;
         end++;
     }
@@ -479,7 +509,7 @@
     }
 
     // Is the binary exponent present?  It's optional.
-    const bool has_exponent = (matches(end, "p") || matches(end, "P"));
+    const bool has_exponent = (matches(end, 'p') || matches(end, 'P'));
     if (has_exponent) {
         end++;
     }
@@ -560,9 +590,9 @@
     if (has_exponent) {
         // Parse the rest of the exponent.
         // (+|-)?
-        if (matches(end, "+")) {
+        if (matches(end, '+')) {
             end++;
-        } else if (matches(end, "-")) {
+        } else if (matches(end, '-')) {
             exponent_sign = -1;
             end++;
         }
@@ -587,10 +617,10 @@
         // Parse optional 'f' or 'h' suffix.  For a hex float, it can only exist
         // when the exponent is present. Otherwise it will look like
         // one of the mantissa digits.
-        if (end < length() && matches(end, "f")) {
+        if (end < length() && matches(end, 'f')) {
             has_f_suffix = true;
             end++;
-        } else if (end < length() && matches(end, "h")) {
+        } else if (end < length() && matches(end, 'h')) {
             has_h_suffix = true;
             end++;
         }
@@ -794,7 +824,7 @@
         advance(static_cast<size_t>(end_ptr - start_ptr));
     }
 
-    if (matches(pos(), "u")) {
+    if (matches(pos(), 'u')) {
         if (!overflow && CheckedConvert<u32>(AInt(res))) {
             advance(1);
             end_source(source);
@@ -803,7 +833,7 @@
         return {Token::Type::kError, source, "value cannot be represented as 'u32'"};
     }
 
-    if (matches(pos(), "i")) {
+    if (matches(pos(), 'i')) {
         if (!overflow && CheckedConvert<i32>(AInt(res))) {
             advance(1);
             end_source(source);
@@ -825,11 +855,11 @@
 
     auto source = begin_source();
 
-    if (matches(curr, "-")) {
+    if (matches(curr, '-')) {
         curr++;
     }
 
-    if (matches(curr, "0x") || matches(curr, "0X")) {
+    if (matches(curr, '0') && (matches(curr + 1, 'x') || matches(curr + 1, 'X'))) {
         curr += 2;
     } else {
         return {};
@@ -849,7 +879,7 @@
 
     auto source = begin_source();
 
-    if (matches(curr, "-")) {
+    if (matches(curr, '-')) {
         curr++;
     }
 
@@ -927,138 +957,162 @@
     auto source = begin_source();
     auto type = Token::Type::kUninitialized;
 
-    if (matches(pos(), "@")) {
+    if (matches(pos(), '@')) {
         type = Token::Type::kAttr;
         advance(1);
-    } else if (matches(pos(), "(")) {
+    } else if (matches(pos(), '(')) {
         type = Token::Type::kParenLeft;
         advance(1);
-    } else if (matches(pos(), ")")) {
+    } else if (matches(pos(), ')')) {
         type = Token::Type::kParenRight;
         advance(1);
-    } else if (matches(pos(), "[")) {
+    } else if (matches(pos(), '[')) {
         type = Token::Type::kBracketLeft;
         advance(1);
-    } else if (matches(pos(), "]")) {
+    } else if (matches(pos(), ']')) {
         type = Token::Type::kBracketRight;
         advance(1);
-    } else if (matches(pos(), "{")) {
+    } else if (matches(pos(), '{')) {
         type = Token::Type::kBraceLeft;
         advance(1);
-    } else if (matches(pos(), "}")) {
+    } else if (matches(pos(), '}')) {
         type = Token::Type::kBraceRight;
         advance(1);
-    } else if (matches(pos(), "&&")) {
-        type = Token::Type::kAndAnd;
-        advance(2);
-    } else if (matches(pos(), "&=")) {
-        type = Token::Type::kAndEqual;
-        advance(2);
-    } else if (matches(pos(), "&")) {
-        type = Token::Type::kAnd;
-        advance(1);
-    } else if (matches(pos(), "/=")) {
-        type = Token::Type::kDivisionEqual;
-        advance(2);
-    } else if (matches(pos(), "/")) {
-        type = Token::Type::kForwardSlash;
-        advance(1);
-    } else if (matches(pos(), "!=")) {
-        type = Token::Type::kNotEqual;
-        advance(2);
-    } else if (matches(pos(), "!")) {
-        type = Token::Type::kBang;
-        advance(1);
-    } else if (matches(pos(), ":")) {
+    } else if (matches(pos(), '&')) {
+        if (matches(pos() + 1, '&')) {
+            type = Token::Type::kAndAnd;
+            advance(2);
+        } else if (matches(pos() + 1, '=')) {
+            type = Token::Type::kAndEqual;
+            advance(2);
+        } else {
+            type = Token::Type::kAnd;
+            advance(1);
+        }
+    } else if (matches(pos(), '/')) {
+        if (matches(pos() + 1, '=')) {
+            type = Token::Type::kDivisionEqual;
+            advance(2);
+        } else {
+            type = Token::Type::kForwardSlash;
+            advance(1);
+        }
+    } else if (matches(pos(), '!')) {
+        if (matches(pos() + 1, '=')) {
+            type = Token::Type::kNotEqual;
+            advance(2);
+        } else {
+            type = Token::Type::kBang;
+            advance(1);
+        }
+    } else if (matches(pos(), ':')) {
         type = Token::Type::kColon;
         advance(1);
-    } else if (matches(pos(), ",")) {
+    } else if (matches(pos(), ',')) {
         type = Token::Type::kComma;
         advance(1);
-    } else if (matches(pos(), "==")) {
-        type = Token::Type::kEqualEqual;
-        advance(2);
-    } else if (matches(pos(), "=")) {
-        type = Token::Type::kEqual;
-        advance(1);
-    } else if (matches(pos(), ">=")) {
-        type = Token::Type::kGreaterThanEqual;
-        advance(2);
-    } else if (matches(pos(), ">>")) {
-        type = Token::Type::kShiftRight;
-        advance(2);
-    } else if (matches(pos(), ">")) {
-        type = Token::Type::kGreaterThan;
-        advance(1);
-    } else if (matches(pos(), "<=")) {
-        type = Token::Type::kLessThanEqual;
-        advance(2);
-    } else if (matches(pos(), "<<")) {
-        type = Token::Type::kShiftLeft;
-        advance(2);
-    } else if (matches(pos(), "<")) {
-        type = Token::Type::kLessThan;
-        advance(1);
-    } else if (matches(pos(), "%=")) {
-        type = Token::Type::kModuloEqual;
-        advance(2);
-    } else if (matches(pos(), "%")) {
-        type = Token::Type::kMod;
-        advance(1);
-    } else if (matches(pos(), "->")) {
-        type = Token::Type::kArrow;
-        advance(2);
-    } else if (matches(pos(), "--")) {
-        type = Token::Type::kMinusMinus;
-        advance(2);
-    } else if (matches(pos(), "-=")) {
-        type = Token::Type::kMinusEqual;
-        advance(2);
-    } else if (matches(pos(), "-")) {
-        type = Token::Type::kMinus;
-        advance(1);
-    } else if (matches(pos(), ".")) {
+    } else if (matches(pos(), '=')) {
+        if (matches(pos() + 1, '=')) {
+            type = Token::Type::kEqualEqual;
+            advance(2);
+        } else {
+            type = Token::Type::kEqual;
+            advance(1);
+        }
+    } else if (matches(pos(), '>')) {
+        if (matches(pos() + 1, '=')) {
+            type = Token::Type::kGreaterThanEqual;
+            advance(2);
+        } else if (matches(pos() + 1, '>')) {
+            type = Token::Type::kShiftRight;
+            advance(2);
+        } else {
+            type = Token::Type::kGreaterThan;
+            advance(1);
+        }
+    } else if (matches(pos(), '<')) {
+        if (matches(pos() + 1, '=')) {
+            type = Token::Type::kLessThanEqual;
+            advance(2);
+        } else if (matches(pos() + 1, '<')) {
+            type = Token::Type::kShiftLeft;
+            advance(2);
+        } else {
+            type = Token::Type::kLessThan;
+            advance(1);
+        }
+    } else if (matches(pos(), '%')) {
+        if (matches(pos() + 1, '=')) {
+            type = Token::Type::kModuloEqual;
+            advance(2);
+        } else {
+            type = Token::Type::kMod;
+            advance(1);
+        }
+    } else if (matches(pos(), '-')) {
+        if (matches(pos() + 1, '>')) {
+            type = Token::Type::kArrow;
+            advance(2);
+        } else if (matches(pos() + 1, '-')) {
+            type = Token::Type::kMinusMinus;
+            advance(2);
+        } else if (matches(pos() + 1, '=')) {
+            type = Token::Type::kMinusEqual;
+            advance(2);
+        } else {
+            type = Token::Type::kMinus;
+            advance(1);
+        }
+    } else if (matches(pos(), '.')) {
         type = Token::Type::kPeriod;
         advance(1);
-    } else if (matches(pos(), "++")) {
-        type = Token::Type::kPlusPlus;
-        advance(2);
-    } else if (matches(pos(), "+=")) {
-        type = Token::Type::kPlusEqual;
-        advance(2);
-    } else if (matches(pos(), "+")) {
-        type = Token::Type::kPlus;
-        advance(1);
-    } else if (matches(pos(), "||")) {
-        type = Token::Type::kOrOr;
-        advance(2);
-    } else if (matches(pos(), "|=")) {
-        type = Token::Type::kOrEqual;
-        advance(2);
-    } else if (matches(pos(), "|")) {
-        type = Token::Type::kOr;
-        advance(1);
-    } else if (matches(pos(), ";")) {
+    } else if (matches(pos(), '+')) {
+        if (matches(pos() + 1, '+')) {
+            type = Token::Type::kPlusPlus;
+            advance(2);
+        } else if (matches(pos() + 1, '=')) {
+            type = Token::Type::kPlusEqual;
+            advance(2);
+        } else {
+            type = Token::Type::kPlus;
+            advance(1);
+        }
+    } else if (matches(pos(), '|')) {
+        if (matches(pos() + 1, '|')) {
+            type = Token::Type::kOrOr;
+            advance(2);
+        } else if (matches(pos() + 1, '=')) {
+            type = Token::Type::kOrEqual;
+            advance(2);
+        } else {
+            type = Token::Type::kOr;
+            advance(1);
+        }
+    } else if (matches(pos(), ';')) {
         type = Token::Type::kSemicolon;
         advance(1);
-    } else if (matches(pos(), "*=")) {
-        type = Token::Type::kTimesEqual;
-        advance(2);
-    } else if (matches(pos(), "*")) {
-        type = Token::Type::kStar;
-        advance(1);
-    } else if (matches(pos(), "~")) {
+    } else if (matches(pos(), '*')) {
+        if (matches(pos() + 1, '=')) {
+            type = Token::Type::kTimesEqual;
+            advance(2);
+        } else {
+            type = Token::Type::kStar;
+            advance(1);
+        }
+    } else if (matches(pos(), '~')) {
         type = Token::Type::kTilde;
         advance(1);
-    } else if (matches(pos(), "_")) {
+    } else if (matches(pos(), '_')) {
         type = Token::Type::kUnderscore;
         advance(1);
-    } else if (matches(pos(), "^=")) {
-        type = Token::Type::kXorEqual;
-        advance(2);
-    } else if (matches(pos(), "^")) {
-        type = Token::Type::kXor;
-        advance(1);
+    } else if (matches(pos(), '^')) {
+        if (matches(pos() + 1, '=')) {
+            type = Token::Type::kXorEqual;
+            advance(2);
+        } else {
+            type = Token::Type::kXor;
+            advance(1);
+        }
     }
 
     end_source(source);
diff --git a/src/tint/reader/wgsl/lexer.h b/src/tint/reader/wgsl/lexer.h
index d93848f..8e0306b 100644
--- a/src/tint/reader/wgsl/lexer.h
+++ b/src/tint/reader/wgsl/lexer.h
@@ -16,6 +16,7 @@
 #define SRC_TINT_READER_WGSL_LEXER_H_
 
 #include <string>
+#include <vector>
 
 #include "src/tint/reader/wgsl/token.h"
 
@@ -29,11 +30,14 @@
     explicit Lexer(const Source::File* file);
     ~Lexer();
 
+    /// @return the token list.
+    std::vector<Token> Lex();
+
+  private:
     /// Returns the next token in the input stream.
     /// @return Token
     Token next();
 
-  private:
     /// Advances past blankspace and comments, if present at the current position.
     /// @returns error token, EOF, or uninitialized
     Token skip_blankspace_and_comments();
@@ -96,7 +100,8 @@
     bool is_hex(char ch) const;
     /// @returns true if string at `pos` matches `substr`
     bool matches(size_t pos, std::string_view substr);
-
+    /// @returns true if char at `pos` matches `ch`
+    bool matches(size_t pos, char ch);
     /// The source file content
     Source::File const* const file_;
     /// The current location within the input
diff --git a/src/tint/reader/wgsl/lexer_test.cc b/src/tint/reader/wgsl/lexer_test.cc
index 799f9c2..f82045a 100644
--- a/src/tint/reader/wgsl/lexer_test.cc
+++ b/src/tint/reader/wgsl/lexer_test.cc
@@ -46,24 +46,33 @@
 TEST_F(LexerTest, Empty) {
     Source::File file("", "");
     Lexer l(&file);
-    auto t = l.next();
-    EXPECT_TRUE(t.IsEof());
+
+    auto list = l.Lex();
+    ASSERT_EQ(1u, list.size());
+    EXPECT_TRUE(list[0].IsEof());
 }
 
 TEST_F(LexerTest, Skips_Blankspace_Basic) {
     Source::File file("", "\t\r\n\t    ident\t\n\t  \r ");
     Lexer l(&file);
 
-    auto t = l.next();
-    EXPECT_TRUE(t.IsIdentifier());
-    EXPECT_EQ(t.source().range.begin.line, 2u);
-    EXPECT_EQ(t.source().range.begin.column, 6u);
-    EXPECT_EQ(t.source().range.end.line, 2u);
-    EXPECT_EQ(t.source().range.end.column, 11u);
-    EXPECT_EQ(t.to_str(), "ident");
+    auto list = l.Lex();
+    ASSERT_EQ(2u, list.size());
 
-    t = l.next();
-    EXPECT_TRUE(t.IsEof());
+    {
+        auto& t = list[0];
+        EXPECT_TRUE(t.IsIdentifier());
+        EXPECT_EQ(t.source().range.begin.line, 2u);
+        EXPECT_EQ(t.source().range.begin.column, 6u);
+        EXPECT_EQ(t.source().range.end.line, 2u);
+        EXPECT_EQ(t.source().range.end.column, 11u);
+        EXPECT_EQ(t.to_str(), "ident");
+    }
+
+    {
+        auto& t = list[1];
+        EXPECT_TRUE(t.IsEof());
+    }
 }
 
 TEST_F(LexerTest, Skips_Blankspace_Exotic) {
@@ -73,16 +82,23 @@
                       kVTab kFF kNL kLS kPS kL2R kR2L);
     Lexer l(&file);
 
-    auto t = l.next();
-    EXPECT_TRUE(t.IsIdentifier());
-    EXPECT_EQ(t.source().range.begin.line, 6u);
-    EXPECT_EQ(t.source().range.begin.column, 7u);
-    EXPECT_EQ(t.source().range.end.line, 6u);
-    EXPECT_EQ(t.source().range.end.column, 12u);
-    EXPECT_EQ(t.to_str(), "ident");
+    auto list = l.Lex();
+    ASSERT_EQ(2u, list.size());
 
-    t = l.next();
-    EXPECT_TRUE(t.IsEof());
+    {
+        auto& t = list[0];
+        EXPECT_TRUE(t.IsIdentifier());
+        EXPECT_EQ(t.source().range.begin.line, 6u);
+        EXPECT_EQ(t.source().range.begin.column, 7u);
+        EXPECT_EQ(t.source().range.end.line, 6u);
+        EXPECT_EQ(t.source().range.end.column, 12u);
+        EXPECT_EQ(t.to_str(), "ident");
+    }
+
+    {
+        auto& t = list[1];
+        EXPECT_TRUE(t.IsEof());
+    }
 }
 
 TEST_F(LexerTest, Skips_Comments_Line) {
@@ -92,24 +108,33 @@
  ident2)");
     Lexer l(&file);
 
-    auto t = l.next();
-    EXPECT_TRUE(t.IsIdentifier());
-    EXPECT_EQ(t.source().range.begin.line, 2u);
-    EXPECT_EQ(t.source().range.begin.column, 1u);
-    EXPECT_EQ(t.source().range.end.line, 2u);
-    EXPECT_EQ(t.source().range.end.column, 7u);
-    EXPECT_EQ(t.to_str(), "ident1");
+    auto list = l.Lex();
+    ASSERT_EQ(3u, list.size());
 
-    t = l.next();
-    EXPECT_TRUE(t.IsIdentifier());
-    EXPECT_EQ(t.source().range.begin.line, 4u);
-    EXPECT_EQ(t.source().range.begin.column, 2u);
-    EXPECT_EQ(t.source().range.end.line, 4u);
-    EXPECT_EQ(t.source().range.end.column, 8u);
-    EXPECT_EQ(t.to_str(), "ident2");
+    {
+        auto& t = list[0];
+        EXPECT_TRUE(t.IsIdentifier());
+        EXPECT_EQ(t.source().range.begin.line, 2u);
+        EXPECT_EQ(t.source().range.begin.column, 1u);
+        EXPECT_EQ(t.source().range.end.line, 2u);
+        EXPECT_EQ(t.source().range.end.column, 7u);
+        EXPECT_EQ(t.to_str(), "ident1");
+    }
 
-    t = l.next();
-    EXPECT_TRUE(t.IsEof());
+    {
+        auto& t = list[1];
+        EXPECT_TRUE(t.IsIdentifier());
+        EXPECT_EQ(t.source().range.begin.line, 4u);
+        EXPECT_EQ(t.source().range.begin.column, 2u);
+        EXPECT_EQ(t.source().range.end.line, 4u);
+        EXPECT_EQ(t.source().range.end.column, 8u);
+        EXPECT_EQ(t.to_str(), "ident2");
+    }
+
+    {
+        auto& t = list[2];
+        EXPECT_TRUE(t.IsEof());
+    }
 }
 
 TEST_F(LexerTest, Skips_Comments_Unicode) {
@@ -119,24 +144,33 @@
  ident2)");
     Lexer l(&file);
 
-    auto t = l.next();
-    EXPECT_TRUE(t.IsIdentifier());
-    EXPECT_EQ(t.source().range.begin.line, 2u);
-    EXPECT_EQ(t.source().range.begin.column, 1u);
-    EXPECT_EQ(t.source().range.end.line, 2u);
-    EXPECT_EQ(t.source().range.end.column, 7u);
-    EXPECT_EQ(t.to_str(), "ident1");
+    auto list = l.Lex();
+    ASSERT_EQ(3u, list.size());
 
-    t = l.next();
-    EXPECT_TRUE(t.IsIdentifier());
-    EXPECT_EQ(t.source().range.begin.line, 4u);
-    EXPECT_EQ(t.source().range.begin.column, 2u);
-    EXPECT_EQ(t.source().range.end.line, 4u);
-    EXPECT_EQ(t.source().range.end.column, 8u);
-    EXPECT_EQ(t.to_str(), "ident2");
+    {
+        auto& t = list[0];
+        EXPECT_TRUE(t.IsIdentifier());
+        EXPECT_EQ(t.source().range.begin.line, 2u);
+        EXPECT_EQ(t.source().range.begin.column, 1u);
+        EXPECT_EQ(t.source().range.end.line, 2u);
+        EXPECT_EQ(t.source().range.end.column, 7u);
+        EXPECT_EQ(t.to_str(), "ident1");
+    }
 
-    t = l.next();
-    EXPECT_TRUE(t.IsEof());
+    {
+        auto& t = list[1];
+        EXPECT_TRUE(t.IsIdentifier());
+        EXPECT_EQ(t.source().range.begin.line, 4u);
+        EXPECT_EQ(t.source().range.begin.column, 2u);
+        EXPECT_EQ(t.source().range.end.line, 4u);
+        EXPECT_EQ(t.source().range.end.column, 8u);
+        EXPECT_EQ(t.to_str(), "ident2");
+    }
+
+    {
+        auto& t = list[2];
+        EXPECT_TRUE(t.IsEof());
+    }
 }
 
 using LineCommentTerminatorTest = testing::TestWithParam<const char*>;
@@ -150,21 +184,29 @@
     Source::File file("", src);
     Lexer l(&file);
 
-    auto t = l.next();
-    EXPECT_TRUE(t.Is(Token::Type::kConst));
-    EXPECT_EQ(t.source().range.begin.line, 1u);
-    EXPECT_EQ(t.source().range.begin.column, 1u);
-    EXPECT_EQ(t.source().range.end.line, 1u);
-    EXPECT_EQ(t.source().range.end.column, 6u);
-
     auto is_same_line = [](std::string_view v) {
         return v == kSpace || v == kHTab || v == kL2R || v == kR2L;
     };
 
+    auto list = l.Lex();
+    ASSERT_EQ(is_same_line(c) ? 2u : 3u, list.size());
+
+    size_t idx = 0;
+
+    {
+        auto& t = list[idx++];
+        EXPECT_TRUE(t.Is(Token::Type::kConst));
+        EXPECT_EQ(t.source().range.begin.line, 1u);
+        EXPECT_EQ(t.source().range.begin.column, 1u);
+        EXPECT_EQ(t.source().range.end.line, 1u);
+        EXPECT_EQ(t.source().range.end.column, 6u);
+    }
+
     if (!is_same_line(c)) {
         size_t line = is_same_line(c) ? 1u : 2u;
         size_t col = is_same_line(c) ? 25u : 1u;
-        t = l.next();
+
+        auto& t = list[idx++];
         EXPECT_TRUE(t.IsIdentifier());
         EXPECT_EQ(t.source().range.begin.line, line);
         EXPECT_EQ(t.source().range.begin.column, col);
@@ -173,8 +215,10 @@
         EXPECT_EQ(t.to_str(), "ident");
     }
 
-    t = l.next();
-    EXPECT_TRUE(t.IsEof());
+    {
+        auto& t = list[idx];
+        EXPECT_TRUE(t.IsEof());
+    }
 }
 INSTANTIATE_TEST_SUITE_P(LexerTest,
                          LineCommentTerminatorTest,
@@ -198,16 +242,23 @@
 text */ident)");
     Lexer l(&file);
 
-    auto t = l.next();
-    EXPECT_TRUE(t.IsIdentifier());
-    EXPECT_EQ(t.source().range.begin.line, 2u);
-    EXPECT_EQ(t.source().range.begin.column, 8u);
-    EXPECT_EQ(t.source().range.end.line, 2u);
-    EXPECT_EQ(t.source().range.end.column, 13u);
-    EXPECT_EQ(t.to_str(), "ident");
+    auto list = l.Lex();
+    ASSERT_EQ(2u, list.size());
 
-    t = l.next();
-    EXPECT_TRUE(t.IsEof());
+    {
+        auto& t = list[0];
+        EXPECT_TRUE(t.IsIdentifier());
+        EXPECT_EQ(t.source().range.begin.line, 2u);
+        EXPECT_EQ(t.source().range.begin.column, 8u);
+        EXPECT_EQ(t.source().range.end.line, 2u);
+        EXPECT_EQ(t.source().range.end.column, 13u);
+        EXPECT_EQ(t.to_str(), "ident");
+    }
+
+    {
+        auto& t = list[1];
+        EXPECT_TRUE(t.IsEof());
+    }
 }
 
 TEST_F(LexerTest, Skips_Comments_Block_Nested) {
@@ -216,16 +267,23 @@
 /////**/ */*/ident)");
     Lexer l(&file);
 
-    auto t = l.next();
-    EXPECT_TRUE(t.IsIdentifier());
-    EXPECT_EQ(t.source().range.begin.line, 3u);
-    EXPECT_EQ(t.source().range.begin.column, 14u);
-    EXPECT_EQ(t.source().range.end.line, 3u);
-    EXPECT_EQ(t.source().range.end.column, 19u);
-    EXPECT_EQ(t.to_str(), "ident");
+    auto list = l.Lex();
+    ASSERT_EQ(2u, list.size());
 
-    t = l.next();
-    EXPECT_TRUE(t.IsEof());
+    {
+        auto& t = list[0];
+        EXPECT_TRUE(t.IsIdentifier());
+        EXPECT_EQ(t.source().range.begin.line, 3u);
+        EXPECT_EQ(t.source().range.begin.column, 14u);
+        EXPECT_EQ(t.source().range.end.line, 3u);
+        EXPECT_EQ(t.source().range.end.column, 19u);
+        EXPECT_EQ(t.to_str(), "ident");
+    }
+
+    {
+        auto& t = list[1];
+        EXPECT_TRUE(t.IsEof());
+    }
 }
 
 TEST_F(LexerTest, Skips_Comments_Block_Unterminated) {
@@ -237,7 +295,10 @@
 abcd)");
     Lexer l(&file);
 
-    auto t = l.next();
+    auto list = l.Lex();
+    ASSERT_EQ(1u, list.size());
+
+    auto& t = list[0];
     ASSERT_TRUE(t.Is(Token::Type::kError));
     EXPECT_EQ(t.to_str(), "unterminated block comment");
     EXPECT_EQ(t.source().range.begin.line, 2u);
@@ -250,7 +311,10 @@
     Source::File file("", std::string{' ', 0, ' '});
     Lexer l(&file);
 
-    auto t = l.next();
+    auto list = l.Lex();
+    ASSERT_EQ(1u, list.size());
+
+    auto& t = list[0];
     EXPECT_TRUE(t.IsError());
     EXPECT_EQ(t.source().range.begin.line, 1u);
     EXPECT_EQ(t.source().range.begin.column, 2u);
@@ -263,7 +327,10 @@
     Source::File file("", std::string{'/', '/', ' ', 0, ' '});
     Lexer l(&file);
 
-    auto t = l.next();
+    auto list = l.Lex();
+    ASSERT_EQ(1u, list.size());
+
+    auto& t = list[0];
     EXPECT_TRUE(t.IsError());
     EXPECT_EQ(t.source().range.begin.line, 1u);
     EXPECT_EQ(t.source().range.begin.column, 4u);
@@ -276,7 +343,10 @@
     Source::File file("", std::string{'/', '*', ' ', 0, '*', '/'});
     Lexer l(&file);
 
-    auto t = l.next();
+    auto list = l.Lex();
+    ASSERT_EQ(1u, list.size());
+
+    auto& t = list[0];
     EXPECT_TRUE(t.IsError());
     EXPECT_EQ(t.source().range.begin.line, 1u);
     EXPECT_EQ(t.source().range.begin.column, 4u);
@@ -292,16 +362,24 @@
     Source::File file("", std::string{'a', 0, 'c'});
     Lexer l(&file);
 
-    auto t = l.next();
-    EXPECT_TRUE(t.IsIdentifier());
-    EXPECT_EQ(t.to_str(), "a");
-    t = l.next();
-    EXPECT_TRUE(t.IsError());
-    EXPECT_EQ(t.source().range.begin.line, 1u);
-    EXPECT_EQ(t.source().range.begin.column, 2u);
-    EXPECT_EQ(t.source().range.end.line, 1u);
-    EXPECT_EQ(t.source().range.end.column, 2u);
-    EXPECT_EQ(t.to_str(), "null character found");
+    auto list = l.Lex();
+    ASSERT_EQ(2u, list.size());
+
+    {
+        auto& t = list[0];
+        EXPECT_TRUE(t.IsIdentifier());
+        EXPECT_EQ(t.to_str(), "a");
+    }
+
+    {
+        auto& t = list[1];
+        EXPECT_TRUE(t.IsError());
+        EXPECT_EQ(t.source().range.begin.line, 1u);
+        EXPECT_EQ(t.source().range.begin.column, 2u);
+        EXPECT_EQ(t.source().range.end.line, 1u);
+        EXPECT_EQ(t.source().range.end.column, 2u);
+        EXPECT_EQ(t.to_str(), "null character found");
+    }
 }
 
 struct FloatData {
@@ -318,22 +396,29 @@
     Source::File file("", params.input);
     Lexer l(&file);
 
-    auto t = l.next();
-    if (std::string(params.input).back() == 'f') {
-        EXPECT_TRUE(t.Is(Token::Type::kFloatLiteral_F));
-    } else if (std::string(params.input).back() == 'h') {
-        EXPECT_TRUE(t.Is(Token::Type::kFloatLiteral_H));
-    } else {
-        EXPECT_TRUE(t.Is(Token::Type::kFloatLiteral));
-    }
-    EXPECT_EQ(t.to_f64(), params.result);
-    EXPECT_EQ(t.source().range.begin.line, 1u);
-    EXPECT_EQ(t.source().range.begin.column, 1u);
-    EXPECT_EQ(t.source().range.end.line, 1u);
-    EXPECT_EQ(t.source().range.end.column, 1u + strlen(params.input));
+    auto list = l.Lex();
+    ASSERT_EQ(2u, list.size());
 
-    t = l.next();
-    EXPECT_TRUE(t.IsEof());
+    {
+        auto& t = list[0];
+        if (std::string(params.input).back() == 'f') {
+            EXPECT_TRUE(t.Is(Token::Type::kFloatLiteral_F));
+        } else if (std::string(params.input).back() == 'h') {
+            EXPECT_TRUE(t.Is(Token::Type::kFloatLiteral_H));
+        } else {
+            EXPECT_TRUE(t.Is(Token::Type::kFloatLiteral));
+        }
+        EXPECT_EQ(t.to_f64(), params.result);
+        EXPECT_EQ(t.source().range.begin.line, 1u);
+        EXPECT_EQ(t.source().range.begin.column, 1u);
+        EXPECT_EQ(t.source().range.end.line, 1u);
+        EXPECT_EQ(t.source().range.end.column, 1u + strlen(params.input));
+    }
+
+    {
+        auto& t = list[1];
+        EXPECT_TRUE(t.IsEof());
+    }
 }
 INSTANTIATE_TEST_SUITE_P(LexerTest,
                          FloatTest,
@@ -437,7 +522,10 @@
     Source::File file("", GetParam());
     Lexer l(&file);
 
-    auto t = l.next();
+    auto list = l.Lex();
+    ASSERT_FALSE(list.empty());
+
+    auto& t = list[0];
     EXPECT_FALSE(t.Is(Token::Type::kFloatLiteral) || t.Is(Token::Type::kFloatLiteral_F) ||
                  t.Is(Token::Type::kFloatLiteral_H));
 }
@@ -478,13 +566,23 @@
     Source::File file("", GetParam());
     Lexer l(&file);
 
-    auto t = l.next();
-    EXPECT_TRUE(t.IsIdentifier());
-    EXPECT_EQ(t.source().range.begin.line, 1u);
-    EXPECT_EQ(t.source().range.begin.column, 1u);
-    EXPECT_EQ(t.source().range.end.line, 1u);
-    EXPECT_EQ(t.source().range.end.column, 1u + strlen(GetParam()));
-    EXPECT_EQ(t.to_str(), GetParam());
+    auto list = l.Lex();
+    ASSERT_EQ(2u, list.size());
+
+    {
+        auto& t = list[0];
+        EXPECT_TRUE(t.IsIdentifier());
+        EXPECT_EQ(t.source().range.begin.line, 1u);
+        EXPECT_EQ(t.source().range.begin.column, 1u);
+        EXPECT_EQ(t.source().range.end.line, 1u);
+        EXPECT_EQ(t.source().range.end.column, 1u + strlen(GetParam()));
+        EXPECT_EQ(t.to_str(), GetParam());
+    }
+
+    {
+        auto& t = list[1];
+        EXPECT_TRUE(t.IsEof());
+    }
 }
 INSTANTIATE_TEST_SUITE_P(LexerTest,
                          AsciiIdentifierTest,
@@ -510,13 +608,23 @@
     Source::File file("", GetParam().utf8);
     Lexer l(&file);
 
-    auto t = l.next();
-    EXPECT_TRUE(t.IsIdentifier());
-    EXPECT_EQ(t.source().range.begin.line, 1u);
-    EXPECT_EQ(t.source().range.begin.column, 1u);
-    EXPECT_EQ(t.source().range.end.line, 1u);
-    EXPECT_EQ(t.source().range.end.column, 1u + GetParam().count);
-    EXPECT_EQ(t.to_str(), GetParam().utf8);
+    auto list = l.Lex();
+    ASSERT_EQ(2u, list.size());
+
+    {
+        auto& t = list[0];
+        EXPECT_TRUE(t.IsIdentifier());
+        EXPECT_EQ(t.source().range.begin.line, 1u);
+        EXPECT_EQ(t.source().range.begin.column, 1u);
+        EXPECT_EQ(t.source().range.end.line, 1u);
+        EXPECT_EQ(t.source().range.end.column, 1u + GetParam().count);
+        EXPECT_EQ(t.to_str(), GetParam().utf8);
+    }
+
+    {
+        auto& t = list[1];
+        EXPECT_TRUE(t.IsEof());
+    }
 }
 INSTANTIATE_TEST_SUITE_P(
     LexerTest,
@@ -554,7 +662,10 @@
     Source::File file("", GetParam());
     Lexer l(&file);
 
-    auto t = l.next();
+    auto list = l.Lex();
+    ASSERT_FALSE(list.empty());
+
+    auto& t = list[0];
     EXPECT_TRUE(t.IsError());
     EXPECT_EQ(t.source().range.begin.line, 1u);
     EXPECT_EQ(t.source().range.begin.column, 1u);
@@ -602,7 +713,10 @@
     Source::File file("", "_");
     Lexer l(&file);
 
-    auto t = l.next();
+    auto list = l.Lex();
+    ASSERT_FALSE(list.empty());
+
+    auto& t = list[0];
     EXPECT_FALSE(t.IsIdentifier());
 }
 
@@ -610,7 +724,10 @@
     Source::File file("", "__test");
     Lexer l(&file);
 
-    auto t = l.next();
+    auto list = l.Lex();
+    ASSERT_FALSE(list.empty());
+
+    auto& t = list[0];
     EXPECT_FALSE(t.IsIdentifier());
 }
 
@@ -618,7 +735,10 @@
     Source::File file("", "01test");
     Lexer l(&file);
 
-    auto t = l.next();
+    auto list = l.Lex();
+    EXPECT_FALSE(list.empty());
+
+    auto& t = list[0];
     EXPECT_FALSE(t.IsIdentifier());
 }
 
@@ -641,7 +761,12 @@
     auto params = std::get<1>(GetParam());
     Source::File file("", params.input);
 
-    auto t = Lexer(&file).next();
+    Lexer l(&file);
+
+    auto list = l.Lex();
+    ASSERT_FALSE(list.empty());
+
+    auto& t = list[0];
     switch (suffix) {
         case 'i':
             EXPECT_TRUE(t.Is(Token::Type::kIntLiteral_I));
@@ -770,7 +895,12 @@
     auto type = std::get<0>(GetParam());
     auto source = std::get<1>(GetParam());
     Source::File file("", source);
-    auto t = Lexer(&file).next();
+
+    Lexer l(&file);
+    auto list = l.Lex();
+    ASSERT_FALSE(list.empty());
+
+    auto& t = list[0];
     EXPECT_TRUE(t.Is(Token::Type::kError));
     auto expect = "value cannot be represented as '" + std::string(type) + "'";
     EXPECT_EQ(t.to_str(), expect);
@@ -800,7 +930,12 @@
 using ParseIntegerTest_LeadingZeros = testing::TestWithParam<const char*>;
 TEST_P(ParseIntegerTest_LeadingZeros, Parse) {
     Source::File file("", GetParam());
-    auto t = Lexer(&file).next();
+
+    Lexer l(&file);
+    auto list = l.Lex();
+    ASSERT_FALSE(list.empty());
+
+    auto& t = list[0];
     EXPECT_TRUE(t.Is(Token::Type::kError));
     EXPECT_EQ(t.to_str(), "integer literal cannot have leading 0s");
 }
@@ -815,7 +950,12 @@
 using ParseIntegerTest_NoSignificantDigits = testing::TestWithParam<const char*>;
 TEST_P(ParseIntegerTest_NoSignificantDigits, Parse) {
     Source::File file("", GetParam());
-    auto t = Lexer(&file).next();
+
+    Lexer l(&file);
+    auto list = l.Lex();
+    ASSERT_FALSE(list.empty());
+
+    auto& t = list[0];
     EXPECT_TRUE(t.Is(Token::Type::kError));
     EXPECT_EQ(t.to_str(), "integer or float hex literal has no significant digits");
 }
@@ -849,15 +989,22 @@
     Source::File file("", params.input);
     Lexer l(&file);
 
-    auto t = l.next();
-    EXPECT_TRUE(t.Is(params.type));
-    EXPECT_EQ(t.source().range.begin.line, 1u);
-    EXPECT_EQ(t.source().range.begin.column, 1u);
-    EXPECT_EQ(t.source().range.end.line, 1u);
-    EXPECT_EQ(t.source().range.end.column, 1u + strlen(params.input));
+    auto list = l.Lex();
+    ASSERT_GE(list.size(), 2u);
 
-    t = l.next();
-    EXPECT_EQ(t.source().range.begin.column, 1 + std::string(params.input).size());
+    {
+        auto& t = list[0];
+        EXPECT_TRUE(t.Is(params.type));
+        EXPECT_EQ(t.source().range.begin.line, 1u);
+        EXPECT_EQ(t.source().range.begin.column, 1u);
+        EXPECT_EQ(t.source().range.end.line, 1u);
+        EXPECT_EQ(t.source().range.end.column, 1u + strlen(params.input));
+    }
+
+    {
+        auto& t = list[1];
+        EXPECT_EQ(t.source().range.begin.column, 1 + std::string(params.input).size());
+    }
 }
 INSTANTIATE_TEST_SUITE_P(LexerTest,
                          PunctuationTest,
@@ -876,8 +1023,6 @@
                                          TokenData{"=", Token::Type::kEqual},
                                          TokenData{"==", Token::Type::kEqualEqual},
                                          TokenData{">", Token::Type::kGreaterThan},
-                                         TokenData{">=", Token::Type::kGreaterThanEqual},
-                                         TokenData{">>", Token::Type::kShiftRight},
                                          TokenData{"<", Token::Type::kLessThan},
                                          TokenData{"<=", Token::Type::kLessThanEqual},
                                          TokenData{"<<", Token::Type::kShiftLeft},
@@ -906,21 +1051,60 @@
                                          TokenData{"|=", Token::Type::kOrEqual},
                                          TokenData{"^=", Token::Type::kXorEqual}));
 
+using SplittablePunctuationTest = testing::TestWithParam<TokenData>;
+TEST_P(SplittablePunctuationTest, Parses) {
+    auto params = GetParam();
+    Source::File file("", params.input);
+    Lexer l(&file);
+
+    auto list = l.Lex();
+    ASSERT_GE(list.size(), 3u);
+
+    {
+        auto& t = list[0];
+        EXPECT_TRUE(t.Is(params.type));
+        EXPECT_EQ(t.source().range.begin.line, 1u);
+        EXPECT_EQ(t.source().range.begin.column, 1u);
+        EXPECT_EQ(t.source().range.end.line, 1u);
+        EXPECT_EQ(t.source().range.end.column, 1u + strlen(params.input));
+    }
+
+    {
+        auto& t = list[1];
+        EXPECT_TRUE(t.Is(Token::Type::kPlaceholder));
+        EXPECT_EQ(t.source().range.begin.line, 1u);
+        EXPECT_EQ(t.source().range.begin.column, 2u);
+        EXPECT_EQ(t.source().range.end.line, 1u);
+        EXPECT_EQ(t.source().range.end.column, 1u + strlen(params.input));
+    }
+
+    {
+        auto& t = list[2];
+        EXPECT_EQ(t.source().range.begin.column, 1 + std::string(params.input).size());
+    }
+}
+INSTANTIATE_TEST_SUITE_P(LexerTest,
+                         SplittablePunctuationTest,
+                         testing::Values(TokenData{">=", Token::Type::kGreaterThanEqual},
+                                         TokenData{">>", Token::Type::kShiftRight}));
+
 using KeywordTest = testing::TestWithParam<TokenData>;
 TEST_P(KeywordTest, Parses) {
     auto params = GetParam();
     Source::File file("", params.input);
     Lexer l(&file);
 
-    auto t = l.next();
+    auto list = l.Lex();
+    ASSERT_GE(list.size(), 2u);
+
+    auto& t = list[0];
     EXPECT_TRUE(t.Is(params.type)) << params.input;
     EXPECT_EQ(t.source().range.begin.line, 1u);
     EXPECT_EQ(t.source().range.begin.column, 1u);
     EXPECT_EQ(t.source().range.end.line, 1u);
     EXPECT_EQ(t.source().range.end.column, 1u + strlen(params.input));
 
-    t = l.next();
-    EXPECT_EQ(t.source().range.begin.column, 1 + std::string(params.input).size());
+    EXPECT_EQ(list[1].source().range.begin.column, 1 + std::string(params.input).size());
 }
 INSTANTIATE_TEST_SUITE_P(
     LexerTest,
diff --git a/src/tint/reader/wgsl/parser_impl.cc b/src/tint/reader/wgsl/parser_impl.cc
index 6ca78d6..7f8edd2 100644
--- a/src/tint/reader/wgsl/parser_impl.cc
+++ b/src/tint/reader/wgsl/parser_impl.cc
@@ -123,7 +123,7 @@
 const char kWorkgroupSizeAttribute[] = "workgroup_size";
 
 // https://gpuweb.github.io/gpuweb/wgsl.html#reserved-keywords
-bool is_reserved(Token t) {
+bool is_reserved(const Token& t) {
     return t == "asm" || t == "bf16" || t == "do" || t == "enum" || t == "f64" || t == "handle" ||
            t == "i8" || t == "i16" || t == "i64" || t == "mat" || t == "premerge" ||
            t == "regardless" || t == "typedef" || t == "u8" || t == "u16" || t == "u64" ||
@@ -181,7 +181,7 @@
     /// Implicit conversion to Source that returns the combined source from start
     /// to the current last token's source.
     operator Source() const {
-        Source end = parser_->last_token().source().End();
+        Source end = parser_->last_source().End();
         if (end < start_) {
             end = start_;
         }
@@ -241,7 +241,7 @@
 
 ParserImpl::VarDeclInfo::~VarDeclInfo() = default;
 
-ParserImpl::ParserImpl(Source::File const* file) : lexer_(std::make_unique<Lexer>(file)) {}
+ParserImpl::ParserImpl(Source::File const* file) : file_(file) {}
 
 ParserImpl::~ParserImpl() = default;
 
@@ -274,33 +274,67 @@
                                        "use of deprecated language feature: " + msg, source);
 }
 
-Token ParserImpl::next() {
-    if (!token_queue_.empty()) {
-        auto t = token_queue_.front();
-        token_queue_.pop_front();
-        last_token_ = t;
-        return last_token_;
+const Token& ParserImpl::next() {
+    if (!tokens_[next_token_idx_].IsEof() && !tokens_[next_token_idx_].IsError()) {
+        // Skip over any placeholder elements
+        while (true) {
+            if (!tokens_[next_token_idx_].IsPlaceholder()) {
+                break;
+            }
+            next_token_idx_++;
+        }
     }
-    last_token_ = lexer_->next();
-    return last_token_;
+    last_source_idx_ = next_token_idx_;
+    return tokens_[next_token_idx_++];
 }
 
-Token ParserImpl::peek(size_t idx) {
-    while (token_queue_.size() < (idx + 1)) {
-        token_queue_.push_back(lexer_->next());
+const Token& ParserImpl::peek(size_t idx) {
+    if (next_token_idx_ + idx >= tokens_.size()) {
+        return tokens_[tokens_.size() - 1];
     }
-    return token_queue_[idx];
+
+    // Skip over any placeholder elements
+    while (true) {
+        if (!tokens_[next_token_idx_ + idx].IsPlaceholder()) {
+            break;
+        }
+        idx++;
+    }
+
+    return tokens_[next_token_idx_ + idx];
 }
 
 bool ParserImpl::peek_is(Token::Type tok, size_t idx) {
     return peek(idx).Is(tok);
 }
 
-Token ParserImpl::last_token() const {
-    return last_token_;
+void ParserImpl::split_token(Token::Type lhs, Token::Type rhs) {
+    if (next_token_idx_ == 0) {
+        TINT_ICE(Reader, builder_.Diagnostics())
+            << "attempt to update placeholder at beginning of tokens";
+    }
+    if (next_token_idx_ >= tokens_.size()) {
+        TINT_ICE(Reader, builder_.Diagnostics())
+            << "attempt to update placeholder past end of tokens";
+    }
+    if (!tokens_[next_token_idx_].IsPlaceholder()) {
+        TINT_ICE(Reader, builder_.Diagnostics()) << "attempt to update non-placeholder token";
+    }
+    tokens_[next_token_idx_ - 1].SetType(lhs);
+    tokens_[next_token_idx_].SetType(rhs);
+}
+
+Source ParserImpl::last_source() const {
+    return tokens_[last_source_idx_].source();
+}
+
+void ParserImpl::InitializeLex() {
+    Lexer l{file_};
+    tokens_ = l.Lex();
 }
 
 bool ParserImpl::Parse() {
+    InitializeLex();
     translation_unit();
     return !has_error();
 }
@@ -310,7 +344,7 @@
 void ParserImpl::translation_unit() {
     bool after_global_decl = false;
     while (continue_parsing()) {
-        auto p = peek();
+        auto& p = peek();
         if (p.IsEof()) {
             break;
         }
@@ -353,7 +387,7 @@
 
         // Match the extension name.
         Expect<std::string> name = {""};
-        auto t = peek();
+        auto& t = peek();
         if (t.IsIdentifier()) {
             synchronized_ = true;
             next();
@@ -508,7 +542,7 @@
     }
 
     // We have a statement outside of a function?
-    auto t = peek();
+    auto& t = peek();
     auto stat = without_error([&] { return statement(); });
     if (stat.matched) {
         // Attempt to jump to the next '}' - the function might have just been
@@ -882,7 +916,7 @@
 //  | 'rgba32sint'
 //  | 'rgba32float'
 Expect<ast::TexelFormat> ParserImpl::expect_texel_format(std::string_view use) {
-    auto t = next();
+    auto& t = next();
     if (t == "rgba8unorm") {
         return ast::TexelFormat::kRgba8Unorm;
     }
@@ -951,7 +985,7 @@
         return Failure::kErrored;
     }
 
-    auto t = peek();
+    auto& t = peek();
     auto type = type_decl();
     if (type.errored) {
         return Failure::kErrored;
@@ -1021,7 +1055,7 @@
         return Failure::kNoMatch;
     }
 
-    auto t = next();
+    auto& t = next();
     const char* use = "type alias";
 
     auto name = expect_ident(use);
@@ -1069,7 +1103,7 @@
 //   | MAT4x4 LESS_THAN type_decl GREATER_THAN
 //   | texture_samplers
 Maybe<const ast::Type*> ParserImpl::type_decl() {
-    auto t = peek();
+    auto& t = peek();
     Source source;
     if (match(Token::Type::kIdentifier, &source)) {
         return builder_.create<ast::TypeName>(source, builder_.Symbols().Register(t.to_str()));
@@ -1139,7 +1173,7 @@
     return type.value;
 }
 
-Expect<const ast::Type*> ParserImpl::expect_type_decl_pointer(Token t) {
+Expect<const ast::Type*> ParserImpl::expect_type_decl_pointer(const Token& t) {
     const char* use = "ptr declaration";
 
     auto storage_class = ast::StorageClass::kNone;
@@ -1180,7 +1214,7 @@
                                access);
 }
 
-Expect<const ast::Type*> ParserImpl::expect_type_decl_atomic(Token t) {
+Expect<const ast::Type*> ParserImpl::expect_type_decl_atomic(const Token& t) {
     const char* use = "atomic declaration";
 
     auto subtype = expect_lt_gt_block(use, [&] { return expect_type(use); });
@@ -1191,7 +1225,7 @@
     return builder_.ty.atomic(make_source_range_from(t.source()), subtype.value);
 }
 
-Expect<const ast::Type*> ParserImpl::expect_type_decl_vector(Token t) {
+Expect<const ast::Type*> ParserImpl::expect_type_decl_vector(const Token& t) {
     uint32_t count = 2;
     if (t.Is(Token::Type::kVec3)) {
         count = 3;
@@ -1212,7 +1246,7 @@
     return builder_.ty.vec(make_source_range_from(t.source()), subtype, count);
 }
 
-Expect<const ast::Type*> ParserImpl::expect_type_decl_array(Token t) {
+Expect<const ast::Type*> ParserImpl::expect_type_decl_array(const Token& t) {
     const char* use = "array declaration";
 
     const ast::Expression* size = nullptr;
@@ -1244,7 +1278,7 @@
     return builder_.ty.array(make_source_range_from(t.source()), subtype.value, size);
 }
 
-Expect<const ast::Type*> ParserImpl::expect_type_decl_matrix(Token t) {
+Expect<const ast::Type*> ParserImpl::expect_type_decl_matrix(const Token& t) {
     uint32_t rows = 2;
     uint32_t columns = 2;
     if (t.IsMat3xN()) {
@@ -1272,7 +1306,7 @@
 }
 
 Expect<ast::StorageClass> ParserImpl::expect_storage_class(std::string_view use) {
-    auto source = peek().source();
+    auto& t = peek();
     auto ident = expect_ident("storage class");
     if (ident.errored) {
         return Failure::kErrored;
@@ -1280,33 +1314,32 @@
 
     auto name = ident.value;
     if (name == "uniform") {
-        return {ast::StorageClass::kUniform, source};
+        return {ast::StorageClass::kUniform, t.source()};
     }
 
     if (name == "workgroup") {
-        return {ast::StorageClass::kWorkgroup, source};
+        return {ast::StorageClass::kWorkgroup, t.source()};
     }
 
     if (name == "storage" || name == "storage_buffer") {
-        return {ast::StorageClass::kStorage, source};
+        return {ast::StorageClass::kStorage, t.source()};
     }
 
     if (name == "private") {
-        return {ast::StorageClass::kPrivate, source};
+        return {ast::StorageClass::kPrivate, t.source()};
     }
 
     if (name == "function") {
-        return {ast::StorageClass::kFunction, source};
+        return {ast::StorageClass::kFunction, t.source()};
     }
 
-    return add_error(source, "invalid storage class", use);
+    return add_error(t.source(), "invalid storage class", use);
 }
 
 // struct_decl
 //   : STRUCT IDENT struct_body_decl
 Maybe<const ast::Struct*> ParserImpl::struct_decl() {
-    auto t = peek();
-    auto source = t.source();
+    auto& t = peek();
 
     if (!match(Token::Type::kStruct)) {
         return Failure::kNoMatch;
@@ -1323,7 +1356,7 @@
     }
 
     auto sym = builder_.Symbols().Register(name.value);
-    return create<ast::Struct>(source, sym, std::move(body.value), ast::AttributeList{});
+    return create<ast::Struct>(t.source(), sym, std::move(body.value), ast::AttributeList{});
 }
 
 // struct_body_decl
@@ -1334,7 +1367,7 @@
         bool errored = false;
         while (continue_parsing()) {
             // Check for the end of the list.
-            auto t = peek();
+            auto& t = peek();
             if (!t.IsIdentifier() && !t.Is(Token::Type::kAttr)) {
                 break;
             }
@@ -1481,7 +1514,7 @@
     ast::ParameterList ret;
     while (continue_parsing()) {
         // Check for the end of the list.
-        auto t = peek();
+        auto& t = peek();
         if (!t.IsIdentifier() && !t.Is(Token::Type::kAttr)) {
             break;
         }
@@ -1522,7 +1555,7 @@
 //   | FRAGMENT
 //   | COMPUTE
 Expect<ast::PipelineStage> ParserImpl::expect_pipeline_stage() {
-    auto t = peek();
+    auto& t = peek();
     if (t == kVertexStage) {
         next();  // Consume the peek
         return {ast::PipelineStage::kVertex, t.source()};
@@ -2010,8 +2043,7 @@
         return Failure::kNoMatch;
     }
 
-    auto t = next();
-    auto source = t.source();
+    auto& t = next();
 
     ast::CaseSelectorList selector_list;
     if (t.Is(Token::Type::kCase)) {
@@ -2036,7 +2068,7 @@
         return add_error(body.source, "expected case body");
     }
 
-    return create<ast::CaseStatement>(source, selector_list, body.value);
+    return create<ast::CaseStatement>(t.source(), selector_list, body.value);
 }
 
 // case_selectors
@@ -2263,26 +2295,24 @@
 // func_call_stmt
 //    : IDENT argument_expression_list
 Maybe<const ast::CallStatement*> ParserImpl::func_call_stmt() {
-    auto t = peek();
-    auto t2 = peek(1);
+    auto& t = peek();
+    auto& t2 = peek(1);
     if (!t.IsIdentifier() || !t2.Is(Token::Type::kParenLeft)) {
         return Failure::kNoMatch;
     }
 
     next();  // Consume the first peek
 
-    auto source = t.source();
-    auto name = t.to_str();
-
     auto params = expect_argument_expression_list("function call");
     if (params.errored) {
         return Failure::kErrored;
     }
 
     return create<ast::CallStatement>(
-        source,
+        t.source(),
         create<ast::CallExpression>(
-            source, create<ast::IdentifierExpression>(source, builder_.Symbols().Register(name)),
+            t.source(),
+            create<ast::IdentifierExpression>(t.source(), builder_.Symbols().Register(t.to_str())),
             std::move(params.value)));
 }
 
@@ -2325,8 +2355,7 @@
 //   | paren_expression
 //   | BITCAST LESS_THAN type_decl GREATER_THAN paren_expression
 Maybe<const ast::Expression*> ParserImpl::primary_expression() {
-    auto t = peek();
-    auto source = t.source();
+    auto& t = peek();
 
     auto lit = const_literal();
     if (lit.errored) {
@@ -2358,7 +2387,7 @@
             return Failure::kErrored;
         }
 
-        return create<ast::BitcastExpression>(source, type.value, params.value);
+        return create<ast::BitcastExpression>(t.source(), type.value, params.value);
     }
 
     if (t.IsIdentifier()) {
@@ -2373,7 +2402,7 @@
                 return Failure::kErrored;
             }
 
-            return create<ast::CallExpression>(source, ident, std::move(params.value));
+            return create<ast::CallExpression>(t.source(), ident, std::move(params.value));
         }
 
         return ident;
@@ -2389,7 +2418,7 @@
             return Failure::kErrored;
         }
 
-        return builder_.Construct(source, type.value, std::move(params.value));
+        return builder_.Construct(t.source(), type.value, std::move(params.value));
     }
 
     return Failure::kNoMatch;
@@ -2491,7 +2520,7 @@
 //   | STAR unary_expression
 //   | AND unary_expression
 Maybe<const ast::Expression*> ParserImpl::unary_expression() {
-    auto t = peek();
+    auto& t = peek();
 
     if (match(Token::Type::kPlusPlus) || match(Token::Type::kMinusMinus)) {
         add_error(t.source(),
@@ -2556,20 +2585,18 @@
             return lhs;
         }
 
-        auto t = next();
-        auto source = t.source();
-        auto name = t.to_name();
+        auto& t = next();
 
         auto rhs = unary_expression();
         if (rhs.errored) {
             return Failure::kErrored;
         }
         if (!rhs.matched) {
-            return add_error(peek(),
-                             "unable to parse right side of " + std::string(name) + " expression");
+            return add_error(peek(), "unable to parse right side of " + std::string(t.to_name()) +
+                                         " expression");
         }
 
-        lhs = create<ast::BinaryExpression>(source, op, lhs, rhs.value);
+        lhs = create<ast::BinaryExpression>(t.source(), op, lhs, rhs.value);
     }
     return Failure::kErrored;
 }
@@ -2603,8 +2630,7 @@
             return lhs;
         }
 
-        auto t = next();
-        auto source = t.source();
+        auto& t = next();
 
         auto rhs = multiplicative_expression();
         if (rhs.errored) {
@@ -2614,7 +2640,7 @@
             return add_error(peek(), "unable to parse right side of + expression");
         }
 
-        lhs = create<ast::BinaryExpression>(source, op, lhs, rhs.value);
+        lhs = create<ast::BinaryExpression>(t.source(), op, lhs, rhs.value);
     }
     return Failure::kErrored;
 }
@@ -2651,8 +2677,7 @@
             return lhs;
         }
 
-        auto t = next();
-        auto source = t.source();
+        auto& t = next();
         auto rhs = additive_expression();
         if (rhs.errored) {
             return Failure::kErrored;
@@ -2662,7 +2687,7 @@
                              std::string("unable to parse right side of ") + name + " expression");
         }
 
-        return lhs = create<ast::BinaryExpression>(source, op, lhs, rhs.value);
+        return lhs = create<ast::BinaryExpression>(t.source(), op, lhs, rhs.value);
     }
     return Failure::kErrored;
 }
@@ -2702,20 +2727,18 @@
             return lhs;
         }
 
-        auto t = next();
-        auto source = t.source();
-        auto name = t.to_name();
+        auto& t = next();
 
         auto rhs = shift_expression();
         if (rhs.errored) {
             return Failure::kErrored;
         }
         if (!rhs.matched) {
-            return add_error(peek(),
-                             "unable to parse right side of " + std::string(name) + " expression");
+            return add_error(peek(), "unable to parse right side of " + std::string(t.to_name()) +
+                                         " expression");
         }
 
-        lhs = create<ast::BinaryExpression>(source, op, lhs, rhs.value);
+        lhs = create<ast::BinaryExpression>(t.source(), op, lhs, rhs.value);
     }
     return Failure::kErrored;
 }
@@ -2749,20 +2772,18 @@
             return lhs;
         }
 
-        auto t = next();
-        auto source = t.source();
-        auto name = t.to_name();
+        auto& t = next();
 
         auto rhs = relational_expression();
         if (rhs.errored) {
             return Failure::kErrored;
         }
         if (!rhs.matched) {
-            return add_error(peek(),
-                             "unable to parse right side of " + std::string(name) + " expression");
+            return add_error(peek(), "unable to parse right side of " + std::string(t.to_name()) +
+                                         " expression");
         }
 
-        lhs = create<ast::BinaryExpression>(source, op, lhs, rhs.value);
+        lhs = create<ast::BinaryExpression>(t.source(), op, lhs, rhs.value);
     }
     return Failure::kErrored;
 }
@@ -2790,8 +2811,7 @@
             return lhs;
         }
 
-        auto t = next();
-        auto source = t.source();
+        auto& t = next();
 
         auto rhs = equality_expression();
         if (rhs.errored) {
@@ -2801,7 +2821,7 @@
             return add_error(peek(), "unable to parse right side of & expression");
         }
 
-        lhs = create<ast::BinaryExpression>(source, ast::BinaryOp::kAnd, lhs, rhs.value);
+        lhs = create<ast::BinaryExpression>(t.source(), ast::BinaryOp::kAnd, lhs, rhs.value);
     }
     return Failure::kErrored;
 }
@@ -2903,8 +2923,7 @@
             return lhs;
         }
 
-        auto t = next();
-        auto source = t.source();
+        auto& t = next();
 
         auto rhs = inclusive_or_expression();
         if (rhs.errored) {
@@ -2914,7 +2933,7 @@
             return add_error(peek(), "unable to parse right side of && expression");
         }
 
-        lhs = create<ast::BinaryExpression>(source, ast::BinaryOp::kLogicalAnd, lhs, rhs.value);
+        lhs = create<ast::BinaryExpression>(t.source(), ast::BinaryOp::kLogicalAnd, lhs, rhs.value);
     }
     return Failure::kErrored;
 }
@@ -3008,13 +3027,14 @@
 // assignment_stmt
 // | lhs_expression ( equal | compound_assignment_operator ) expression
 // | underscore equal expression
+//
 // increment_stmt
 // | lhs_expression PLUS_PLUS
+//
 // decrement_stmt
 // | lhs_expression MINUS_MINUS
 Maybe<const ast::Statement*> ParserImpl::assignment_stmt() {
-    auto t = peek();
-    auto source = t.source();
+    auto& t = peek();
 
     // tint:295 - Test for `ident COLON` - this is invalid grammar, and without
     // special casing will error as "missing = for assignment", which is less
@@ -3028,6 +3048,7 @@
         return Failure::kErrored;
     }
     if (!lhs.matched) {
+        Source source = t.source();
         if (!match(Token::Type::kUnderscore, &source)) {
             return Failure::kNoMatch;
         }
@@ -3039,9 +3060,9 @@
     // the assignment statement, and we cannot tell which we are parsing until we
     // hit the ++/--/= token.
     if (match(Token::Type::kPlusPlus)) {
-        return create<ast::IncrementDecrementStatement>(source, lhs.value, true);
+        return create<ast::IncrementDecrementStatement>(t.source(), lhs.value, true);
     } else if (match(Token::Type::kMinusMinus)) {
-        return create<ast::IncrementDecrementStatement>(source, lhs.value, false);
+        return create<ast::IncrementDecrementStatement>(t.source(), lhs.value, false);
     }
 
     auto compound_op = compound_assignment_operator();
@@ -3063,10 +3084,10 @@
     }
 
     if (compound_op.value != ast::BinaryOp::kNone) {
-        return create<ast::CompoundAssignmentStatement>(source, lhs.value, rhs.value,
+        return create<ast::CompoundAssignmentStatement>(t.source(), lhs.value, rhs.value,
                                                         compound_op.value);
     } else {
-        return create<ast::AssignmentStatement>(source, lhs.value, rhs.value);
+        return create<ast::AssignmentStatement>(t.source(), lhs.value, rhs.value);
     }
 }
 
@@ -3076,7 +3097,7 @@
 //   | TRUE
 //   | FALSE
 Maybe<const ast::LiteralExpression*> ParserImpl::const_literal() {
-    auto t = peek();
+    auto& t = peek();
     if (match(Token::Type::kIntLiteral)) {
         return create<ast::IntLiteralExpression>(t.source(), t.to_i64(),
                                                  ast::IntLiteralExpression::Suffix::kNone);
@@ -3141,7 +3162,7 @@
 }
 
 Expect<const ast::Attribute*> ParserImpl::expect_attribute() {
-    auto t = peek();
+    auto& t = peek();
     auto attr = attribute();
     if (attr.errored) {
         return Failure::kErrored;
@@ -3154,7 +3175,7 @@
 
 Maybe<const ast::Attribute*> ParserImpl::attribute() {
     using Result = Maybe<const ast::Attribute*>;
-    auto t = next();
+    auto& t = next();
 
     if (!t.IsIdentifier()) {
         return Failure::kNoMatch;
@@ -3167,6 +3188,7 @@
             if (val.errored) {
                 return Failure::kErrored;
             }
+            match(Token::Type::kComma);
 
             return create<ast::LocationAttribute>(t.source(), val.value);
         });
@@ -3179,6 +3201,7 @@
             if (val.errored) {
                 return Failure::kErrored;
             }
+            match(Token::Type::kComma);
 
             return create<ast::BindingAttribute>(t.source(), val.value);
         });
@@ -3191,6 +3214,7 @@
             if (val.errored) {
                 return Failure::kErrored;
             }
+            match(Token::Type::kComma);
 
             return create<ast::GroupAttribute>(t.source(), val.value);
         });
@@ -3201,7 +3225,7 @@
             ast::InterpolationType type;
             ast::InterpolationSampling sampling = ast::InterpolationSampling::kNone;
 
-            auto type_tok = next();
+            auto& type_tok = next();
             if (type_tok == "perspective") {
                 type = ast::InterpolationType::kPerspective;
             } else if (type_tok == "linear") {
@@ -3213,15 +3237,18 @@
             }
 
             if (match(Token::Type::kComma)) {
-                auto sampling_tok = next();
-                if (sampling_tok == "center") {
-                    sampling = ast::InterpolationSampling::kCenter;
-                } else if (sampling_tok == "centroid") {
-                    sampling = ast::InterpolationSampling::kCentroid;
-                } else if (sampling_tok == "sample") {
-                    sampling = ast::InterpolationSampling::kSample;
-                } else {
-                    return add_error(sampling_tok, "invalid interpolation sampling");
+                if (!peek_is(Token::Type::kParenRight)) {
+                    auto& sampling_tok = next();
+                    if (sampling_tok == "center") {
+                        sampling = ast::InterpolationSampling::kCenter;
+                    } else if (sampling_tok == "centroid") {
+                        sampling = ast::InterpolationSampling::kCentroid;
+                    } else if (sampling_tok == "sample") {
+                        sampling = ast::InterpolationSampling::kSample;
+                    } else {
+                        return add_error(sampling_tok, "invalid interpolation sampling");
+                    }
+                    match(Token::Type::kComma);
                 }
             }
 
@@ -3240,6 +3267,7 @@
                 return Failure::kErrored;
             }
 
+            match(Token::Type::kComma);
             return create<ast::BuiltinAttribute>(t.source(), builtin.value);
         });
     }
@@ -3259,22 +3287,28 @@
             x = std::move(expr.value);
 
             if (match(Token::Type::kComma)) {
-                expr = primary_expression();
-                if (expr.errored) {
-                    return Failure::kErrored;
-                } else if (!expr.matched) {
-                    return add_error(peek(), "expected workgroup_size y parameter");
-                }
-                y = std::move(expr.value);
-
-                if (match(Token::Type::kComma)) {
+                if (!peek_is(Token::Type::kParenRight)) {
                     expr = primary_expression();
                     if (expr.errored) {
                         return Failure::kErrored;
                     } else if (!expr.matched) {
-                        return add_error(peek(), "expected workgroup_size z parameter");
+                        return add_error(peek(), "expected workgroup_size y parameter");
                     }
-                    z = std::move(expr.value);
+                    y = std::move(expr.value);
+
+                    if (match(Token::Type::kComma)) {
+                        if (!peek_is(Token::Type::kParenRight)) {
+                            expr = primary_expression();
+                            if (expr.errored) {
+                                return Failure::kErrored;
+                            } else if (!expr.matched) {
+                                return add_error(peek(), "expected workgroup_size z parameter");
+                            }
+                            z = std::move(expr.value);
+
+                            match(Token::Type::kComma);
+                        }
+                    }
                 }
             }
 
@@ -3326,6 +3360,7 @@
             if (val.errored) {
                 return Failure::kErrored;
             }
+            match(Token::Type::kComma);
 
             return create<ast::StructMemberSizeAttribute>(t.source(), val.value);
         });
@@ -3338,6 +3373,7 @@
             if (val.errored) {
                 return Failure::kErrored;
             }
+            match(Token::Type::kComma);
 
             return create<ast::StructMemberAlignAttribute>(t.source(), val.value);
         });
@@ -3350,6 +3386,7 @@
             if (val.errored) {
                 return Failure::kErrored;
             }
+            match(Token::Type::kComma);
 
             return create<ast::IdAttribute>(t.source(), val.value);
         });
@@ -3367,7 +3404,7 @@
 }
 
 bool ParserImpl::match(Token::Type tok, Source* source /*= nullptr*/) {
-    auto t = peek();
+    auto& t = peek();
 
     if (source != nullptr) {
         *source = t.source();
@@ -3381,7 +3418,7 @@
 }
 
 bool ParserImpl::expect(std::string_view use, Token::Type tok) {
-    auto t = peek();
+    auto& t = peek();
     if (t.Is(tok)) {
         next();
         synchronized_ = true;
@@ -3394,12 +3431,10 @@
         next();
 
         // Push the second character to the token queue.
-        auto source = t.source();
-        source.range.begin.column++;
         if (t.Is(Token::Type::kShiftRight)) {
-            token_queue_.push_front(Token(Token::Type::kGreaterThan, source));
+            split_token(Token::Type::kGreaterThan, Token::Type::kGreaterThan);
         } else if (t.Is(Token::Type::kGreaterThanEqual)) {
-            token_queue_.push_front(Token(Token::Type::kEqual, source));
+            split_token(Token::Type::kGreaterThan, Token::Type::kEqual);
         }
 
         synchronized_ = true;
@@ -3422,7 +3457,7 @@
 }
 
 Expect<int32_t> ParserImpl::expect_sint(std::string_view use) {
-    auto t = peek();
+    auto& t = peek();
     if (!t.Is(Token::Type::kIntLiteral) && !t.Is(Token::Type::kIntLiteral_I)) {
         return add_error(t.source(), "expected signed integer literal", use);
     }
@@ -3465,7 +3500,7 @@
 }
 
 Expect<std::string> ParserImpl::expect_ident(std::string_view use) {
-    auto t = peek();
+    auto& t = peek();
     if (t.IsIdentifier()) {
         synchronized_ = true;
         next();
@@ -3561,7 +3596,7 @@
     BlockCounters counters;
 
     for (size_t i = 0; i < kMaxResynchronizeLookahead; i++) {
-        auto t = peek(i);
+        auto& t = peek(i);
         if (counters.consume(t) > 0) {
             continue;  // Nested block
         }
diff --git a/src/tint/reader/wgsl/parser_impl.h b/src/tint/reader/wgsl/parser_impl.h
index a9c0423..f86d865 100644
--- a/src/tint/reader/wgsl/parser_impl.h
+++ b/src/tint/reader/wgsl/parser_impl.h
@@ -15,7 +15,6 @@
 #ifndef SRC_TINT_READER_WGSL_PARSER_IMPL_H_
 #define SRC_TINT_READER_WGSL_PARSER_IMPL_H_
 
-#include <deque>
 #include <memory>
 #include <string>
 #include <string_view>
@@ -300,6 +299,10 @@
     explicit ParserImpl(Source::File const* file);
     ~ParserImpl();
 
+    /// Reads tokens from the source file. This will be called automatically
+    /// by |parse|.
+    void InitializeLex();
+
     /// Run the parser
     /// @returns true if the parse was successful, false otherwise.
     bool Parse();
@@ -330,19 +333,19 @@
     ProgramBuilder& builder() { return builder_; }
 
     /// @returns the next token
-    Token next();
+    const Token& next();
     /// Peeks ahead and returns the token at `idx` ahead of the current position
     /// @param idx the index of the token to return
     /// @returns the token `idx` positions ahead without advancing
-    Token peek(size_t idx = 0);
+    const Token& peek(size_t idx = 0);
     /// Peeks ahead and returns true if the token at `idx` ahead of the current
     /// position is |tok|
     /// @param idx the index of the token to return
     /// @param tok the token to look for
     /// @returns true if the token `idx` positions ahead is |tok|
     bool peek_is(Token::Type tok, size_t idx = 0);
-    /// @returns the last token that was returned by `next()`
-    Token last_token() const;
+    /// @returns the last source location that was returned by `next()`
+    Source last_source() const;
     /// Appends an error at `t` with the message `msg`
     /// @param t the token to associate the error with
     /// @param msg the error message
@@ -812,11 +815,11 @@
     /// Used to ensure that all attributes are consumed.
     bool expect_attributes_consumed(ast::AttributeList& list);
 
-    Expect<const ast::Type*> expect_type_decl_pointer(Token t);
-    Expect<const ast::Type*> expect_type_decl_atomic(Token t);
-    Expect<const ast::Type*> expect_type_decl_vector(Token t);
-    Expect<const ast::Type*> expect_type_decl_array(Token t);
-    Expect<const ast::Type*> expect_type_decl_matrix(Token t);
+    Expect<const ast::Type*> expect_type_decl_pointer(const Token& t);
+    Expect<const ast::Type*> expect_type_decl_atomic(const Token& t);
+    Expect<const ast::Type*> expect_type_decl_vector(const Token& t);
+    Expect<const ast::Type*> expect_type_decl_array(const Token& t);
+    Expect<const ast::Type*> expect_type_decl_matrix(const Token& t);
 
     Expect<const ast::Type*> expect_type(std::string_view use);
 
@@ -824,6 +827,8 @@
     Maybe<const ast::Statement*> for_header_initializer();
     Maybe<const ast::Statement*> for_header_continuing();
 
+    void split_token(Token::Type lhs, Token::Type rhs);
+
     class MultiTokenSource;
     MultiTokenSource make_source_range();
     MultiTokenSource make_source_range_from(const Source& start);
@@ -837,9 +842,10 @@
         return builder_.create<T>(std::forward<ARGS>(args)...);
     }
 
-    std::unique_ptr<Lexer> lexer_;
-    std::deque<Token> token_queue_;
-    Token last_token_;
+    Source::File const* const file_;
+    std::vector<Token> tokens_;
+    size_t next_token_idx_ = 0;
+    size_t last_source_idx_ = 0;
     bool synchronized_ = true;
     uint32_t parse_depth_ = 0;
     std::vector<Token::Type> sync_tokens_;
diff --git a/src/tint/reader/wgsl/parser_impl_error_msg_test.cc b/src/tint/reader/wgsl/parser_impl_error_msg_test.cc
index 453e549..6a825de 100644
--- a/src/tint/reader/wgsl/parser_impl_error_msg_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_error_msg_test.cc
@@ -357,18 +357,18 @@
 }
 
 TEST_F(ParserImplErrorTest, FunctionDeclWorkgroupSizeYInvalid) {
-    EXPECT("@workgroup_size(1, ) fn f() {}",
+    EXPECT("@workgroup_size(1, fn) fn f() {}",
            R"(test.wgsl:1:20 error: expected workgroup_size y parameter
-@workgroup_size(1, ) fn f() {}
-                   ^
+@workgroup_size(1, fn) fn f() {}
+                   ^^
 )");
 }
 
 TEST_F(ParserImplErrorTest, FunctionDeclWorkgroupSizeZInvalid) {
-    EXPECT("@workgroup_size(1, 2, ) fn f() {}",
+    EXPECT("@workgroup_size(1, 2, fn) fn f() {}",
            R"(test.wgsl:1:23 error: expected workgroup_size z parameter
-@workgroup_size(1, 2, ) fn f() {}
-                      ^
+@workgroup_size(1, 2, fn) fn f() {}
+                      ^^
 )");
 }
 
diff --git a/src/tint/reader/wgsl/parser_impl_function_attribute_test.cc b/src/tint/reader/wgsl/parser_impl_function_attribute_test.cc
index a32e140..8e56ee7 100644
--- a/src/tint/reader/wgsl/parser_impl_function_attribute_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_function_attribute_test.cc
@@ -41,6 +41,38 @@
     EXPECT_EQ(values[2], nullptr);
 }
 
+TEST_F(ParserImplTest, Attribute_Workgroup_1Param_TrailingComma) {
+    auto p = parser("workgroup_size(4,)");
+    auto attr = p->attribute();
+    EXPECT_TRUE(attr.matched);
+    EXPECT_FALSE(attr.errored);
+    ASSERT_NE(attr.value, nullptr) << p->error();
+    ASSERT_FALSE(p->has_error());
+    auto* func_attr = attr.value->As<ast::Attribute>();
+    ASSERT_NE(func_attr, nullptr);
+    ASSERT_TRUE(func_attr->Is<ast::WorkgroupAttribute>());
+
+    auto values = func_attr->As<ast::WorkgroupAttribute>()->Values();
+
+    ASSERT_TRUE(values[0]->Is<ast::IntLiteralExpression>());
+    EXPECT_EQ(values[0]->As<ast::IntLiteralExpression>()->value, 4);
+    EXPECT_EQ(values[0]->As<ast::IntLiteralExpression>()->suffix,
+              ast::IntLiteralExpression::Suffix::kNone);
+
+    EXPECT_EQ(values[1], nullptr);
+    EXPECT_EQ(values[2], nullptr);
+}
+
+TEST_F(ParserImplTest, Attribute_Workgroup_1Param_TrailingComma_Double) {
+    auto p = parser("workgroup_size(4,,)");
+    auto attr = p->attribute();
+    EXPECT_FALSE(attr.matched);
+    EXPECT_TRUE(attr.errored);
+    EXPECT_EQ(attr.value, nullptr);
+    EXPECT_TRUE(p->has_error());
+    EXPECT_EQ(p->error(), "1:18: expected workgroup_size y parameter");
+}
+
 TEST_F(ParserImplTest, Attribute_Workgroup_2Param) {
     auto p = parser("workgroup_size(4, 5)");
     auto attr = p->attribute();
@@ -67,6 +99,42 @@
     EXPECT_EQ(values[2], nullptr);
 }
 
+TEST_F(ParserImplTest, Attribute_Workgroup_2Param_TrailingComma) {
+    auto p = parser("workgroup_size(4, 5,)");
+    auto attr = p->attribute();
+    EXPECT_TRUE(attr.matched);
+    EXPECT_FALSE(attr.errored);
+    ASSERT_NE(attr.value, nullptr) << p->error();
+    ASSERT_FALSE(p->has_error());
+    auto* func_attr = attr.value->As<ast::Attribute>();
+    ASSERT_NE(func_attr, nullptr) << p->error();
+    ASSERT_TRUE(func_attr->Is<ast::WorkgroupAttribute>());
+
+    auto values = func_attr->As<ast::WorkgroupAttribute>()->Values();
+
+    ASSERT_TRUE(values[0]->Is<ast::IntLiteralExpression>());
+    EXPECT_EQ(values[0]->As<ast::IntLiteralExpression>()->value, 4);
+    EXPECT_EQ(values[0]->As<ast::IntLiteralExpression>()->suffix,
+              ast::IntLiteralExpression::Suffix::kNone);
+
+    ASSERT_TRUE(values[1]->Is<ast::IntLiteralExpression>());
+    EXPECT_EQ(values[1]->As<ast::IntLiteralExpression>()->value, 5);
+    EXPECT_EQ(values[1]->As<ast::IntLiteralExpression>()->suffix,
+              ast::IntLiteralExpression::Suffix::kNone);
+
+    EXPECT_EQ(values[2], nullptr);
+}
+
+TEST_F(ParserImplTest, Attribute_Workgroup21Param_TrailingComma_Double) {
+    auto p = parser("workgroup_size(4,5,,)");
+    auto attr = p->attribute();
+    EXPECT_FALSE(attr.matched);
+    EXPECT_TRUE(attr.errored);
+    EXPECT_EQ(attr.value, nullptr);
+    EXPECT_TRUE(p->has_error());
+    EXPECT_EQ(p->error(), "1:20: expected workgroup_size z parameter");
+}
+
 TEST_F(ParserImplTest, Attribute_Workgroup_3Param) {
     auto p = parser("workgroup_size(4, 5, 6)");
     auto attr = p->attribute();
@@ -96,6 +164,35 @@
               ast::IntLiteralExpression::Suffix::kNone);
 }
 
+TEST_F(ParserImplTest, Attribute_Workgroup_3Param_TrailingComma) {
+    auto p = parser("workgroup_size(4, 5, 6,)");
+    auto attr = p->attribute();
+    EXPECT_TRUE(attr.matched);
+    EXPECT_FALSE(attr.errored);
+    ASSERT_NE(attr.value, nullptr) << p->error();
+    ASSERT_FALSE(p->has_error());
+    auto* func_attr = attr.value->As<ast::Attribute>();
+    ASSERT_NE(func_attr, nullptr);
+    ASSERT_TRUE(func_attr->Is<ast::WorkgroupAttribute>());
+
+    auto values = func_attr->As<ast::WorkgroupAttribute>()->Values();
+
+    ASSERT_TRUE(values[0]->Is<ast::IntLiteralExpression>());
+    EXPECT_EQ(values[0]->As<ast::IntLiteralExpression>()->value, 4);
+    EXPECT_EQ(values[0]->As<ast::IntLiteralExpression>()->suffix,
+              ast::IntLiteralExpression::Suffix::kNone);
+
+    ASSERT_TRUE(values[1]->Is<ast::IntLiteralExpression>());
+    EXPECT_EQ(values[1]->As<ast::IntLiteralExpression>()->value, 5);
+    EXPECT_EQ(values[1]->As<ast::IntLiteralExpression>()->suffix,
+              ast::IntLiteralExpression::Suffix::kNone);
+
+    ASSERT_TRUE(values[2]->Is<ast::IntLiteralExpression>());
+    EXPECT_EQ(values[2]->As<ast::IntLiteralExpression>()->value, 6);
+    EXPECT_EQ(values[2]->As<ast::IntLiteralExpression>()->suffix,
+              ast::IntLiteralExpression::Suffix::kNone);
+}
+
 TEST_F(ParserImplTest, Attribute_Workgroup_WithIdent) {
     auto p = parser("workgroup_size(4, height)");
     auto attr = p->attribute();
@@ -129,7 +226,7 @@
     EXPECT_TRUE(attr.errored);
     EXPECT_EQ(attr.value, nullptr);
     EXPECT_TRUE(p->has_error());
-    EXPECT_EQ(p->error(), "1:23: expected ')' for workgroup_size attribute");
+    EXPECT_EQ(p->error(), "1:25: expected ')' for workgroup_size attribute");
 }
 
 TEST_F(ParserImplTest, Attribute_Workgroup_MissingLeftParam) {
@@ -202,16 +299,6 @@
     EXPECT_EQ(p->error(), "1:21: expected ')' for workgroup_size attribute");
 }
 
-TEST_F(ParserImplTest, Attribute_Workgroup_Missing_Z_Value) {
-    auto p = parser("workgroup_size(1, 2, )");
-    auto attr = p->attribute();
-    EXPECT_FALSE(attr.matched);
-    EXPECT_TRUE(attr.errored);
-    EXPECT_EQ(attr.value, nullptr);
-    EXPECT_TRUE(p->has_error());
-    EXPECT_EQ(p->error(), "1:22: expected workgroup_size z parameter");
-}
-
 // TODO(crbug.com/tint/1503): Remove when @stage is removed
 TEST_F(ParserImplTest, Attribute_Stage) {
     auto p = parser("stage(compute)");
diff --git a/src/tint/reader/wgsl/parser_impl_global_constant_decl_test.cc b/src/tint/reader/wgsl/parser_impl_global_constant_decl_test.cc
index bfa678e..46bfa34 100644
--- a/src/tint/reader/wgsl/parser_impl_global_constant_decl_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_global_constant_decl_test.cc
@@ -204,6 +204,36 @@
     EXPECT_EQ(override_attr->value, 7u);
 }
 
+TEST_F(ParserImplTest, GlobalOverrideDecl_WithId_TrailingComma) {
+    auto p = parser("@id(7,) override a : f32 = 1.");
+    auto attrs = p->attribute_list();
+    EXPECT_FALSE(attrs.errored);
+    EXPECT_TRUE(attrs.matched);
+
+    auto e = p->global_constant_decl(attrs.value);
+    EXPECT_FALSE(p->has_error()) << p->error();
+    EXPECT_TRUE(e.matched);
+    EXPECT_FALSE(e.errored);
+    auto* override = e.value->As<ast::Override>();
+    ASSERT_NE(override, nullptr);
+
+    EXPECT_EQ(override->symbol, p->builder().Symbols().Get("a"));
+    ASSERT_NE(override->type, nullptr);
+    EXPECT_TRUE(override->type->Is<ast::F32>());
+
+    EXPECT_EQ(override->source.range.begin.line, 1u);
+    EXPECT_EQ(override->source.range.begin.column, 18u);
+    EXPECT_EQ(override->source.range.end.line, 1u);
+    EXPECT_EQ(override->source.range.end.column, 19u);
+
+    ASSERT_NE(override->constructor, nullptr);
+    EXPECT_TRUE(override->constructor->Is<ast::LiteralExpression>());
+
+    auto* override_attr = ast::GetAttribute<ast::IdAttribute>(override->attributes);
+    ASSERT_NE(override_attr, nullptr);
+    EXPECT_EQ(override_attr->value, 7u);
+}
+
 TEST_F(ParserImplTest, GlobalOverrideDecl_WithoutId) {
     auto p = parser("override a : f32 = 1.");
     auto attrs = p->attribute_list();
diff --git a/src/tint/reader/wgsl/parser_impl_pipeline_stage_test.cc b/src/tint/reader/wgsl/parser_impl_pipeline_stage_test.cc
index 75a1ec2..3c730e2 100644
--- a/src/tint/reader/wgsl/parser_impl_pipeline_stage_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_pipeline_stage_test.cc
@@ -40,7 +40,7 @@
     EXPECT_EQ(stage.source.range.end.line, 1u);
     EXPECT_EQ(stage.source.range.end.column, 1u + params.input.size());
 
-    auto t = p->next();
+    auto& t = p->next();
     EXPECT_TRUE(t.IsEof());
 }
 INSTANTIATE_TEST_SUITE_P(
diff --git a/src/tint/reader/wgsl/parser_impl_storage_class_test.cc b/src/tint/reader/wgsl/parser_impl_storage_class_test.cc
index deb87fc..7144f2c 100644
--- a/src/tint/reader/wgsl/parser_impl_storage_class_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_storage_class_test.cc
@@ -37,7 +37,7 @@
     EXPECT_FALSE(p->has_error());
     EXPECT_EQ(sc.value, params.result);
 
-    auto t = p->next();
+    auto& t = p->next();
     EXPECT_TRUE(t.IsEof());
 }
 INSTANTIATE_TEST_SUITE_P(
diff --git a/src/tint/reader/wgsl/parser_impl_struct_member_attribute_test.cc b/src/tint/reader/wgsl/parser_impl_struct_member_attribute_test.cc
index d185f37..df75f96 100644
--- a/src/tint/reader/wgsl/parser_impl_struct_member_attribute_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_struct_member_attribute_test.cc
@@ -33,6 +33,22 @@
     EXPECT_EQ(o->size, 4u);
 }
 
+TEST_F(ParserImplTest, Attribute_Size_TrailingComma) {
+    auto p = parser("size(4,)");
+    auto attr = p->attribute();
+    EXPECT_TRUE(attr.matched);
+    EXPECT_FALSE(attr.errored);
+    ASSERT_NE(attr.value, nullptr);
+    ASSERT_FALSE(p->has_error());
+
+    auto* member_attr = attr.value->As<ast::Attribute>();
+    ASSERT_NE(member_attr, nullptr);
+    ASSERT_TRUE(member_attr->Is<ast::StructMemberSizeAttribute>());
+
+    auto* o = member_attr->As<ast::StructMemberSizeAttribute>();
+    EXPECT_EQ(o->size, 4u);
+}
+
 TEST_F(ParserImplTest, Attribute_Size_MissingLeftParen) {
     auto p = parser("size 4)");
     auto attr = p->attribute();
@@ -89,6 +105,22 @@
     EXPECT_EQ(o->align, 4u);
 }
 
+TEST_F(ParserImplTest, Attribute_Align_TrailingComma) {
+    auto p = parser("align(4,)");
+    auto attr = p->attribute();
+    EXPECT_TRUE(attr.matched);
+    EXPECT_FALSE(attr.errored);
+    ASSERT_NE(attr.value, nullptr);
+    ASSERT_FALSE(p->has_error());
+
+    auto* member_attr = attr.value->As<ast::Attribute>();
+    ASSERT_NE(member_attr, nullptr);
+    ASSERT_TRUE(member_attr->Is<ast::StructMemberAlignAttribute>());
+
+    auto* o = member_attr->As<ast::StructMemberAlignAttribute>();
+    EXPECT_EQ(o->align, 4u);
+}
+
 TEST_F(ParserImplTest, Attribute_Align_MissingLeftParen) {
     auto p = parser("align 4)");
     auto attr = p->attribute();
diff --git a/src/tint/reader/wgsl/parser_impl_test_helper.h b/src/tint/reader/wgsl/parser_impl_test_helper.h
index f6ee8ea..1b14039 100644
--- a/src/tint/reader/wgsl/parser_impl_test_helper.h
+++ b/src/tint/reader/wgsl/parser_impl_test_helper.h
@@ -38,6 +38,7 @@
     std::unique_ptr<ParserImpl> parser(const std::string& str) {
         auto file = std::make_unique<Source::File>("test.wgsl", str);
         auto impl = std::make_unique<ParserImpl>(file.get());
+        impl->InitializeLex();
         files_.emplace_back(std::move(file));
         return impl;
     }
@@ -60,6 +61,7 @@
     std::unique_ptr<ParserImpl> parser(const std::string& str) {
         auto file = std::make_unique<Source::File>("test.wgsl", str);
         auto impl = std::make_unique<ParserImpl>(file.get());
+        impl->InitializeLex();
         files_.emplace_back(std::move(file));
         return impl;
     }
diff --git a/src/tint/reader/wgsl/parser_impl_variable_attribute_test.cc b/src/tint/reader/wgsl/parser_impl_variable_attribute_test.cc
index 8833273..01e4260 100644
--- a/src/tint/reader/wgsl/parser_impl_variable_attribute_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_variable_attribute_test.cc
@@ -32,6 +32,21 @@
     EXPECT_EQ(loc->value, 4u);
 }
 
+TEST_F(ParserImplTest, Attribute_Location_TrailingComma) {
+    auto p = parser("location(4,)");
+    auto attr = p->attribute();
+    EXPECT_TRUE(attr.matched);
+    EXPECT_FALSE(attr.errored);
+    ASSERT_NE(attr.value, nullptr);
+    auto* var_attr = attr.value->As<ast::Attribute>();
+    ASSERT_NE(var_attr, nullptr);
+    ASSERT_FALSE(p->has_error());
+    ASSERT_TRUE(var_attr->Is<ast::LocationAttribute>());
+
+    auto* loc = var_attr->As<ast::LocationAttribute>();
+    EXPECT_EQ(loc->value, 4u);
+}
+
 TEST_F(ParserImplTest, Attribute_Location_MissingLeftParen) {
     auto p = parser("location 4)");
     auto attr = p->attribute();
@@ -99,6 +114,22 @@
     auto* builtin = var_attr->As<ast::BuiltinAttribute>();
     EXPECT_EQ(builtin->builtin, params.result);
 }
+TEST_P(BuiltinTest, Attribute_Builtin_TrailingComma) {
+    auto params = GetParam();
+    auto p = parser(std::string("builtin(") + params.input + ",)");
+
+    auto attr = p->attribute();
+    EXPECT_TRUE(attr.matched);
+    EXPECT_FALSE(attr.errored);
+    ASSERT_NE(attr.value, nullptr);
+    auto* var_attr = attr.value->As<ast::Attribute>();
+    ASSERT_FALSE(p->has_error()) << p->error();
+    ASSERT_NE(var_attr, nullptr);
+    ASSERT_TRUE(var_attr->Is<ast::BuiltinAttribute>());
+
+    auto* builtin = var_attr->As<ast::BuiltinAttribute>();
+    EXPECT_EQ(builtin->builtin, params.result);
+}
 INSTANTIATE_TEST_SUITE_P(
     ParserImplTest,
     BuiltinTest,
@@ -182,6 +213,32 @@
     EXPECT_EQ(interp->sampling, ast::InterpolationSampling::kNone);
 }
 
+TEST_F(ParserImplTest, Attribute_Interpolate_Single_TrailingComma) {
+    auto p = parser("interpolate(flat,)");
+    auto attr = p->attribute();
+    EXPECT_TRUE(attr.matched);
+    EXPECT_FALSE(attr.errored);
+    ASSERT_NE(attr.value, nullptr);
+    auto* var_attr = attr.value->As<ast::Attribute>();
+    ASSERT_NE(var_attr, nullptr);
+    ASSERT_FALSE(p->has_error());
+    ASSERT_TRUE(var_attr->Is<ast::InterpolateAttribute>());
+
+    auto* interp = var_attr->As<ast::InterpolateAttribute>();
+    EXPECT_EQ(interp->type, ast::InterpolationType::kFlat);
+    EXPECT_EQ(interp->sampling, ast::InterpolationSampling::kNone);
+}
+
+TEST_F(ParserImplTest, Attribute_Interpolate_Single_DoubleTrailingComma) {
+    auto p = parser("interpolate(flat,,)");
+    auto attr = p->attribute();
+    EXPECT_FALSE(attr.matched);
+    EXPECT_TRUE(attr.errored);
+    EXPECT_EQ(attr.value, nullptr);
+    EXPECT_TRUE(p->has_error());
+    EXPECT_EQ(p->error(), "1:18: invalid interpolation sampling");
+}
+
 TEST_F(ParserImplTest, Attribute_Interpolate_Perspective_Center) {
     auto p = parser("interpolate(perspective, center)");
     auto attr = p->attribute();
@@ -198,6 +255,22 @@
     EXPECT_EQ(interp->sampling, ast::InterpolationSampling::kCenter);
 }
 
+TEST_F(ParserImplTest, Attribute_Interpolate_Double_TrailingComma) {
+    auto p = parser("interpolate(perspective, center,)");
+    auto attr = p->attribute();
+    EXPECT_TRUE(attr.matched);
+    EXPECT_FALSE(attr.errored);
+    ASSERT_NE(attr.value, nullptr);
+    auto* var_attr = attr.value->As<ast::Attribute>();
+    ASSERT_NE(var_attr, nullptr);
+    ASSERT_FALSE(p->has_error());
+    ASSERT_TRUE(var_attr->Is<ast::InterpolateAttribute>());
+
+    auto* interp = var_attr->As<ast::InterpolateAttribute>();
+    EXPECT_EQ(interp->type, ast::InterpolationType::kPerspective);
+    EXPECT_EQ(interp->sampling, ast::InterpolationSampling::kCenter);
+}
+
 TEST_F(ParserImplTest, Attribute_Interpolate_Perspective_Centroid) {
     auto p = parser("interpolate(perspective, centroid)");
     auto attr = p->attribute();
@@ -270,16 +343,6 @@
     EXPECT_EQ(p->error(), "1:13: invalid interpolation type");
 }
 
-TEST_F(ParserImplTest, Attribute_Interpolate_MissingSecondValue) {
-    auto p = parser("interpolate(perspective,)");
-    auto attr = p->attribute();
-    EXPECT_FALSE(attr.matched);
-    EXPECT_TRUE(attr.errored);
-    EXPECT_EQ(attr.value, nullptr);
-    EXPECT_TRUE(p->has_error());
-    EXPECT_EQ(p->error(), "1:25: invalid interpolation sampling");
-}
-
 TEST_F(ParserImplTest, Attribute_Interpolate_InvalidSecondValue) {
     auto p = parser("interpolate(perspective, nope)");
     auto attr = p->attribute();
@@ -305,6 +368,21 @@
     EXPECT_EQ(binding->value, 4u);
 }
 
+TEST_F(ParserImplTest, Attribute_Binding_TrailingComma) {
+    auto p = parser("binding(4,)");
+    auto attr = p->attribute();
+    EXPECT_TRUE(attr.matched);
+    EXPECT_FALSE(attr.errored);
+    ASSERT_NE(attr.value, nullptr);
+    auto* var_attr = attr.value->As<ast::Attribute>();
+    ASSERT_NE(var_attr, nullptr);
+    ASSERT_FALSE(p->has_error());
+    ASSERT_TRUE(var_attr->Is<ast::BindingAttribute>());
+
+    auto* binding = var_attr->As<ast::BindingAttribute>();
+    EXPECT_EQ(binding->value, 4u);
+}
+
 TEST_F(ParserImplTest, Attribute_Binding_MissingLeftParen) {
     auto p = parser("binding 4)");
     auto attr = p->attribute();
@@ -360,6 +438,21 @@
     EXPECT_EQ(group->value, 4u);
 }
 
+TEST_F(ParserImplTest, Attribute_group_TrailingComma) {
+    auto p = parser("group(4,)");
+    auto attr = p->attribute();
+    EXPECT_TRUE(attr.matched);
+    EXPECT_FALSE(attr.errored);
+    ASSERT_NE(attr.value, nullptr);
+    auto* var_attr = attr.value->As<ast::Attribute>();
+    ASSERT_FALSE(p->has_error());
+    ASSERT_NE(var_attr, nullptr);
+    ASSERT_TRUE(var_attr->Is<ast::GroupAttribute>());
+
+    auto* group = var_attr->As<ast::GroupAttribute>();
+    EXPECT_EQ(group->value, 4u);
+}
+
 TEST_F(ParserImplTest, Attribute_Group_MissingLeftParen) {
     auto p = parser("group 2)");
     auto attr = p->attribute();
diff --git a/src/tint/reader/wgsl/parser_impl_variable_decl_test.cc b/src/tint/reader/wgsl/parser_impl_variable_decl_test.cc
index 39f8b24..3ed0948 100644
--- a/src/tint/reader/wgsl/parser_impl_variable_decl_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_variable_decl_test.cc
@@ -68,7 +68,7 @@
     EXPECT_FALSE(v.errored);
     EXPECT_FALSE(p->has_error());
 
-    auto t = p->next();
+    auto& t = p->next();
     ASSERT_TRUE(t.IsIdentifier());
 }
 
diff --git a/src/tint/reader/wgsl/parser_impl_variable_qualifier_test.cc b/src/tint/reader/wgsl/parser_impl_variable_qualifier_test.cc
index aefd2b5..0dc0a1a 100644
--- a/src/tint/reader/wgsl/parser_impl_variable_qualifier_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_variable_qualifier_test.cc
@@ -40,7 +40,7 @@
     EXPECT_EQ(sc->storage_class, params.storage_class);
     EXPECT_EQ(sc->access, params.access);
 
-    auto t = p->next();
+    auto& t = p->next();
     EXPECT_TRUE(t.IsEof());
 }
 INSTANTIATE_TEST_SUITE_P(
@@ -83,7 +83,7 @@
     EXPECT_FALSE(sc.errored);
     EXPECT_FALSE(sc.matched);
 
-    auto t = p->next();
+    auto& t = p->next();
     ASSERT_TRUE(t.Is(Token::Type::kIdentifier));
 }
 
@@ -94,7 +94,7 @@
     EXPECT_FALSE(sc.errored);
     EXPECT_FALSE(sc.matched);
 
-    auto t = p->next();
+    auto& t = p->next();
     ASSERT_TRUE(t.Is(Token::Type::kIdentifier));
 }
 
diff --git a/src/tint/reader/wgsl/token.cc b/src/tint/reader/wgsl/token.cc
index 44255d3..a7b437f 100644
--- a/src/tint/reader/wgsl/token.cc
+++ b/src/tint/reader/wgsl/token.cc
@@ -37,6 +37,8 @@
             return "'i'-suffixed integer literal";
         case Token::Type::kIntLiteral_U:
             return "'u'-suffixed integer literal";
+        case Token::Type::kPlaceholder:
+            return "placeholder";
         case Token::Type::kUninitialized:
             return "uninitialized";
 
@@ -285,13 +287,9 @@
 
 Token::Token(Token&&) = default;
 
-Token::Token(const Token&) = default;
-
 Token::~Token() = default;
 
-Token& Token::operator=(const Token& rhs) = default;
-
-bool Token::operator==(std::string_view ident) {
+bool Token::operator==(std::string_view ident) const {
     if (type_ != Type::kIdentifier) {
         return false;
     }
diff --git a/src/tint/reader/wgsl/token.h b/src/tint/reader/wgsl/token.h
index 473588d..352c18f 100644
--- a/src/tint/reader/wgsl/token.h
+++ b/src/tint/reader/wgsl/token.h
@@ -32,6 +32,8 @@
         kError = -2,
         /// Uninitialized token
         kUninitialized = 0,
+        /// Placeholder token which maybe fillled in later
+        kPlaceholder = 1,
         /// End of input string reached
         kEOF,
 
@@ -310,19 +312,16 @@
     Token(Type type, const Source& source, double val);
     /// Move constructor
     Token(Token&&);
-    /// Copy constructor
-    Token(const Token&);
     ~Token();
 
-    /// Assignment operator
-    /// @param b the token to copy
-    /// @return Token
-    Token& operator=(const Token& b);
-
     /// Equality operator with an identifier
     /// @param ident the identifier string
     /// @return true if this token is an identifier and is equal to ident.
-    bool operator==(std::string_view ident);
+    bool operator==(std::string_view ident) const;
+
+    /// Sets the token to the given type
+    /// @param type the type to set
+    void SetType(Token::Type type) { type_ = type; }
 
     /// Returns true if the token is of the given type
     /// @param t the type to check against.
@@ -331,6 +330,8 @@
 
     /// @returns true if the token is uninitialized
     bool IsUninitialized() const { return type_ == Type::kUninitialized; }
+    /// @returns true if the token is a placeholder
+    bool IsPlaceholder() const { return type_ == Type::kPlaceholder; }
     /// @returns true if the token is EOF
     bool IsEof() const { return type_ == Type::kEOF; }
     /// @returns true if the token is Error
@@ -372,6 +373,11 @@
         return type_ == Type::kVec2 || type_ == Type::kVec3 || type_ == Type::kVec4;
     }
 
+    /// @returns true if the token can be split during parse into component tokens
+    bool IsSplittable() const {
+        return Is(Token::Type::kShiftRight) || Is(Token::Type::kGreaterThanEqual);
+    }
+
     /// @returns the source information for this token
     Source source() const { return source_; }
 
diff --git a/src/tint/resolver/builtin_test.cc b/src/tint/resolver/builtin_test.cc
index fa773b3..9d8d11e 100644
--- a/src/tint/resolver/builtin_test.cc
+++ b/src/tint/resolver/builtin_test.cc
@@ -1593,7 +1593,8 @@
                          ResolverBuiltinTest_Texture,
                          testing::ValuesIn(ast::builtin::test::TextureOverloadCase::ValidCases()));
 
-std::string to_str(const std::string& function, const sem::ParameterList& params) {
+std::string to_str(const std::string& function,
+                   utils::ConstVectorRef<const sem::Parameter*> params) {
     std::stringstream out;
     out << function << "(";
     bool first = true;
diff --git a/src/tint/resolver/const_eval.cc b/src/tint/resolver/const_eval.cc
index c5ee387..e1c3416 100644
--- a/src/tint/resolver/const_eval.cc
+++ b/src/tint/resolver/const_eval.cc
@@ -119,7 +119,7 @@
 // Forward declaration
 const Constant* CreateComposite(ProgramBuilder& builder,
                                 const sem::Type* type,
-                                std::vector<const sem::Constant*> elements);
+                                utils::VectorRef<const sem::Constant*> elements);
 
 /// Element holds a single scalar or abstract-numeric value.
 /// Element implements the Constant interface.
@@ -251,13 +251,16 @@
 /// implementation. Use CreateComposite() to create the appropriate Constant type.
 /// Composite implements the Constant interface.
 struct Composite : Constant {
-    Composite(const sem::Type* t, std::vector<const sem::Constant*> els, bool all_0, bool any_0)
+    Composite(const sem::Type* t,
+              utils::VectorRef<const sem::Constant*> els,
+              bool all_0,
+              bool any_0)
         : type(t), elements(std::move(els)), all_zero(all_0), any_zero(any_0), hash(CalcHash()) {}
     ~Composite() override = default;
     const sem::Type* Type() const override { return type; }
     std::variant<std::monostate, AInt, AFloat> Value() const override { return {}; }
     const sem::Constant* Index(size_t i) const override {
-        return i < elements.size() ? elements[i] : nullptr;
+        return i < elements.Length() ? elements[i] : nullptr;
     }
     bool AllZero() const override { return all_zero; }
     bool AnyZero() const override { return any_zero; }
@@ -269,8 +272,8 @@
                                            const Source& source) const override {
         // Convert each of the composite element types.
         auto* el_ty = sem::Type::ElementOf(target_ty);
-        std::vector<const sem::Constant*> conv_els;
-        conv_els.reserve(elements.size());
+        utils::Vector<const sem::Constant*, 4> conv_els;
+        conv_els.Reserve(elements.Length());
         for (auto* el : elements) {
             // Note: This file is the only place where `sem::Constant`s are created, so this
             // static_cast is safe.
@@ -281,7 +284,7 @@
             if (!conv_el.Get()) {
                 return nullptr;
             }
-            conv_els.emplace_back(conv_el.Get());
+            conv_els.Push(conv_el.Get());
         }
         return CreateComposite(builder, target_ty, std::move(conv_els));
     }
@@ -295,7 +298,7 @@
     }
 
     sem::Type const* const type;
-    const std::vector<const sem::Constant*> elements;
+    const utils::Vector<const sem::Constant*, 8> elements;
     const bool all_zero;
     const bool any_zero;
     const size_t hash;
@@ -327,15 +330,15 @@
         },
         [&](const sem::Struct* s) -> const Constant* {
             std::unordered_map<sem::Type*, const Constant*> zero_by_type;
-            std::vector<const sem::Constant*> zeros;
-            zeros.reserve(s->Members().size());
+            utils::Vector<const sem::Constant*, 4> zeros;
+            zeros.Reserve(s->Members().size());
             for (auto* member : s->Members()) {
                 auto* zero = utils::GetOrCreate(zero_by_type, member->Type(),
                                                 [&] { return ZeroValue(builder, member->Type()); });
                 if (!zero) {
                     return nullptr;
                 }
-                zeros.emplace_back(zero);
+                zeros.Push(zero);
             }
             if (zero_by_type.size() == 1) {
                 // All members were of the same type, so the zero value is the same for all members.
@@ -392,14 +395,14 @@
 /// depending on the element types and values.
 const Constant* CreateComposite(ProgramBuilder& builder,
                                 const sem::Type* type,
-                                std::vector<const sem::Constant*> elements) {
-    if (elements.size() == 0) {
+                                utils::VectorRef<const sem::Constant*> elements) {
+    if (elements.IsEmpty()) {
         return nullptr;
     }
     bool any_zero = false;
     bool all_zero = true;
     bool all_equal = true;
-    auto* first = elements.front();
+    auto* first = elements.Front();
     for (auto* el : elements) {
         if (!el) {
             return nullptr;
@@ -417,7 +420,7 @@
         }
     }
     if (all_equal) {
-        return builder.create<Splat>(type, elements[0], elements.size());
+        return builder.create<Splat>(type, elements[0], elements.Length());
     } else {
         return builder.create<Composite>(type, std::move(elements), all_zero, any_zero);
     }
@@ -433,9 +436,10 @@
     if (el_ty == ty) {
         return f(c);
     }
-    std::vector<const sem::Constant*> els(n);
+    utils::Vector<const sem::Constant*, 8> els;
+    els.Reserve(n);
     for (uint32_t i = 0; i < n; i++) {
-        els[i] = TransformElements(builder, c->Index(i), f);
+        els.Push(TransformElements(builder, c->Index(i), f));
     }
     return CreateComposite(builder, c->Type(), std::move(els));
 }
@@ -475,27 +479,29 @@
         });
 }
 
-const sem::Constant* ConstEval::ArrayOrStructCtor(const sem::Type* ty,
-                                                  const std::vector<const sem::Expression*>& args) {
-    if (args.empty()) {
+const sem::Constant* ConstEval::ArrayOrStructCtor(
+    const sem::Type* ty,
+    utils::ConstVectorRef<const sem::Expression*> args) {
+    if (args.IsEmpty()) {
         return ZeroValue(builder, ty);
     }
 
-    if (args.size() == 1 && args[0]->Type() == ty) {
+    if (args.Length() == 1 && args[0]->Type() == ty) {
         // Identity constructor.
         return args[0]->ConstantValue();
     }
 
     // Multiple arguments. Must be a type constructor.
-    std::vector<const sem::Constant*> els;
-    els.reserve(args.size());
+    utils::Vector<const sem::Constant*, 4> els;
+    els.Reserve(args.Length());
     for (auto* arg : args) {
-        els.emplace_back(arg->ConstantValue());
+        els.Push(arg->ConstantValue());
     }
     return CreateComposite(builder, ty, std::move(els));
 }
 
-const sem::Constant* ConstEval::Conv(const sem::Type* ty, ArgumentList args, size_t) {
+const sem::Constant* ConstEval::Conv(const sem::Type* ty,
+                                     utils::ConstVectorRef<const sem::Expression*> args) {
     uint32_t el_count = 0;
     auto* el_ty = sem::Type::ElementOf(ty, &el_count);
     if (!el_ty) {
@@ -515,79 +521,79 @@
     return nullptr;
 }
 
-const sem::Constant* ConstEval::Zero(const sem::Type* ty, ArgumentList, size_t) {
+const sem::Constant* ConstEval::Zero(const sem::Type* ty,
+                                     utils::ConstVectorRef<const sem::Expression*>) {
     return ZeroValue(builder, ty);
 }
 
-const sem::Constant* ConstEval::Identity(const sem::Type*, ArgumentList args, size_t) {
+const sem::Constant* ConstEval::Identity(const sem::Type*,
+                                         utils::ConstVectorRef<const sem::Expression*> args) {
     return args[0]->ConstantValue();
 }
 
 const sem::Constant* ConstEval::VecSplat(const sem::Type* ty,
-                                         sem::Expression const* const* args,
-                                         size_t) {
+                                         utils::ConstVectorRef<const sem::Expression*> args) {
     if (auto* arg = args[0]->ConstantValue()) {
         return builder.create<Splat>(ty, arg, static_cast<const sem::Vector*>(ty)->Width());
     }
     return nullptr;
 }
 
-const sem::Constant* ConstEval::VecCtorS(const sem::Type* ty, ArgumentList args, size_t num_args) {
-    std::vector<const sem::Constant*> els;
-    els.reserve(num_args);
-    for (size_t i = 0; i < num_args; i++) {
-        els.emplace_back(args[i]->ConstantValue());
+const sem::Constant* ConstEval::VecCtorS(const sem::Type* ty,
+                                         utils::ConstVectorRef<const sem::Expression*> args) {
+    utils::Vector<const sem::Constant*, 4> els;
+    for (auto* arg : args) {
+        els.Push(arg->ConstantValue());
     }
     return CreateComposite(builder, ty, std::move(els));
 }
 
-const sem::Constant* ConstEval::VecCtorM(const sem::Type* ty, ArgumentList args, size_t num_args) {
-    std::vector<const sem::Constant*> els;
-    els.reserve(num_args);
-    for (size_t i = 0; i < num_args; i++) {
-        auto* arg = args[i]->ConstantValue();
-        if (!arg) {
+const sem::Constant* ConstEval::VecCtorM(const sem::Type* ty,
+                                         utils::ConstVectorRef<const sem::Expression*> args) {
+    utils::Vector<const sem::Constant*, 4> els;
+    for (auto* arg : args) {
+        auto* val = arg->ConstantValue();
+        if (!val) {
             return nullptr;
         }
         auto* arg_ty = arg->Type();
         if (auto* arg_vec = arg_ty->As<sem::Vector>()) {
             // Extract out vector elements.
             for (uint32_t j = 0; j < arg_vec->Width(); j++) {
-                auto* el = arg->Index(j);
+                auto* el = val->Index(j);
                 if (!el) {
                     return nullptr;
                 }
-                els.emplace_back(el);
+                els.Push(el);
             }
         } else {
-            els.emplace_back(arg);
+            els.Push(val);
         }
     }
     return CreateComposite(builder, ty, std::move(els));
 }
 
-const sem::Constant* ConstEval::MatCtorS(const sem::Type* ty, ArgumentList args, size_t num_args) {
+const sem::Constant* ConstEval::MatCtorS(const sem::Type* ty,
+                                         utils::ConstVectorRef<const sem::Expression*> args) {
     auto* m = static_cast<const sem::Matrix*>(ty);
 
-    std::vector<const sem::Constant*> els;
-    els.reserve(num_args);
+    utils::Vector<const sem::Constant*, 4> els;
     for (uint32_t c = 0; c < m->columns(); c++) {
-        std::vector<const sem::Constant*> column;
-        column.reserve(m->rows());
+        utils::Vector<const sem::Constant*, 4> column;
         for (uint32_t r = 0; r < m->rows(); r++) {
             auto i = r + c * m->rows();
-            column.emplace_back(args[i]->ConstantValue());
+            column.Push(args[i]->ConstantValue());
         }
-        els.push_back(CreateComposite(builder, m->ColumnType(), std::move(column)));
+        els.Push(CreateComposite(builder, m->ColumnType(), std::move(column)));
     }
     return CreateComposite(builder, ty, std::move(els));
 }
 
-const sem::Constant* ConstEval::MatCtorV(const sem::Type* ty, ArgumentList args, size_t num_args) {
-    std::vector<const sem::Constant*> els;
-    els.reserve(num_args);
-    for (size_t i = 0; i < num_args; i++) {
-        els.emplace_back(args[i]->ConstantValue());
+const sem::Constant* ConstEval::MatCtorV(const sem::Type* ty,
+                                         utils::ConstVectorRef<const sem::Expression*> args) {
+    utils::Vector<const sem::Constant*, 4> els;
+    for (auto* arg : args) {
+        els.Push(arg->ConstantValue());
     }
     return CreateComposite(builder, ty, std::move(els));
 }
@@ -631,15 +637,15 @@
 
 const sem::Constant* ConstEval::Swizzle(const sem::Type* ty,
                                         const sem::Expression* vec_expr,
-                                        const std::vector<uint32_t>& indices) {
+                                        utils::ConstVectorRef<uint32_t> indices) {
     auto* vec_val = vec_expr->ConstantValue();
     if (!vec_val) {
         return nullptr;
     }
-    if (indices.size() == 1) {
+    if (indices.Length() == 1) {
         return vec_val->Index(static_cast<size_t>(indices[0]));
     } else {
-        auto values = utils::Transform(
+        auto values = utils::Transform<4>(
             indices, [&](uint32_t i) { return vec_val->Index(static_cast<size_t>(i)); });
         return CreateComposite(builder, ty, std::move(values));
     }
@@ -651,8 +657,7 @@
 }
 
 const sem::Constant* ConstEval::OpComplement(const sem::Type*,
-                                             sem::Expression const* const* args,
-                                             size_t) {
+                                             utils::ConstVectorRef<const sem::Expression*> args) {
     return TransformElements(builder, args[0]->ConstantValue(), [&](const sem::Constant* c) {
         return Dispatch_ia_iu32(c, [&](auto i) {  //
             return CreateElement(builder, c->Type(), decltype(i)(~i.value));
@@ -661,8 +666,7 @@
 }
 
 const sem::Constant* ConstEval::OpMinus(const sem::Type*,
-                                        sem::Expression const* const* args,
-                                        size_t) {
+                                        utils::ConstVectorRef<const sem::Expression*> args) {
     return TransformElements(builder, args[0]->ConstantValue(), [&](const sem::Constant* c) {
         return Dispatch_fia_fi32_f16(c, [&](auto i) {  //
             // For signed integrals, avoid C++ UB by not negating the smallest negative number. In
diff --git a/src/tint/resolver/const_eval.h b/src/tint/resolver/const_eval.h
index 6a85c85..9b8a3b1 100644
--- a/src/tint/resolver/const_eval.h
+++ b/src/tint/resolver/const_eval.h
@@ -17,9 +17,9 @@
 
 #include <stddef.h>
 #include <string>
-#include <vector>
 
 #include "src/tint/utils/result.h"
+#include "src/tint/utils/vector.h"
 
 // Forward declarations
 namespace tint {
@@ -44,14 +44,9 @@
 /// before calling a method to evaluate an expression's value.
 class ConstEval {
   public:
-    /// Typedef for a pointer to an array of `const sem::Expression*`, where each expression is an
-    /// argument to the function.
-    using ArgumentList = sem::Expression const* const*;
-
     /// Typedef for a constant evaluation function
-    using Function = const sem::Constant* (ConstEval::*)(const sem::Type* result_ty,
-                                                         ArgumentList args,
-                                                         size_t num_args);
+    using Function = const sem::Constant* (
+        ConstEval::*)(const sem::Type* result_ty, utils::ConstVectorRef<const sem::Expression*>);
 
     /// The result type of a method that may raise a diagnostic error and the caller should abort
     /// resolving. Can be one of three distinct values:
@@ -76,7 +71,7 @@
     /// @param args the input arguments
     /// @return the constructed value, or null if the value cannot be calculated
     const sem::Constant* ArrayOrStructCtor(const sem::Type* ty,
-                                           const std::vector<const sem::Expression*>& args);
+                                           utils::ConstVectorRef<const sem::Expression*> args);
 
     /// @param ty the target type
     /// @param expr the input expression
@@ -105,7 +100,7 @@
     /// @return the result of the swizzle, or null if the value cannot be calculated
     const sem::Constant* Swizzle(const sem::Type* ty,
                                  const sem::Expression* vector,
-                                 const std::vector<uint32_t>& indices);
+                                 utils::ConstVectorRef<uint32_t> indices);
 
     /// Convert the `value` to `target_type`
     /// @param ty the result type
@@ -121,58 +116,58 @@
     /// Type conversion
     /// @param ty the result type
     /// @param args the input arguments
-    /// @param num_args the number of input arguments
     /// @return the converted value, or null if the value cannot be calculated
-    const sem::Constant* Conv(const sem::Type* ty, ArgumentList args, size_t num_args);
+    const sem::Constant* Conv(const sem::Type* ty,
+                              utils::ConstVectorRef<const sem::Expression*> args);
 
     /// Zero value type constructor
     /// @param ty the result type
     /// @param args the input arguments (no arguments provided)
-    /// @param num_args the number of input arguments (no arguments provided)
     /// @return the constructed value, or null if the value cannot be calculated
-    const sem::Constant* Zero(const sem::Type* ty, ArgumentList args, size_t num_args);
+    const sem::Constant* Zero(const sem::Type* ty,
+                              utils::ConstVectorRef<const sem::Expression*> args);
 
     /// Identity value type constructor
     /// @param ty the result type
     /// @param args the input arguments
-    /// @param num_args the number of input arguments (must be 1)
     /// @return the constructed value, or null if the value cannot be calculated
-    const sem::Constant* Identity(const sem::Type* ty, ArgumentList args, size_t num_args);
+    const sem::Constant* Identity(const sem::Type* ty,
+                                  utils::ConstVectorRef<const sem::Expression*> args);
 
     /// Vector splat constructor
     /// @param ty the vector type
     /// @param args the input arguments
-    /// @param num_args the number of input arguments (must be 1)
     /// @return the constructed value, or null if the value cannot be calculated
-    const sem::Constant* VecSplat(const sem::Type* ty, ArgumentList args, size_t num_args);
+    const sem::Constant* VecSplat(const sem::Type* ty,
+                                  utils::ConstVectorRef<const sem::Expression*> args);
 
     /// Vector constructor using scalars
     /// @param ty the vector type
     /// @param args the input arguments
-    /// @param num_args the number of input arguments (must be equal to vector width)
     /// @return the constructed value, or null if the value cannot be calculated
-    const sem::Constant* VecCtorS(const sem::Type* ty, ArgumentList args, size_t num_args);
+    const sem::Constant* VecCtorS(const sem::Type* ty,
+                                  utils::ConstVectorRef<const sem::Expression*> args);
 
     /// Vector constructor using a mix of scalars and smaller vectors
     /// @param ty the vector type
     /// @param args the input arguments
-    /// @param num_args the number of input arguments
     /// @return the constructed value, or null if the value cannot be calculated
-    const sem::Constant* VecCtorM(const sem::Type* ty, ArgumentList args, size_t num_args);
+    const sem::Constant* VecCtorM(const sem::Type* ty,
+                                  utils::ConstVectorRef<const sem::Expression*> args);
 
     /// Matrix constructor using scalar values
     /// @param ty the matrix type
     /// @param args the input arguments
-    /// @param num_args the number of input arguments (must equal num-columns * num-rows)
     /// @return the constructed value, or null if the value cannot be calculated
-    const sem::Constant* MatCtorS(const sem::Type* ty, ArgumentList args, size_t num_args);
+    const sem::Constant* MatCtorS(const sem::Type* ty,
+                                  utils::ConstVectorRef<const sem::Expression*> args);
 
     /// Matrix constructor using column vectors
     /// @param ty the matrix type
     /// @param args the input arguments
-    /// @param num_args the number of input arguments (must equal num-columns)
     /// @return the constructed value, or null if the value cannot be calculated
-    const sem::Constant* MatCtorV(const sem::Type* ty, ArgumentList args, size_t num_args);
+    const sem::Constant* MatCtorV(const sem::Type* ty,
+                                  utils::ConstVectorRef<const sem::Expression*> args);
 
     ////////////////////////////////////////////////////////////////////////////
     // Operators
@@ -181,20 +176,16 @@
     /// Complement operator '~'
     /// @param ty the integer type
     /// @param args the input arguments
-    /// @param num_args the number of input arguments (must be 1)
     /// @return the result value, or null if the value cannot be calculated
     const sem::Constant* OpComplement(const sem::Type* ty,
-                                      sem::Expression const* const* args,
-                                      size_t num_args);
+                                      utils::ConstVectorRef<const sem::Expression*> args);
 
     /// Minus operator '-'
     /// @param ty the expression type
     /// @param args the input arguments
-    /// @param num_args the number of input arguments (must be 1)
     /// @return the result value, or null if the value cannot be calculated
     const sem::Constant* OpMinus(const sem::Type* ty,
-                                 sem::Expression const* const* args,
-                                 size_t num_args);
+                                 utils::ConstVectorRef<const sem::Expression*> args);
 
   private:
     /// Adds the given error message to the diagnostics
diff --git a/src/tint/resolver/intrinsic_table.cc b/src/tint/resolver/intrinsic_table.cc
index b874df1..8e6d72d 100644
--- a/src/tint/resolver/intrinsic_table.cc
+++ b/src/tint/resolver/intrinsic_table.cc
@@ -48,6 +48,12 @@
 class NumberMatcher;
 class TypeMatcher;
 
+/// The utils::Vector `N` template argument value for arrays of parameters.
+constexpr static const size_t kNumFixedParams = 8;
+
+/// The utils::Vector `N` template argument value for arrays of overload candidates.
+constexpr static const size_t kNumFixedCandidates = 8;
+
 /// A special type that matches all TypeMatchers
 class Any final : public Castable<Any, sem::Type> {
   public:
@@ -882,7 +888,7 @@
         /// @param i the IntrinsicPrototype to create a hash for
         /// @return the hash value
         inline std::size_t operator()(const IntrinsicPrototype& i) const {
-            size_t hash = utils::Hash(i.parameters.size());
+            size_t hash = utils::Hash(i.parameters.Length());
             for (auto& p : i.parameters) {
                 utils::HashCombine(&hash, p.type, p.usage);
             }
@@ -892,16 +898,16 @@
 
     const OverloadInfo* overload = nullptr;
     sem::Type const* return_type = nullptr;
-    std::vector<Parameter> parameters;
+    utils::Vector<Parameter, kNumFixedParams> parameters;
 };
 
 /// Equality operator for IntrinsicPrototype
 bool operator==(const IntrinsicPrototype& a, const IntrinsicPrototype& b) {
     if (a.overload != b.overload || a.return_type != b.return_type ||
-        a.parameters.size() != b.parameters.size()) {
+        a.parameters.Length() != b.parameters.Length()) {
         return false;
     }
-    for (size_t i = 0; i < a.parameters.size(); i++) {
+    for (size_t i = 0; i < a.parameters.Length(); i++) {
         auto& pa = a.parameters[i];
         auto& pb = b.parameters[i];
         if (pa.type != pb.type || pa.usage != pb.usage) {
@@ -917,7 +923,7 @@
     explicit Impl(ProgramBuilder& builder);
 
     Builtin Lookup(sem::BuiltinType builtin_type,
-                   const std::vector<const sem::Type*>& args,
+                   utils::VectorRef<const sem::Type*> args,
                    const Source& source) override;
 
     UnaryOperator Lookup(ast::UnaryOp op, const sem::Type* arg, const Source& source) override;
@@ -930,7 +936,7 @@
 
     CtorOrConv Lookup(CtorConvIntrinsic type,
                       const sem::Type* template_arg,
-                      const std::vector<const sem::Type*>& args,
+                      utils::VectorRef<const sem::Type*> args,
                       const Source& source) override;
 
   private:
@@ -941,7 +947,7 @@
         /// The template types and numbers
         TemplateState templates;
         /// The parameter types for the candidate overload
-        std::vector<IntrinsicPrototype::Parameter> parameters;
+        utils::Vector<IntrinsicPrototype::Parameter, kNumFixedParams> parameters;
         /// The match-score of the candidate overload.
         /// A score of zero indicates an exact match.
         /// Non-zero scores are used for diagnostics when no overload matches.
@@ -950,10 +956,10 @@
     };
 
     /// A list of candidates
-    using Candidates = std::vector<Candidate>;
+    using Candidates = utils::Vector<Candidate, kNumFixedCandidates>;
 
     /// Callback function when no overloads match.
-    using OnNoMatch = std::function<void(Candidates)>;
+    using OnNoMatch = std::function<void(utils::VectorRef<Candidate>)>;
 
     /// Sorts the candidates based on their score, with the lowest (best-ranking) scores first.
     static inline void SortCandidates(Candidates& candidates) {
@@ -975,7 +981,7 @@
     ///          IntrinsicPrototype::return_type.
     IntrinsicPrototype MatchIntrinsic(const IntrinsicInfo& intrinsic,
                                       const char* intrinsic_name,
-                                      const std::vector<const sem::Type*>& args,
+                                      utils::VectorRef<const sem::Type*> args,
                                       TemplateState templates,
                                       OnNoMatch on_no_match) const;
 
@@ -987,7 +993,7 @@
     ///                  template as `f32`.
     /// @returns the evaluated Candidate information.
     Candidate ScoreOverload(const OverloadInfo* overload,
-                            const std::vector<const sem::Type*>& args,
+                            utils::VectorRef<const sem::Type*> args,
                             TemplateState templates) const;
 
     /// Performs overload resolution given the list of candidates, by ranking the conversions of
@@ -1002,7 +1008,7 @@
     /// @returns the resolved Candidate.
     Candidate ResolveCandidate(Candidates&& candidates,
                                const char* intrinsic_name,
-                               const std::vector<const sem::Type*>& args,
+                               utils::VectorRef<const sem::Type*> args,
                                TemplateState templates) const;
 
     /// Match constructs a new MatchState
@@ -1020,14 +1026,14 @@
 
     // Prints the list of candidates for emitting diagnostics
     void PrintCandidates(std::ostream& ss,
-                         const Candidates& candidates,
+                         utils::ConstVectorRef<Candidate> candidates,
                          const char* intrinsic_name) const;
 
     /// Raises an error when no overload is a clear winner of overload resolution
     void ErrAmbiguousOverload(const char* intrinsic_name,
-                              const std::vector<const sem::Type*>& args,
+                              utils::ConstVectorRef<const sem::Type*> args,
                               TemplateState templates,
-                              Candidates candidates) const;
+                              utils::ConstVectorRef<Candidate> candidates) const;
 
     ProgramBuilder& builder;
     Matchers matchers;
@@ -1042,7 +1048,7 @@
 /// types.
 std::string CallSignature(ProgramBuilder& builder,
                           const char* intrinsic_name,
-                          const std::vector<const sem::Type*>& args,
+                          utils::VectorRef<const sem::Type*> args,
                           const sem::Type* template_arg = nullptr) {
     std::stringstream ss;
     ss << intrinsic_name;
@@ -1076,18 +1082,18 @@
 Impl::Impl(ProgramBuilder& b) : builder(b) {}
 
 Impl::Builtin Impl::Lookup(sem::BuiltinType builtin_type,
-                           const std::vector<const sem::Type*>& args,
+                           utils::VectorRef<const sem::Type*> args,
                            const Source& source) {
     const char* intrinsic_name = sem::str(builtin_type);
 
     // Generates an error when no overloads match the provided arguments
-    auto on_no_match = [&](Candidates candidates) {
+    auto on_no_match = [&](utils::VectorRef<Candidate> candidates) {
         std::stringstream ss;
         ss << "no matching call to " << CallSignature(builder, intrinsic_name, args) << std::endl;
-        if (!candidates.empty()) {
+        if (!candidates.IsEmpty()) {
             ss << std::endl
-               << candidates.size() << " candidate function" << (candidates.size() > 1 ? "s:" : ":")
-               << std::endl;
+               << candidates.Length() << " candidate function"
+               << (candidates.Length() > 1 ? "s:" : ":") << std::endl;
             PrintCandidates(ss, candidates, intrinsic_name);
         }
         builder.Diagnostics().add_error(diag::System::Resolver, ss.str(), source);
@@ -1102,11 +1108,11 @@
 
     // De-duplicate builtins that are identical.
     auto* sem = utils::GetOrCreate(builtins, match, [&] {
-        std::vector<sem::Parameter*> params;
-        params.reserve(match.parameters.size());
+        utils::Vector<sem::Parameter*, kNumFixedParams> params;
+        params.Reserve(match.parameters.Length());
         for (auto& p : match.parameters) {
-            params.emplace_back(builder.create<sem::Parameter>(
-                nullptr, static_cast<uint32_t>(params.size()), p.type, ast::StorageClass::kNone,
+            params.Push(builder.create<sem::Parameter>(
+                nullptr, static_cast<uint32_t>(params.Length()), p.type, ast::StorageClass::kNone,
                 ast::Access::kUndefined, p.usage));
         }
         sem::PipelineStageSet supported_stages;
@@ -1144,21 +1150,23 @@
         }
     }();
 
+    utils::Vector args{arg};
+
     // Generates an error when no overloads match the provided arguments
-    auto on_no_match = [&, name = intrinsic_name](Candidates candidates) {
+    auto on_no_match = [&, name = intrinsic_name](utils::VectorRef<Candidate> candidates) {
         std::stringstream ss;
-        ss << "no matching overload for " << CallSignature(builder, name, {arg}) << std::endl;
-        if (!candidates.empty()) {
+        ss << "no matching overload for " << CallSignature(builder, name, args) << std::endl;
+        if (!candidates.IsEmpty()) {
             ss << std::endl
-               << candidates.size() << " candidate operator" << (candidates.size() > 1 ? "s:" : ":")
-               << std::endl;
+               << candidates.Length() << " candidate operator"
+               << (candidates.Length() > 1 ? "s:" : ":") << std::endl;
             PrintCandidates(ss, candidates, name);
         }
         builder.Diagnostics().add_error(diag::System::Resolver, ss.str(), source);
     };
 
     // Resolve the intrinsic overload
-    auto match = MatchIntrinsic(kUnaryOperators[intrinsic_index], intrinsic_name, {arg},
+    auto match = MatchIntrinsic(kUnaryOperators[intrinsic_index], intrinsic_name, args,
                                 TemplateState{}, on_no_match);
     if (!match.overload) {
         return {};
@@ -1219,21 +1227,23 @@
         }
     }();
 
+    utils::Vector args{lhs, rhs};
+
     // Generates an error when no overloads match the provided arguments
-    auto on_no_match = [&, name = intrinsic_name](Candidates candidates) {
+    auto on_no_match = [&, name = intrinsic_name](utils::VectorRef<Candidate> candidates) {
         std::stringstream ss;
-        ss << "no matching overload for " << CallSignature(builder, name, {lhs, rhs}) << std::endl;
-        if (!candidates.empty()) {
+        ss << "no matching overload for " << CallSignature(builder, name, args) << std::endl;
+        if (!candidates.IsEmpty()) {
             ss << std::endl
-               << candidates.size() << " candidate operator" << (candidates.size() > 1 ? "s:" : ":")
-               << std::endl;
+               << candidates.Length() << " candidate operator"
+               << (candidates.Length() > 1 ? "s:" : ":") << std::endl;
             PrintCandidates(ss, candidates, name);
         }
         builder.Diagnostics().add_error(diag::System::Resolver, ss.str(), source);
     };
 
     // Resolve the intrinsic overload
-    auto match = MatchIntrinsic(kBinaryOperators[intrinsic_index], intrinsic_name, {lhs, rhs},
+    auto match = MatchIntrinsic(kBinaryOperators[intrinsic_index], intrinsic_name, args,
                                 TemplateState{}, on_no_match);
     if (!match.overload) {
         return {};
@@ -1249,32 +1259,32 @@
 
 IntrinsicTable::CtorOrConv Impl::Lookup(CtorConvIntrinsic type,
                                         const sem::Type* template_arg,
-                                        const std::vector<const sem::Type*>& args,
+                                        utils::VectorRef<const sem::Type*> args,
                                         const Source& source) {
     auto name = str(type);
 
     // Generates an error when no overloads match the provided arguments
-    auto on_no_match = [&](Candidates candidates) {
+    auto on_no_match = [&](utils::VectorRef<Candidate> candidates) {
         std::stringstream ss;
         ss << "no matching constructor for " << CallSignature(builder, name, args, template_arg)
            << std::endl;
         Candidates ctor, conv;
         for (auto candidate : candidates) {
             if (candidate.overload->flags.Contains(OverloadFlag::kIsConstructor)) {
-                ctor.emplace_back(candidate);
+                ctor.Push(candidate);
             } else {
-                conv.emplace_back(candidate);
+                conv.Push(candidate);
             }
         }
-        if (!ctor.empty()) {
+        if (!ctor.IsEmpty()) {
             ss << std::endl
-               << ctor.size() << " candidate constructor" << (ctor.size() > 1 ? "s:" : ":")
+               << ctor.Length() << " candidate constructor" << (ctor.Length() > 1 ? "s:" : ":")
                << std::endl;
             PrintCandidates(ss, ctor, name);
         }
-        if (!conv.empty()) {
+        if (!conv.IsEmpty()) {
             ss << std::endl
-               << conv.size() << " candidate conversion" << (conv.size() > 1 ? "s:" : ":")
+               << conv.Length() << " candidate conversion" << (conv.Length() > 1 ? "s:" : ":")
                << std::endl;
             PrintCandidates(ss, conv, name);
         }
@@ -1296,11 +1306,11 @@
 
     // Was this overload a constructor or conversion?
     if (match.overload->flags.Contains(OverloadFlag::kIsConstructor)) {
-        sem::ParameterList params;
-        params.reserve(match.parameters.size());
+        utils::Vector<const sem::Parameter*, 8> params;
+        params.Reserve(match.parameters.Length());
         for (auto& p : match.parameters) {
-            params.emplace_back(builder.create<sem::Parameter>(
-                nullptr, static_cast<uint32_t>(params.size()), p.type, ast::StorageClass::kNone,
+            params.Push(builder.create<sem::Parameter>(
+                nullptr, static_cast<uint32_t>(params.Length()), p.type, ast::StorageClass::kNone,
                 ast::Access::kUndefined, p.usage));
         }
         auto eval_stage = match.overload->const_eval_fn ? sem::EvaluationStage::kConstant
@@ -1326,13 +1336,13 @@
 
 IntrinsicPrototype Impl::MatchIntrinsic(const IntrinsicInfo& intrinsic,
                                         const char* intrinsic_name,
-                                        const std::vector<const sem::Type*>& args,
+                                        utils::VectorRef<const sem::Type*> args,
                                         TemplateState templates,
                                         OnNoMatch on_no_match) const {
     size_t num_matched = 0;
     size_t match_idx = 0;
-    Candidates candidates;
-    candidates.reserve(intrinsic.num_overloads);
+    utils::Vector<Candidate, kNumFixedCandidates> candidates;
+    candidates.Reserve(intrinsic.num_overloads);
     for (size_t overload_idx = 0; overload_idx < static_cast<size_t>(intrinsic.num_overloads);
          overload_idx++) {
         auto candidate = ScoreOverload(&intrinsic.overloads[overload_idx], args, templates);
@@ -1340,7 +1350,7 @@
             match_idx = overload_idx;
             num_matched++;
         }
-        candidates.emplace_back(std::move(candidate));
+        candidates.Push(std::move(candidate));
     }
 
     // How many candidates matched?
@@ -1380,7 +1390,7 @@
 }
 
 Impl::Candidate Impl::ScoreOverload(const OverloadInfo* overload,
-                                    const std::vector<const sem::Type*>& args,
+                                    utils::VectorRef<const sem::Type*> args,
                                     TemplateState templates) const {
     // Penalty weights for overload mismatching.
     // This scoring is used to order the suggested overloads in diagnostic on overload mismatch, and
@@ -1392,7 +1402,7 @@
     constexpr int kMismatchedTemplateNumberPenalty = 1;
 
     size_t num_parameters = static_cast<size_t>(overload->num_parameters);
-    size_t num_arguments = static_cast<size_t>(args.size());
+    size_t num_arguments = static_cast<size_t>(args.Length());
 
     size_t score = 0;
 
@@ -1459,14 +1469,14 @@
     }
 
     // Now that all the template types have been finalized, we can construct the parameters.
-    std::vector<IntrinsicPrototype::Parameter> parameters;
+    utils::Vector<IntrinsicPrototype::Parameter, kNumFixedParams> parameters;
     if (score == 0) {
-        parameters.reserve(num_params);
+        parameters.Reserve(num_params);
         for (size_t p = 0; p < num_params; p++) {
             auto& parameter = overload->parameters[p];
             auto* indices = parameter.matcher_indices;
             auto* ty = Match(templates, overload, indices).Type(args[p]->UnwrapRef());
-            parameters.emplace_back(IntrinsicPrototype::Parameter{ty, parameter.usage});
+            parameters.Emplace(ty, parameter.usage);
         }
     }
 
@@ -1475,9 +1485,10 @@
 
 Impl::Candidate Impl::ResolveCandidate(Impl::Candidates&& candidates,
                                        const char* intrinsic_name,
-                                       const std::vector<const sem::Type*>& args,
+                                       utils::VectorRef<const sem::Type*> args,
                                        TemplateState templates) const {
-    std::vector<uint32_t> best_ranks(args.size(), 0xffffffff);
+    utils::Vector<uint32_t, kNumFixedParams> best_ranks;
+    best_ranks.Resize(args.Length(), 0xffffffff);
     size_t num_matched = 0;
     Candidate* best = nullptr;
     for (auto& candidate : candidates) {
@@ -1486,7 +1497,7 @@
         }
         bool some_won = false;   // An argument ranked less than the 'best' overload's argument
         bool some_lost = false;  // An argument ranked more than the 'best' overload's argument
-        for (size_t i = 0; i < args.size(); i++) {
+        for (size_t i = 0; i < args.Length(); i++) {
             auto rank = sem::Type::ConversionRank(args[i], candidate.parameters[i].type);
             if (best_ranks[i] > rank) {
                 best_ranks[i] = rank;
@@ -1593,7 +1604,7 @@
 }
 
 void Impl::PrintCandidates(std::ostream& ss,
-                           const Candidates& candidates,
+                           utils::ConstVectorRef<Candidate> candidates,
                            const char* intrinsic_name) const {
     for (auto& candidate : candidates) {
         ss << "  ";
@@ -1627,9 +1638,9 @@
 }
 
 void Impl::ErrAmbiguousOverload(const char* intrinsic_name,
-                                const std::vector<const sem::Type*>& args,
+                                utils::ConstVectorRef<const sem::Type*> args,
                                 TemplateState templates,
-                                Candidates candidates) const {
+                                utils::ConstVectorRef<Candidate> candidates) const {
     std::stringstream ss;
     ss << "ambiguous overload while attempting to match " << intrinsic_name;
     for (size_t i = 0; i < std::numeric_limits<size_t>::max(); i++) {
diff --git a/src/tint/resolver/intrinsic_table.h b/src/tint/resolver/intrinsic_table.h
index 79eb82f..b36f8d8 100644
--- a/src/tint/resolver/intrinsic_table.h
+++ b/src/tint/resolver/intrinsic_table.h
@@ -17,13 +17,13 @@
 
 #include <memory>
 #include <string>
-#include <vector>
 
 #include "src/tint/ast/binary_expression.h"
 #include "src/tint/ast/unary_op.h"
 #include "src/tint/resolver/const_eval.h"
 #include "src/tint/resolver/ctor_conv_intrinsic.h"
 #include "src/tint/sem/builtin.h"
+#include "src/tint/utils/vector.h"
 
 // Forward declarations
 namespace tint {
@@ -87,7 +87,7 @@
     /// @param source the source of the builtin call
     /// @return the semantic builtin if found, otherwise nullptr
     virtual Builtin Lookup(sem::BuiltinType type,
-                           const std::vector<const sem::Type*>& args,
+                           utils::VectorRef<const sem::Type*> args,
                            const Source& source) = 0;
 
     /// Lookup looks for the unary op overload with the given signature, raising an error
@@ -123,7 +123,7 @@
     /// @return a sem::TypeConstructor, sem::TypeConversion or nullptr if nothing matched
     virtual CtorOrConv Lookup(CtorConvIntrinsic type,
                               const sem::Type* template_arg,
-                              const std::vector<const sem::Type*>& args,
+                              utils::VectorRef<const sem::Type*> args,
                               const Source& source) = 0;
 };
 
diff --git a/src/tint/resolver/intrinsic_table_test.cc b/src/tint/resolver/intrinsic_table_test.cc
index c246dd9..e8cb206 100644
--- a/src/tint/resolver/intrinsic_table_test.cc
+++ b/src/tint/resolver/intrinsic_table_test.cc
@@ -53,18 +53,18 @@
 
 TEST_F(IntrinsicTableTest, MatchF32) {
     auto* f32 = create<sem::F32>();
-    auto result = table->Lookup(BuiltinType::kCos, {f32}, Source{});
+    auto result = table->Lookup(BuiltinType::kCos, utils::Vector{f32}, Source{});
     ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_EQ(result.sem->Type(), BuiltinType::kCos);
     EXPECT_EQ(result.sem->ReturnType(), f32);
-    ASSERT_EQ(result.sem->Parameters().size(), 1u);
+    ASSERT_EQ(result.sem->Parameters().Length(), 1u);
     EXPECT_EQ(result.sem->Parameters()[0]->Type(), f32);
 }
 
 TEST_F(IntrinsicTableTest, MismatchF32) {
     auto* i32 = create<sem::I32>();
-    auto result = table->Lookup(BuiltinType::kCos, {i32}, Source{});
+    auto result = table->Lookup(BuiltinType::kCos, utils::Vector{i32}, Source{});
     ASSERT_EQ(result.sem, nullptr);
     ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
 }
@@ -73,18 +73,18 @@
     auto* f32 = create<sem::F32>();
     auto* u32 = create<sem::U32>();
     auto* vec2_f32 = create<sem::Vector>(f32, 2u);
-    auto result = table->Lookup(BuiltinType::kUnpack2x16float, {u32}, Source{});
+    auto result = table->Lookup(BuiltinType::kUnpack2x16float, utils::Vector{u32}, Source{});
     ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_EQ(result.sem->Type(), BuiltinType::kUnpack2x16float);
     EXPECT_EQ(result.sem->ReturnType(), vec2_f32);
-    ASSERT_EQ(result.sem->Parameters().size(), 1u);
+    ASSERT_EQ(result.sem->Parameters().Length(), 1u);
     EXPECT_EQ(result.sem->Parameters()[0]->Type(), u32);
 }
 
 TEST_F(IntrinsicTableTest, MismatchU32) {
     auto* f32 = create<sem::F32>();
-    auto result = table->Lookup(BuiltinType::kUnpack2x16float, {f32}, Source{});
+    auto result = table->Lookup(BuiltinType::kUnpack2x16float, utils::Vector{f32}, Source{});
     ASSERT_EQ(result.sem, nullptr);
     ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
 }
@@ -94,12 +94,12 @@
     auto* i32 = create<sem::I32>();
     auto* vec4_f32 = create<sem::Vector>(f32, 4u);
     auto* tex = create<sem::SampledTexture>(ast::TextureDimension::k1d, f32);
-    auto result = table->Lookup(BuiltinType::kTextureLoad, {tex, i32, i32}, Source{});
+    auto result = table->Lookup(BuiltinType::kTextureLoad, utils::Vector{tex, i32, i32}, Source{});
     ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_EQ(result.sem->Type(), BuiltinType::kTextureLoad);
     EXPECT_EQ(result.sem->ReturnType(), vec4_f32);
-    ASSERT_EQ(result.sem->Parameters().size(), 3u);
+    ASSERT_EQ(result.sem->Parameters().Length(), 3u);
     EXPECT_EQ(result.sem->Parameters()[0]->Type(), tex);
     EXPECT_EQ(result.sem->Parameters()[0]->Usage(), ParameterUsage::kTexture);
     EXPECT_EQ(result.sem->Parameters()[1]->Type(), i32);
@@ -111,48 +111,48 @@
 TEST_F(IntrinsicTableTest, MismatchI32) {
     auto* f32 = create<sem::F32>();
     auto* tex = create<sem::SampledTexture>(ast::TextureDimension::k1d, f32);
-    auto result = table->Lookup(BuiltinType::kTextureLoad, {tex, f32}, Source{});
+    auto result = table->Lookup(BuiltinType::kTextureLoad, utils::Vector{tex, f32}, Source{});
     ASSERT_EQ(result.sem, nullptr);
     ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
 }
 
 TEST_F(IntrinsicTableTest, MatchIU32AsI32) {
     auto* i32 = create<sem::I32>();
-    auto result = table->Lookup(BuiltinType::kCountOneBits, {i32}, Source{});
+    auto result = table->Lookup(BuiltinType::kCountOneBits, utils::Vector{i32}, Source{});
     ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_EQ(result.sem->Type(), BuiltinType::kCountOneBits);
     EXPECT_EQ(result.sem->ReturnType(), i32);
-    ASSERT_EQ(result.sem->Parameters().size(), 1u);
+    ASSERT_EQ(result.sem->Parameters().Length(), 1u);
     EXPECT_EQ(result.sem->Parameters()[0]->Type(), i32);
 }
 
 TEST_F(IntrinsicTableTest, MatchIU32AsU32) {
     auto* u32 = create<sem::U32>();
-    auto result = table->Lookup(BuiltinType::kCountOneBits, {u32}, Source{});
+    auto result = table->Lookup(BuiltinType::kCountOneBits, utils::Vector{u32}, Source{});
     ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_EQ(result.sem->Type(), BuiltinType::kCountOneBits);
     EXPECT_EQ(result.sem->ReturnType(), u32);
-    ASSERT_EQ(result.sem->Parameters().size(), 1u);
+    ASSERT_EQ(result.sem->Parameters().Length(), 1u);
     EXPECT_EQ(result.sem->Parameters()[0]->Type(), u32);
 }
 
 TEST_F(IntrinsicTableTest, MismatchIU32) {
     auto* f32 = create<sem::F32>();
-    auto result = table->Lookup(BuiltinType::kCountOneBits, {f32}, Source{});
+    auto result = table->Lookup(BuiltinType::kCountOneBits, utils::Vector{f32}, Source{});
     ASSERT_EQ(result.sem, nullptr);
     ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
 }
 
 TEST_F(IntrinsicTableTest, MatchFIU32AsI32) {
     auto* i32 = create<sem::I32>();
-    auto result = table->Lookup(BuiltinType::kClamp, {i32, i32, i32}, Source{});
+    auto result = table->Lookup(BuiltinType::kClamp, utils::Vector{i32, i32, i32}, Source{});
     ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_EQ(result.sem->Type(), BuiltinType::kClamp);
     EXPECT_EQ(result.sem->ReturnType(), i32);
-    ASSERT_EQ(result.sem->Parameters().size(), 3u);
+    ASSERT_EQ(result.sem->Parameters().Length(), 3u);
     EXPECT_EQ(result.sem->Parameters()[0]->Type(), i32);
     EXPECT_EQ(result.sem->Parameters()[1]->Type(), i32);
     EXPECT_EQ(result.sem->Parameters()[2]->Type(), i32);
@@ -160,12 +160,12 @@
 
 TEST_F(IntrinsicTableTest, MatchFIU32AsU32) {
     auto* u32 = create<sem::U32>();
-    auto result = table->Lookup(BuiltinType::kClamp, {u32, u32, u32}, Source{});
+    auto result = table->Lookup(BuiltinType::kClamp, utils::Vector{u32, u32, u32}, Source{});
     ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_EQ(result.sem->Type(), BuiltinType::kClamp);
     EXPECT_EQ(result.sem->ReturnType(), u32);
-    ASSERT_EQ(result.sem->Parameters().size(), 3u);
+    ASSERT_EQ(result.sem->Parameters().Length(), 3u);
     EXPECT_EQ(result.sem->Parameters()[0]->Type(), u32);
     EXPECT_EQ(result.sem->Parameters()[1]->Type(), u32);
     EXPECT_EQ(result.sem->Parameters()[2]->Type(), u32);
@@ -173,12 +173,12 @@
 
 TEST_F(IntrinsicTableTest, MatchFIU32AsF32) {
     auto* f32 = create<sem::F32>();
-    auto result = table->Lookup(BuiltinType::kClamp, {f32, f32, f32}, Source{});
+    auto result = table->Lookup(BuiltinType::kClamp, utils::Vector{f32, f32, f32}, Source{});
     ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_EQ(result.sem->Type(), BuiltinType::kClamp);
     EXPECT_EQ(result.sem->ReturnType(), f32);
-    ASSERT_EQ(result.sem->Parameters().size(), 3u);
+    ASSERT_EQ(result.sem->Parameters().Length(), 3u);
     EXPECT_EQ(result.sem->Parameters()[0]->Type(), f32);
     EXPECT_EQ(result.sem->Parameters()[1]->Type(), f32);
     EXPECT_EQ(result.sem->Parameters()[2]->Type(), f32);
@@ -186,7 +186,7 @@
 
 TEST_F(IntrinsicTableTest, MismatchFIU32) {
     auto* bool_ = create<sem::Bool>();
-    auto result = table->Lookup(BuiltinType::kClamp, {bool_, bool_, bool_}, Source{});
+    auto result = table->Lookup(BuiltinType::kClamp, utils::Vector{bool_, bool_, bool_}, Source{});
     ASSERT_EQ(result.sem, nullptr);
     ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
 }
@@ -194,12 +194,12 @@
 TEST_F(IntrinsicTableTest, MatchBool) {
     auto* f32 = create<sem::F32>();
     auto* bool_ = create<sem::Bool>();
-    auto result = table->Lookup(BuiltinType::kSelect, {f32, f32, bool_}, Source{});
+    auto result = table->Lookup(BuiltinType::kSelect, utils::Vector{f32, f32, bool_}, Source{});
     ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_EQ(result.sem->Type(), BuiltinType::kSelect);
     EXPECT_EQ(result.sem->ReturnType(), f32);
-    ASSERT_EQ(result.sem->Parameters().size(), 3u);
+    ASSERT_EQ(result.sem->Parameters().Length(), 3u);
     EXPECT_EQ(result.sem->Parameters()[0]->Type(), f32);
     EXPECT_EQ(result.sem->Parameters()[1]->Type(), f32);
     EXPECT_EQ(result.sem->Parameters()[2]->Type(), bool_);
@@ -207,7 +207,7 @@
 
 TEST_F(IntrinsicTableTest, MismatchBool) {
     auto* f32 = create<sem::F32>();
-    auto result = table->Lookup(BuiltinType::kSelect, {f32, f32, f32}, Source{});
+    auto result = table->Lookup(BuiltinType::kSelect, utils::Vector{f32, f32, f32}, Source{});
     ASSERT_EQ(result.sem, nullptr);
     ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
 }
@@ -217,19 +217,19 @@
     auto* atomicI32 = create<sem::Atomic>(i32);
     auto* ptr =
         create<sem::Pointer>(atomicI32, ast::StorageClass::kWorkgroup, ast::Access::kReadWrite);
-    auto result = table->Lookup(BuiltinType::kAtomicLoad, {ptr}, Source{});
+    auto result = table->Lookup(BuiltinType::kAtomicLoad, utils::Vector{ptr}, Source{});
     ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_EQ(result.sem->Type(), BuiltinType::kAtomicLoad);
     EXPECT_EQ(result.sem->ReturnType(), i32);
-    ASSERT_EQ(result.sem->Parameters().size(), 1u);
+    ASSERT_EQ(result.sem->Parameters().Length(), 1u);
     EXPECT_EQ(result.sem->Parameters()[0]->Type(), ptr);
 }
 
 TEST_F(IntrinsicTableTest, MismatchPointer) {
     auto* i32 = create<sem::I32>();
     auto* atomicI32 = create<sem::Atomic>(i32);
-    auto result = table->Lookup(BuiltinType::kAtomicLoad, {atomicI32}, Source{});
+    auto result = table->Lookup(BuiltinType::kAtomicLoad, utils::Vector{atomicI32}, Source{});
     ASSERT_EQ(result.sem, nullptr);
     ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
 }
@@ -237,12 +237,12 @@
 TEST_F(IntrinsicTableTest, MatchArray) {
     auto* arr = create<sem::Array>(create<sem::U32>(), 0u, 4u, 4u, 4u, 4u);
     auto* arr_ptr = create<sem::Pointer>(arr, ast::StorageClass::kStorage, ast::Access::kReadWrite);
-    auto result = table->Lookup(BuiltinType::kArrayLength, {arr_ptr}, Source{});
+    auto result = table->Lookup(BuiltinType::kArrayLength, utils::Vector{arr_ptr}, Source{});
     ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_EQ(result.sem->Type(), BuiltinType::kArrayLength);
     EXPECT_TRUE(result.sem->ReturnType()->Is<sem::U32>());
-    ASSERT_EQ(result.sem->Parameters().size(), 1u);
+    ASSERT_EQ(result.sem->Parameters().Length(), 1u);
     auto* param_type = result.sem->Parameters()[0]->Type();
     ASSERT_TRUE(param_type->Is<sem::Pointer>());
     EXPECT_TRUE(param_type->As<sem::Pointer>()->StoreType()->Is<sem::Array>());
@@ -250,7 +250,7 @@
 
 TEST_F(IntrinsicTableTest, MismatchArray) {
     auto* f32 = create<sem::F32>();
-    auto result = table->Lookup(BuiltinType::kArrayLength, {f32}, Source{});
+    auto result = table->Lookup(BuiltinType::kArrayLength, utils::Vector{f32}, Source{});
     ASSERT_EQ(result.sem, nullptr);
     ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
 }
@@ -261,12 +261,13 @@
     auto* vec4_f32 = create<sem::Vector>(f32, 4u);
     auto* tex = create<sem::SampledTexture>(ast::TextureDimension::k2d, f32);
     auto* sampler = create<sem::Sampler>(ast::SamplerKind::kSampler);
-    auto result = table->Lookup(BuiltinType::kTextureSample, {tex, sampler, vec2_f32}, Source{});
+    auto result =
+        table->Lookup(BuiltinType::kTextureSample, utils::Vector{tex, sampler, vec2_f32}, Source{});
     ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_EQ(result.sem->Type(), BuiltinType::kTextureSample);
     EXPECT_EQ(result.sem->ReturnType(), vec4_f32);
-    ASSERT_EQ(result.sem->Parameters().size(), 3u);
+    ASSERT_EQ(result.sem->Parameters().Length(), 3u);
     EXPECT_EQ(result.sem->Parameters()[0]->Type(), tex);
     EXPECT_EQ(result.sem->Parameters()[0]->Usage(), ParameterUsage::kTexture);
     EXPECT_EQ(result.sem->Parameters()[1]->Type(), sampler);
@@ -279,7 +280,8 @@
     auto* f32 = create<sem::F32>();
     auto* vec2_f32 = create<sem::Vector>(f32, 2u);
     auto* tex = create<sem::SampledTexture>(ast::TextureDimension::k2d, f32);
-    auto result = table->Lookup(BuiltinType::kTextureSample, {tex, f32, vec2_f32}, Source{});
+    auto result =
+        table->Lookup(BuiltinType::kTextureSample, utils::Vector{tex, f32, vec2_f32}, Source{});
     ASSERT_EQ(result.sem, nullptr);
     ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
 }
@@ -290,12 +292,13 @@
     auto* vec2_i32 = create<sem::Vector>(i32, 2u);
     auto* vec4_f32 = create<sem::Vector>(f32, 4u);
     auto* tex = create<sem::SampledTexture>(ast::TextureDimension::k2d, f32);
-    auto result = table->Lookup(BuiltinType::kTextureLoad, {tex, vec2_i32, i32}, Source{});
+    auto result =
+        table->Lookup(BuiltinType::kTextureLoad, utils::Vector{tex, vec2_i32, i32}, Source{});
     ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_EQ(result.sem->Type(), BuiltinType::kTextureLoad);
     EXPECT_EQ(result.sem->ReturnType(), vec4_f32);
-    ASSERT_EQ(result.sem->Parameters().size(), 3u);
+    ASSERT_EQ(result.sem->Parameters().Length(), 3u);
     EXPECT_EQ(result.sem->Parameters()[0]->Type(), tex);
     EXPECT_EQ(result.sem->Parameters()[0]->Usage(), ParameterUsage::kTexture);
     EXPECT_EQ(result.sem->Parameters()[1]->Type(), vec2_i32);
@@ -310,12 +313,13 @@
     auto* vec2_i32 = create<sem::Vector>(i32, 2u);
     auto* vec4_f32 = create<sem::Vector>(f32, 4u);
     auto* tex = create<sem::MultisampledTexture>(ast::TextureDimension::k2d, f32);
-    auto result = table->Lookup(BuiltinType::kTextureLoad, {tex, vec2_i32, i32}, Source{});
+    auto result =
+        table->Lookup(BuiltinType::kTextureLoad, utils::Vector{tex, vec2_i32, i32}, Source{});
     ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_EQ(result.sem->Type(), BuiltinType::kTextureLoad);
     EXPECT_EQ(result.sem->ReturnType(), vec4_f32);
-    ASSERT_EQ(result.sem->Parameters().size(), 3u);
+    ASSERT_EQ(result.sem->Parameters().Length(), 3u);
     EXPECT_EQ(result.sem->Parameters()[0]->Type(), tex);
     EXPECT_EQ(result.sem->Parameters()[0]->Usage(), ParameterUsage::kTexture);
     EXPECT_EQ(result.sem->Parameters()[1]->Type(), vec2_i32);
@@ -329,12 +333,13 @@
     auto* i32 = create<sem::I32>();
     auto* vec2_i32 = create<sem::Vector>(i32, 2u);
     auto* tex = create<sem::DepthTexture>(ast::TextureDimension::k2d);
-    auto result = table->Lookup(BuiltinType::kTextureLoad, {tex, vec2_i32, i32}, Source{});
+    auto result =
+        table->Lookup(BuiltinType::kTextureLoad, utils::Vector{tex, vec2_i32, i32}, Source{});
     ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_EQ(result.sem->Type(), BuiltinType::kTextureLoad);
     EXPECT_EQ(result.sem->ReturnType(), f32);
-    ASSERT_EQ(result.sem->Parameters().size(), 3u);
+    ASSERT_EQ(result.sem->Parameters().Length(), 3u);
     EXPECT_EQ(result.sem->Parameters()[0]->Type(), tex);
     EXPECT_EQ(result.sem->Parameters()[0]->Usage(), ParameterUsage::kTexture);
     EXPECT_EQ(result.sem->Parameters()[1]->Type(), vec2_i32);
@@ -348,12 +353,13 @@
     auto* i32 = create<sem::I32>();
     auto* vec2_i32 = create<sem::Vector>(i32, 2u);
     auto* tex = create<sem::DepthMultisampledTexture>(ast::TextureDimension::k2d);
-    auto result = table->Lookup(BuiltinType::kTextureLoad, {tex, vec2_i32, i32}, Source{});
+    auto result =
+        table->Lookup(BuiltinType::kTextureLoad, utils::Vector{tex, vec2_i32, i32}, Source{});
     ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_EQ(result.sem->Type(), BuiltinType::kTextureLoad);
     EXPECT_EQ(result.sem->ReturnType(), f32);
-    ASSERT_EQ(result.sem->Parameters().size(), 3u);
+    ASSERT_EQ(result.sem->Parameters().Length(), 3u);
     EXPECT_EQ(result.sem->Parameters()[0]->Type(), tex);
     EXPECT_EQ(result.sem->Parameters()[0]->Usage(), ParameterUsage::kTexture);
     EXPECT_EQ(result.sem->Parameters()[1]->Type(), vec2_i32);
@@ -368,12 +374,12 @@
     auto* vec2_i32 = create<sem::Vector>(i32, 2u);
     auto* vec4_f32 = create<sem::Vector>(f32, 4u);
     auto* tex = create<sem::ExternalTexture>();
-    auto result = table->Lookup(BuiltinType::kTextureLoad, {tex, vec2_i32}, Source{});
+    auto result = table->Lookup(BuiltinType::kTextureLoad, utils::Vector{tex, vec2_i32}, Source{});
     ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_EQ(result.sem->Type(), BuiltinType::kTextureLoad);
     EXPECT_EQ(result.sem->ReturnType(), vec4_f32);
-    ASSERT_EQ(result.sem->Parameters().size(), 2u);
+    ASSERT_EQ(result.sem->Parameters().Length(), 2u);
     EXPECT_EQ(result.sem->Parameters()[0]->Type(), tex);
     EXPECT_EQ(result.sem->Parameters()[0]->Usage(), ParameterUsage::kTexture);
     EXPECT_EQ(result.sem->Parameters()[1]->Type(), vec2_i32);
@@ -389,12 +395,13 @@
     auto* tex = create<sem::StorageTexture>(ast::TextureDimension::k2d, ast::TexelFormat::kR32Float,
                                             ast::Access::kWrite, subtype);
 
-    auto result = table->Lookup(BuiltinType::kTextureStore, {tex, vec2_i32, vec4_f32}, Source{});
+    auto result =
+        table->Lookup(BuiltinType::kTextureStore, utils::Vector{tex, vec2_i32, vec4_f32}, Source{});
     ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_EQ(result.sem->Type(), BuiltinType::kTextureStore);
     EXPECT_TRUE(result.sem->ReturnType()->Is<sem::Void>());
-    ASSERT_EQ(result.sem->Parameters().size(), 3u);
+    ASSERT_EQ(result.sem->Parameters().Length(), 3u);
     EXPECT_EQ(result.sem->Parameters()[0]->Type(), tex);
     EXPECT_EQ(result.sem->Parameters()[0]->Usage(), ParameterUsage::kTexture);
     EXPECT_EQ(result.sem->Parameters()[1]->Type(), vec2_i32);
@@ -407,7 +414,7 @@
     auto* f32 = create<sem::F32>();
     auto* i32 = create<sem::I32>();
     auto* vec2_i32 = create<sem::Vector>(i32, 2u);
-    auto result = table->Lookup(BuiltinType::kTextureLoad, {f32, vec2_i32}, Source{});
+    auto result = table->Lookup(BuiltinType::kTextureLoad, utils::Vector{f32, vec2_i32}, Source{});
     ASSERT_EQ(result.sem, nullptr);
     ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
 }
@@ -416,19 +423,21 @@
     auto* f32 = create<sem::F32>();
     auto result = table->Lookup(
         BuiltinType::kCos,
-        {create<sem::Reference>(f32, ast::StorageClass::kFunction, ast::Access::kReadWrite)},
+        utils::Vector{
+            create<sem::Reference>(f32, ast::StorageClass::kFunction, ast::Access::kReadWrite),
+        },
         Source{});
     ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_EQ(result.sem->Type(), BuiltinType::kCos);
     EXPECT_EQ(result.sem->ReturnType(), f32);
-    ASSERT_EQ(result.sem->Parameters().size(), 1u);
+    ASSERT_EQ(result.sem->Parameters().Length(), 1u);
     EXPECT_EQ(result.sem->Parameters()[0]->Type(), f32);
 }
 
 TEST_F(IntrinsicTableTest, MatchTemplateType) {
     auto* f32 = create<sem::F32>();
-    auto result = table->Lookup(BuiltinType::kClamp, {f32, f32, f32}, Source{});
+    auto result = table->Lookup(BuiltinType::kClamp, utils::Vector{f32, f32, f32}, Source{});
     ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_EQ(result.sem->Type(), BuiltinType::kClamp);
@@ -441,7 +450,7 @@
 TEST_F(IntrinsicTableTest, MismatchTemplateType) {
     auto* f32 = create<sem::F32>();
     auto* u32 = create<sem::U32>();
-    auto result = table->Lookup(BuiltinType::kClamp, {f32, u32, f32}, Source{});
+    auto result = table->Lookup(BuiltinType::kClamp, utils::Vector{f32, u32, f32}, Source{});
     ASSERT_EQ(result.sem, nullptr);
     ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
 }
@@ -449,12 +458,13 @@
 TEST_F(IntrinsicTableTest, MatchOpenSizeVector) {
     auto* f32 = create<sem::F32>();
     auto* vec2_f32 = create<sem::Vector>(f32, 2u);
-    auto result = table->Lookup(BuiltinType::kClamp, {vec2_f32, vec2_f32, vec2_f32}, Source{});
+    auto result =
+        table->Lookup(BuiltinType::kClamp, utils::Vector{vec2_f32, vec2_f32, vec2_f32}, Source{});
     ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_EQ(result.sem->Type(), BuiltinType::kClamp);
     EXPECT_EQ(result.sem->ReturnType(), vec2_f32);
-    ASSERT_EQ(result.sem->Parameters().size(), 3u);
+    ASSERT_EQ(result.sem->Parameters().Length(), 3u);
     EXPECT_EQ(result.sem->Parameters()[0]->Type(), vec2_f32);
     EXPECT_EQ(result.sem->Parameters()[1]->Type(), vec2_f32);
     EXPECT_EQ(result.sem->Parameters()[2]->Type(), vec2_f32);
@@ -464,7 +474,8 @@
     auto* f32 = create<sem::F32>();
     auto* u32 = create<sem::U32>();
     auto* vec2_f32 = create<sem::Vector>(f32, 2u);
-    auto result = table->Lookup(BuiltinType::kClamp, {vec2_f32, u32, vec2_f32}, Source{});
+    auto result =
+        table->Lookup(BuiltinType::kClamp, utils::Vector{vec2_f32, u32, vec2_f32}, Source{});
     ASSERT_EQ(result.sem, nullptr);
     ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
 }
@@ -473,12 +484,12 @@
     auto* f32 = create<sem::F32>();
     auto* vec3_f32 = create<sem::Vector>(f32, 3u);
     auto* mat3_f32 = create<sem::Matrix>(vec3_f32, 3u);
-    auto result = table->Lookup(BuiltinType::kDeterminant, {mat3_f32}, Source{});
+    auto result = table->Lookup(BuiltinType::kDeterminant, utils::Vector{mat3_f32}, Source{});
     ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
     EXPECT_EQ(result.sem->Type(), BuiltinType::kDeterminant);
     EXPECT_EQ(result.sem->ReturnType(), f32);
-    ASSERT_EQ(result.sem->Parameters().size(), 1u);
+    ASSERT_EQ(result.sem->Parameters().Length(), 1u);
     EXPECT_EQ(result.sem->Parameters()[0]->Type(), mat3_f32);
 }
 
@@ -486,7 +497,7 @@
     auto* f32 = create<sem::F32>();
     auto* vec2_f32 = create<sem::Vector>(f32, 2u);
     auto* mat3x2_f32 = create<sem::Matrix>(vec2_f32, 3u);
-    auto result = table->Lookup(BuiltinType::kDeterminant, {mat3x2_f32}, Source{});
+    auto result = table->Lookup(BuiltinType::kDeterminant, utils::Vector{mat3x2_f32}, Source{});
     ASSERT_EQ(result.sem, nullptr);
     ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
 }
@@ -495,7 +506,7 @@
     // None of the arguments match, so expect the overloads with 2 parameters to
     // come first
     auto* bool_ = create<sem::Bool>();
-    table->Lookup(BuiltinType::kTextureDimensions, {bool_, bool_}, Source{});
+    table->Lookup(BuiltinType::kTextureDimensions, utils::Vector{bool_, bool_}, Source{});
     ASSERT_EQ(Diagnostics().str(),
               R"(error: no matching call to textureDimensions(bool, bool)
 
@@ -533,7 +544,7 @@
 TEST_F(IntrinsicTableTest, OverloadOrderByMatchingParameter) {
     auto* tex = create<sem::DepthTexture>(ast::TextureDimension::k2d);
     auto* bool_ = create<sem::Bool>();
-    table->Lookup(BuiltinType::kTextureDimensions, {tex, bool_}, Source{});
+    table->Lookup(BuiltinType::kTextureDimensions, utils::Vector{tex, bool_}, Source{});
     ASSERT_EQ(Diagnostics().str(),
               R"(error: no matching call to textureDimensions(texture_depth_2d, bool)
 
@@ -572,14 +583,15 @@
     auto* f32 = create<sem::F32>();
     auto* vec2_f32 = create<sem::Vector>(create<sem::F32>(), 2u);
     auto* bool_ = create<sem::Bool>();
-    auto a = table->Lookup(BuiltinType::kSelect, {f32, f32, bool_}, Source{});
+    auto a = table->Lookup(BuiltinType::kSelect, utils::Vector{f32, f32, bool_}, Source{});
     ASSERT_NE(a.sem, nullptr) << Diagnostics().str();
 
-    auto b = table->Lookup(BuiltinType::kSelect, {f32, f32, bool_}, Source{});
+    auto b = table->Lookup(BuiltinType::kSelect, utils::Vector{f32, f32, bool_}, Source{});
     ASSERT_NE(b.sem, nullptr) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
 
-    auto c = table->Lookup(BuiltinType::kSelect, {vec2_f32, vec2_f32, bool_}, Source{});
+    auto c =
+        table->Lookup(BuiltinType::kSelect, utils::Vector{vec2_f32, vec2_f32, bool_}, Source{});
     ASSERT_NE(c.sem, nullptr) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
 
@@ -676,12 +688,12 @@
 TEST_F(IntrinsicTableTest, MatchTypeConstructorImplicit) {
     auto* i32 = create<sem::I32>();
     auto* vec3_i32 = create<sem::Vector>(i32, 3u);
-    auto result =
-        table->Lookup(CtorConvIntrinsic::kVec3, nullptr, {i32, i32, i32}, Source{{12, 34}});
+    auto result = table->Lookup(CtorConvIntrinsic::kVec3, nullptr, utils::Vector{i32, i32, i32},
+                                Source{{12, 34}});
     ASSERT_NE(result.target, nullptr);
     EXPECT_EQ(result.target->ReturnType(), vec3_i32);
     EXPECT_TRUE(result.target->Is<sem::TypeConstructor>());
-    ASSERT_EQ(result.target->Parameters().size(), 3u);
+    ASSERT_EQ(result.target->Parameters().Length(), 3u);
     EXPECT_EQ(result.target->Parameters()[0]->Type(), i32);
     EXPECT_EQ(result.target->Parameters()[1]->Type(), i32);
     EXPECT_EQ(result.target->Parameters()[2]->Type(), i32);
@@ -691,11 +703,12 @@
 TEST_F(IntrinsicTableTest, MatchTypeConstructorExplicit) {
     auto* i32 = create<sem::I32>();
     auto* vec3_i32 = create<sem::Vector>(i32, 3u);
-    auto result = table->Lookup(CtorConvIntrinsic::kVec3, i32, {i32, i32, i32}, Source{{12, 34}});
+    auto result = table->Lookup(CtorConvIntrinsic::kVec3, i32, utils::Vector{i32, i32, i32},
+                                Source{{12, 34}});
     ASSERT_NE(result.target, nullptr);
     EXPECT_EQ(result.target->ReturnType(), vec3_i32);
     EXPECT_TRUE(result.target->Is<sem::TypeConstructor>());
-    ASSERT_EQ(result.target->Parameters().size(), 3u);
+    ASSERT_EQ(result.target->Parameters().Length(), 3u);
     EXPECT_EQ(result.target->Parameters()[0]->Type(), i32);
     EXPECT_EQ(result.target->Parameters()[1]->Type(), i32);
     EXPECT_EQ(result.target->Parameters()[2]->Type(), i32);
@@ -705,10 +718,11 @@
 TEST_F(IntrinsicTableTest, MismatchTypeConstructorImplicit) {
     auto* i32 = create<sem::I32>();
     auto* f32 = create<sem::F32>();
-    auto result =
-        table->Lookup(CtorConvIntrinsic::kVec3, nullptr, {i32, f32, i32}, Source{{12, 34}});
+    auto result = table->Lookup(CtorConvIntrinsic::kVec3, nullptr, utils::Vector{i32, f32, i32},
+                                Source{{12, 34}});
     ASSERT_EQ(result.target, nullptr);
-    EXPECT_EQ(Diagnostics().str(), R"(12:34 error: no matching constructor for vec3(i32, f32, i32)
+    EXPECT_EQ(Diagnostics().str(),
+              R"(12:34 error: no matching constructor for vec3(i32, f32, i32)
 
 6 candidate constructors:
   vec3(x: T, y: T, z: T) -> vec3<T>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
@@ -730,7 +744,8 @@
 TEST_F(IntrinsicTableTest, MismatchTypeConstructorExplicit) {
     auto* i32 = create<sem::I32>();
     auto* f32 = create<sem::F32>();
-    auto result = table->Lookup(CtorConvIntrinsic::kVec3, i32, {i32, f32, i32}, Source{{12, 34}});
+    auto result = table->Lookup(CtorConvIntrinsic::kVec3, i32, utils::Vector{i32, f32, i32},
+                                Source{{12, 34}});
     ASSERT_EQ(result.target, nullptr);
     EXPECT_EQ(Diagnostics().str(),
               R"(12:34 error: no matching constructor for vec3<i32>(i32, f32, i32)
@@ -757,18 +772,20 @@
     auto* vec3_i32 = create<sem::Vector>(i32, 3u);
     auto* f32 = create<sem::F32>();
     auto* vec3_f32 = create<sem::Vector>(f32, 3u);
-    auto result = table->Lookup(CtorConvIntrinsic::kVec3, i32, {vec3_f32}, Source{{12, 34}});
+    auto result =
+        table->Lookup(CtorConvIntrinsic::kVec3, i32, utils::Vector{vec3_f32}, Source{{12, 34}});
     ASSERT_NE(result.target, nullptr);
     EXPECT_EQ(result.target->ReturnType(), vec3_i32);
     EXPECT_TRUE(result.target->Is<sem::TypeConversion>());
-    ASSERT_EQ(result.target->Parameters().size(), 1u);
+    ASSERT_EQ(result.target->Parameters().Length(), 1u);
     EXPECT_EQ(result.target->Parameters()[0]->Type(), vec3_f32);
 }
 
 TEST_F(IntrinsicTableTest, MismatchTypeConversion) {
     auto* arr = create<sem::Array>(create<sem::U32>(), 0u, 4u, 4u, 4u, 4u);
     auto* f32 = create<sem::F32>();
-    auto result = table->Lookup(CtorConvIntrinsic::kVec3, f32, {arr}, Source{{12, 34}});
+    auto result =
+        table->Lookup(CtorConvIntrinsic::kVec3, f32, utils::Vector{arr}, Source{{12, 34}});
     ASSERT_EQ(result.target, nullptr);
     EXPECT_EQ(Diagnostics().str(),
               R"(12:34 error: no matching constructor for vec3<f32>(array<u32>)
@@ -792,7 +809,8 @@
 
 TEST_F(IntrinsicTableTest, Err257Arguments) {  // crbug.com/1323605
     auto* f32 = create<sem::F32>();
-    std::vector<const sem::Type*> arg_tys(257, f32);
+    utils::Vector<const sem::Type*, 0> arg_tys;
+    arg_tys.Resize(257, f32);
     auto result = table->Lookup(BuiltinType::kAbs, std::move(arg_tys), Source{});
     ASSERT_EQ(result.sem, nullptr);
     ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
@@ -805,10 +823,10 @@
     // The first should win overload resolution.
     auto* ai = create<sem::AbstractInt>();
     auto* i32 = create<sem::I32>();
-    auto result = table->Lookup(CtorConvIntrinsic::kI32, nullptr, {ai}, Source{});
+    auto result = table->Lookup(CtorConvIntrinsic::kI32, nullptr, utils::Vector{ai}, Source{});
     ASSERT_NE(result.target, nullptr);
     EXPECT_EQ(result.target->ReturnType(), i32);
-    EXPECT_EQ(result.target->Parameters().size(), 1u);
+    EXPECT_EQ(result.target->Parameters().Length(), 1u);
     EXPECT_EQ(result.target->Parameters()[0]->Type(), i32);
 }
 
@@ -816,7 +834,6 @@
 // AbstractBinaryTests
 ////////////////////////////////////////////////////////////////////////////////
 namespace AbstractBinaryTests {
-
 struct Case {
     template <typename RESULT,
               typename PARAM_LHS,
@@ -991,7 +1008,6 @@
 // AbstractTernaryTests
 ////////////////////////////////////////////////////////////////////////////////
 namespace AbstractTernaryTests {
-
 struct Case {
     template <typename RESULT,
               typename PARAM_A,
@@ -1030,7 +1046,8 @@
     auto* arg_a = GetParam().arg_a(*this);
     auto* arg_b = GetParam().arg_b(*this);
     auto* arg_c = GetParam().arg_c(*this);
-    auto builtin = table->Lookup(sem::BuiltinType::kClamp, {arg_a, arg_b, arg_c}, Source{{12, 34}});
+    auto builtin = table->Lookup(sem::BuiltinType::kClamp, utils::Vector{arg_a, arg_b, arg_c},
+                                 Source{{12, 34}});
 
     bool matched = builtin.sem != nullptr;
     bool expected_match = GetParam().expected_match;
diff --git a/src/tint/resolver/resolver.cc b/src/tint/resolver/resolver.cc
index b418deb..b7b7586 100644
--- a/src/tint/resolver/resolver.cc
+++ b/src/tint/resolver/resolver.cc
@@ -85,6 +85,7 @@
 #include "src/tint/utils/reverse.h"
 #include "src/tint/utils/scoped_assignment.h"
 #include "src/tint/utils/transform.h"
+#include "src/tint/utils/vector.h"
 
 namespace tint::resolver {
 
@@ -720,7 +721,7 @@
 sem::Function* Resolver::Function(const ast::Function* decl) {
     uint32_t parameter_index = 0;
     std::unordered_map<Symbol, Source> parameter_names;
-    std::vector<sem::Parameter*> parameters;
+    utils::Vector<sem::Parameter*, 8> parameters;
 
     // Resolve all the parameters
     for (auto* param : decl->params) {
@@ -745,7 +746,7 @@
             return nullptr;
         }
 
-        parameters.emplace_back(p);
+        parameters.Push(p);
 
         auto* p_ty = const_cast<sem::Type*>(p->Type());
         if (auto* str = p_ty->As<sem::Struct>()) {
@@ -799,7 +800,7 @@
         }
     }
 
-    auto* func = builder_->create<sem::Function>(decl, return_type, parameters);
+    auto* func = builder_->create<sem::Function>(decl, return_type, std::move(parameters));
     builder_->Sem().Add(decl, func);
 
     TINT_SCOPED_ASSIGNMENT(current_function_, func);
@@ -1229,7 +1230,7 @@
 }
 
 sem::Expression* Resolver::Expression(const ast::Expression* root) {
-    std::vector<const ast::Expression*> sorted;
+    utils::Vector<const ast::Expression*, 128> sorted;
     constexpr size_t kMaxExpressionDepth = 512U;
     bool failed = false;
     if (!ast::TraverseExpressions<ast::TraverseOrder::RightToLeft>(
@@ -1245,7 +1246,7 @@
                     failed = true;
                     return ast::TraverseAction::Stop;
                 }
-                sorted.emplace_back(expr);
+                sorted.Push(expr);
                 return ast::TraverseAction::Descend;
             })) {
         return nullptr;
@@ -1377,9 +1378,9 @@
         [&](Default) { return expr; });
 }
 
-bool Resolver::MaterializeArguments(std::vector<const sem::Expression*>& args,
+bool Resolver::MaterializeArguments(utils::VectorRef<const sem::Expression*> args,
                                     const sem::CallTarget* target) {
-    for (size_t i = 0, n = std::min(args.size(), target->Parameters().size()); i < n; i++) {
+    for (size_t i = 0, n = std::min(args.Length(), target->Parameters().Length()); i < n; i++) {
         const auto* param_ty = target->Parameters()[i]->Type();
         if (ShouldMaterializeArgument(param_ty)) {
             auto* materialized = Materialize(args[i], param_ty);
@@ -1480,7 +1481,8 @@
     // * A type conversion.
 
     // Resolve all of the arguments, their types and the set of behaviors.
-    std::vector<const sem::Expression*> args(expr->args.size());
+    utils::Vector<const sem::Expression*, 8> args;
+    args.Reserve(expr->args.size());
     auto args_stage = sem::EvaluationStage::kConstant;
     sem::Behaviors arg_behaviors;
     for (size_t i = 0; i < expr->args.size(); i++) {
@@ -1488,7 +1490,7 @@
         if (!arg) {
             return nullptr;
         }
-        args[i] = arg;
+        args.Push(arg);
         args_stage = sem::EarliestStage(args_stage, arg->Stage());
         arg_behaviors.Add(arg->Behaviors());
     }
@@ -1512,8 +1514,8 @@
         const sem::Constant* value = nullptr;
         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());
+            value =
+                (const_eval_.*ctor_or_conv.const_eval_fn)(ctor_or_conv.target->ReturnType(), args);
         }
         return builder_->create<sem::Call>(expr, ctor_or_conv.target, stage, std::move(args),
                                            current_statement_, value, has_side_effects);
@@ -1563,16 +1565,17 @@
             [&](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(), args_stage}},
+                    array_ctors_, ArrayConstructorSig{{arr, args.Length(), args_stage}},
                     [&]() -> sem::TypeConstructor* {
-                        sem::ParameterList params(args.size());
-                        for (size_t i = 0; i < args.size(); i++) {
-                            params[i] = builder_->create<sem::Parameter>(
-                                nullptr,                   // declaration
-                                static_cast<uint32_t>(i),  // index
-                                arr->ElemType(),           // type
-                                ast::StorageClass::kNone,  // storage_class
-                                ast::Access::kUndefined);  // access
+                        utils::Vector<const sem::Parameter*, 8> params;
+                        params.Reserve(args.Length());
+                        for (size_t i = 0; i < args.Length(); i++) {
+                            params.Push(builder_->create<sem::Parameter>(
+                                nullptr,                    // declaration
+                                static_cast<uint32_t>(i),   // index
+                                arr->ElemType(),            // type
+                                ast::StorageClass::kNone,   // storage_class
+                                ast::Access::kUndefined));  // access
                         }
                         return builder_->create<sem::TypeConstructor>(arr, std::move(params),
                                                                       args_stage);
@@ -1581,10 +1584,11 @@
             },
             [&](const sem::Struct* str) -> sem::Call* {
                 auto* call_target = utils::GetOrCreate(
-                    struct_ctors_, StructConstructorSig{{str, args.size(), args_stage}},
+                    struct_ctors_, StructConstructorSig{{str, args.Length(), 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++) {
+                        utils::Vector<const sem::Parameter*, 8> params;
+                        params.Resize(std::min(args.Length(), str->Members().size()));
+                        for (size_t i = 0, n = params.Length(); i < n; i++) {
                             params[i] = builder_->create<sem::Parameter>(
                                 nullptr,                    // declaration
                                 static_cast<uint32_t>(i),   // index
@@ -1705,10 +1709,10 @@
 
 sem::Call* Resolver::BuiltinCall(const ast::CallExpression* expr,
                                  sem::BuiltinType builtin_type,
-                                 std::vector<const sem::Expression*> args) {
+                                 utils::VectorRef<const sem::Expression*> args) {
     IntrinsicTable::Builtin builtin;
     {
-        auto arg_tys = utils::Transform(args, [](auto* arg) { return arg->Type(); });
+        auto arg_tys = utils::Transform<8>(args, [](auto* arg) { return arg->Type(); });
         builtin = intrinsic_table_->Lookup(builtin_type, arg_tys, expr->source);
         if (!builtin.sem) {
             return nullptr;
@@ -1738,8 +1742,7 @@
     // If the builtin is @const, and all arguments have constant values, evaluate the builtin now.
     const sem::Constant* value = nullptr;
     if (stage == sem::EvaluationStage::kConstant) {
-        value = (const_eval_.*builtin.const_eval_fn)(builtin.sem->ReturnType(), args.data(),
-                                                     args.size());
+        value = (const_eval_.*builtin.const_eval_fn)(builtin.sem->ReturnType(), args);
     }
 
     bool has_side_effects =
@@ -1771,8 +1774,9 @@
     return call;
 }
 
-void Resolver::CollectTextureSamplerPairs(const sem::Builtin* builtin,
-                                          const std::vector<const sem::Expression*>& args) const {
+void Resolver::CollectTextureSamplerPairs(
+    const sem::Builtin* builtin,
+    utils::ConstVectorRef<const sem::Expression*> args) const {
     // Collect a texture/sampler pair for this builtin.
     const auto& signature = builtin->Signature();
     int texture_index = signature.IndexOf(sem::ParameterUsage::kTexture);
@@ -1792,7 +1796,7 @@
 
 sem::Call* Resolver::FunctionCall(const ast::CallExpression* expr,
                                   sem::Function* target,
-                                  std::vector<const sem::Expression*> args,
+                                  utils::VectorRef<const sem::Expression*> args,
                                   sem::Behaviors arg_behaviors) {
     auto sym = expr->target.name->symbol;
     auto name = builder_->Symbols().NameFor(sym);
@@ -1838,8 +1842,9 @@
     return call;
 }
 
-void Resolver::CollectTextureSamplerPairs(sem::Function* func,
-                                          const std::vector<const sem::Expression*>& args) const {
+void Resolver::CollectTextureSamplerPairs(
+    sem::Function* func,
+    utils::ConstVectorRef<const sem::Expression*> args) const {
     // Map all texture/sampler pairs from the target function to the
     // current function. These can only be global or parameter
     // variables. Resolve any parameter variables to the corresponding
@@ -1992,7 +1997,7 @@
     auto* source_var = object->SourceVariable();
 
     const sem::Type* ret = nullptr;
-    std::vector<uint32_t> swizzle;
+    utils::Vector<uint32_t, 4> swizzle;
 
     // Object may be a side-effecting expression (e.g. function call).
     bool has_side_effects = object && object->HasSideEffects();
@@ -2030,33 +2035,33 @@
         Mark(expr->member);
         std::string s = builder_->Symbols().NameFor(expr->member->symbol);
         auto size = s.size();
-        swizzle.reserve(s.size());
+        swizzle.Reserve(s.size());
 
         for (auto c : s) {
             switch (c) {
                 case 'x':
                 case 'r':
-                    swizzle.emplace_back(0);
+                    swizzle.Push(0u);
                     break;
                 case 'y':
                 case 'g':
-                    swizzle.emplace_back(1);
+                    swizzle.Push(1u);
                     break;
                 case 'z':
                 case 'b':
-                    swizzle.emplace_back(2);
+                    swizzle.Push(2u);
                     break;
                 case 'w':
                 case 'a':
-                    swizzle.emplace_back(3);
+                    swizzle.Push(3u);
                     break;
                 default:
                     AddError("invalid vector swizzle character",
-                             expr->member->source.Begin() + swizzle.size());
+                             expr->member->source.Begin() + swizzle.Length());
                     return nullptr;
             }
 
-            if (swizzle.back() >= vec->Width()) {
+            if (swizzle.Back() >= vec->Width()) {
                 AddError("invalid vector swizzle member", expr->member->source);
                 return nullptr;
             }
@@ -2126,8 +2131,7 @@
 
     const sem::Constant* value = nullptr;
     if (op.const_eval_fn) {
-        const sem::Expression* args[] = {lhs, rhs};
-        value = (const_eval_.*op.const_eval_fn)(op.result, args, 2u);
+        value = (const_eval_.*op.const_eval_fn)(op.result, utils::Vector{lhs, rhs});
     }
 
     bool has_side_effects = lhs->HasSideEffects() || rhs->HasSideEffects();
@@ -2203,7 +2207,7 @@
             stage = expr->Stage();
             if (stage == sem::EvaluationStage::kConstant) {
                 if (op.const_eval_fn) {
-                    value = (const_eval_.*op.const_eval_fn)(ty, &expr, 1u);
+                    value = (const_eval_.*op.const_eval_fn)(ty, utils::Vector{expr});
                 } else {
                     stage = sem::EvaluationStage::kRuntime;
                 }
diff --git a/src/tint/resolver/resolver.h b/src/tint/resolver/resolver.h
index 4a1c250..03f1b9e 100644
--- a/src/tint/resolver/resolver.h
+++ b/src/tint/resolver/resolver.h
@@ -195,12 +195,12 @@
     sem::Function* Function(const ast::Function*);
     sem::Call* FunctionCall(const ast::CallExpression*,
                             sem::Function* target,
-                            std::vector<const sem::Expression*> args,
+                            utils::VectorRef<const sem::Expression*> args,
                             sem::Behaviors arg_behaviors);
     sem::Expression* Identifier(const ast::IdentifierExpression*);
     sem::Call* BuiltinCall(const ast::CallExpression*,
                            sem::BuiltinType,
-                           std::vector<const sem::Expression*> args);
+                           utils::VectorRef<const sem::Expression*> args);
     sem::Expression* Literal(const ast::LiteralExpression*);
     sem::Expression* MemberAccessor(const ast::MemberAccessorExpression*);
     sem::Expression* UnaryOp(const ast::UnaryOpExpression*);
@@ -223,7 +223,7 @@
 
     /// Materializes all the arguments in `args` to the parameter types of `target`.
     /// @returns true on success, false on failure.
-    bool MaterializeArguments(std::vector<const sem::Expression*>& args,
+    bool MaterializeArguments(utils::VectorRef<const sem::Expression*> args,
                               const sem::CallTarget* target);
 
     /// @returns true if an argument of an abstract numeric type, passed to a parameter of type
@@ -257,9 +257,9 @@
     // CollectTextureSamplerPairs() collects all the texture/sampler pairs from the target function
     // / builtin, and records these on the current function by calling AddTextureSamplerPair().
     void CollectTextureSamplerPairs(sem::Function* func,
-                                    const std::vector<const sem::Expression*>& args) const;
+                                    utils::ConstVectorRef<const sem::Expression*> args) const;
     void CollectTextureSamplerPairs(const sem::Builtin* builtin,
-                                    const std::vector<const sem::Expression*>& args) const;
+                                    utils::ConstVectorRef<const sem::Expression*> args) const;
 
     /// Resolves the WorkgroupSize for the given function, assigning it to
     /// current_function_
diff --git a/src/tint/resolver/resolver_test.cc b/src/tint/resolver/resolver_test.cc
index 9b56c31..bacbcd6 100644
--- a/src/tint/resolver/resolver_test.cc
+++ b/src/tint/resolver/resolver_test.cc
@@ -763,7 +763,7 @@
 
     auto* func_sem = Sem().Get(func);
     ASSERT_NE(func_sem, nullptr);
-    EXPECT_EQ(func_sem->Parameters().size(), 3u);
+    EXPECT_EQ(func_sem->Parameters().Length(), 3u);
     EXPECT_TRUE(func_sem->Parameters()[0]->Type()->Is<sem::F32>());
     EXPECT_TRUE(func_sem->Parameters()[1]->Type()->Is<sem::I32>());
     EXPECT_TRUE(func_sem->Parameters()[2]->Type()->Is<sem::U32>());
@@ -796,7 +796,7 @@
 
     auto* func_sem = Sem().Get(func);
     ASSERT_NE(func_sem, nullptr);
-    EXPECT_EQ(func_sem->Parameters().size(), 0u);
+    EXPECT_EQ(func_sem->Parameters().Length(), 0u);
     EXPECT_TRUE(func_sem->ReturnType()->Is<sem::Void>());
 
     const auto& vars = func_sem->TransitivelyReferencedGlobals();
@@ -832,7 +832,7 @@
 
     auto* func2_sem = Sem().Get(func2);
     ASSERT_NE(func2_sem, nullptr);
-    EXPECT_EQ(func2_sem->Parameters().size(), 0u);
+    EXPECT_EQ(func2_sem->Parameters().Length(), 0u);
 
     const auto& vars = func2_sem->TransitivelyReferencedGlobals();
     ASSERT_EQ(vars.size(), 3u);
@@ -1867,9 +1867,9 @@
     ASSERT_NE(ep_1_sem, nullptr);
     ASSERT_NE(ep_2_sem, nullptr);
 
-    EXPECT_EQ(func_b_sem->Parameters().size(), 0u);
-    EXPECT_EQ(func_a_sem->Parameters().size(), 0u);
-    EXPECT_EQ(func_c_sem->Parameters().size(), 0u);
+    EXPECT_EQ(func_b_sem->Parameters().Length(), 0u);
+    EXPECT_EQ(func_a_sem->Parameters().Length(), 0u);
+    EXPECT_EQ(func_c_sem->Parameters().Length(), 0u);
 
     const auto& b_eps = func_b_sem->AncestorEntryPoints();
     ASSERT_EQ(2u, b_eps.size());
diff --git a/src/tint/resolver/type_constructor_validation_test.cc b/src/tint/resolver/type_constructor_validation_test.cc
index 41cc698..0fb01bc 100644
--- a/src/tint/resolver/type_constructor_validation_test.cc
+++ b/src/tint/resolver/type_constructor_validation_test.cc
@@ -371,7 +371,7 @@
             auto* ctor = call->Target()->As<sem::TypeConstructor>();
             ASSERT_NE(ctor, nullptr);
             EXPECT_EQ(call->Type(), ctor->ReturnType());
-            ASSERT_EQ(ctor->Parameters().size(), 1u);
+            ASSERT_EQ(ctor->Parameters().Length(), 1u);
             EXPECT_EQ(ctor->Parameters()[0]->Type(), TypeOf(arg));
             break;
         }
@@ -379,7 +379,7 @@
             auto* conv = call->Target()->As<sem::TypeConversion>();
             ASSERT_NE(conv, nullptr);
             EXPECT_EQ(call->Type(), conv->ReturnType());
-            ASSERT_EQ(conv->Parameters().size(), 1u);
+            ASSERT_EQ(conv->Parameters().Length(), 1u);
             EXPECT_EQ(conv->Parameters()[0]->Type(), TypeOf(arg));
             break;
         }
@@ -499,7 +499,7 @@
     auto* ctor = call->Target()->As<sem::TypeConstructor>();
     ASSERT_NE(ctor, nullptr);
     EXPECT_EQ(call->Type(), ctor->ReturnType());
-    ASSERT_EQ(ctor->Parameters().size(), 0u);
+    ASSERT_EQ(ctor->Parameters().Length(), 0u);
 }
 
 TEST_F(ResolverTypeConstructorValidationTest, Expr_Constructor_Array_type_match) {
@@ -515,7 +515,7 @@
     auto* ctor = call->Target()->As<sem::TypeConstructor>();
     ASSERT_NE(ctor, nullptr);
     EXPECT_EQ(call->Type(), ctor->ReturnType());
-    ASSERT_EQ(ctor->Parameters().size(), 3u);
+    ASSERT_EQ(ctor->Parameters().Length(), 3u);
     EXPECT_TRUE(ctor->Parameters()[0]->Type()->Is<sem::U32>());
     EXPECT_TRUE(ctor->Parameters()[1]->Type()->Is<sem::U32>());
     EXPECT_TRUE(ctor->Parameters()[2]->Type()->Is<sem::U32>());
@@ -686,7 +686,7 @@
     auto* ctor = call->Target()->As<sem::TypeConstructor>();
     ASSERT_NE(ctor, nullptr);
     EXPECT_EQ(call->Type(), ctor->ReturnType());
-    ASSERT_EQ(ctor->Parameters().size(), 1u);
+    ASSERT_EQ(ctor->Parameters().Length(), 1u);
     EXPECT_TRUE(ctor->Parameters()[0]->Type()->Is<sem::I32>());
 }
 
@@ -704,7 +704,7 @@
     auto* ctor = call->Target()->As<sem::TypeConstructor>();
     ASSERT_NE(ctor, nullptr);
     EXPECT_EQ(call->Type(), ctor->ReturnType());
-    ASSERT_EQ(ctor->Parameters().size(), 1u);
+    ASSERT_EQ(ctor->Parameters().Length(), 1u);
     EXPECT_TRUE(ctor->Parameters()[0]->Type()->Is<sem::U32>());
 }
 
@@ -722,7 +722,7 @@
     auto* ctor = call->Target()->As<sem::TypeConstructor>();
     ASSERT_NE(ctor, nullptr);
     EXPECT_EQ(call->Type(), ctor->ReturnType());
-    ASSERT_EQ(ctor->Parameters().size(), 1u);
+    ASSERT_EQ(ctor->Parameters().Length(), 1u);
     EXPECT_TRUE(ctor->Parameters()[0]->Type()->Is<sem::F32>());
 }
 
@@ -742,7 +742,7 @@
     auto* ctor = call->Target()->As<sem::TypeConstructor>();
     ASSERT_NE(ctor, nullptr);
     EXPECT_EQ(call->Type(), ctor->ReturnType());
-    ASSERT_EQ(ctor->Parameters().size(), 1u);
+    ASSERT_EQ(ctor->Parameters().Length(), 1u);
     EXPECT_TRUE(ctor->Parameters()[0]->Type()->Is<sem::F16>());
 }
 
@@ -760,7 +760,7 @@
     auto* ctor = call->Target()->As<sem::TypeConversion>();
     ASSERT_NE(ctor, nullptr);
     EXPECT_EQ(call->Type(), ctor->ReturnType());
-    ASSERT_EQ(ctor->Parameters().size(), 1u);
+    ASSERT_EQ(ctor->Parameters().Length(), 1u);
     EXPECT_TRUE(ctor->Parameters()[0]->Type()->Is<sem::F32>());
 }
 
@@ -778,7 +778,7 @@
     auto* ctor = call->Target()->As<sem::TypeConversion>();
     ASSERT_NE(ctor, nullptr);
     EXPECT_EQ(call->Type(), ctor->ReturnType());
-    ASSERT_EQ(ctor->Parameters().size(), 1u);
+    ASSERT_EQ(ctor->Parameters().Length(), 1u);
     EXPECT_TRUE(ctor->Parameters()[0]->Type()->Is<sem::I32>());
 }
 
@@ -798,7 +798,7 @@
     auto* ctor = call->Target()->As<sem::TypeConversion>();
     ASSERT_NE(ctor, nullptr);
     EXPECT_EQ(call->Type(), ctor->ReturnType());
-    ASSERT_EQ(ctor->Parameters().size(), 1u);
+    ASSERT_EQ(ctor->Parameters().Length(), 1u);
     EXPECT_TRUE(ctor->Parameters()[0]->Type()->Is<sem::U32>());
 }
 
@@ -818,7 +818,7 @@
     auto* ctor = call->Target()->As<sem::TypeConversion>();
     ASSERT_NE(ctor, nullptr);
     EXPECT_EQ(call->Type(), ctor->ReturnType());
-    ASSERT_EQ(ctor->Parameters().size(), 1u);
+    ASSERT_EQ(ctor->Parameters().Length(), 1u);
     EXPECT_TRUE(ctor->Parameters()[0]->Type()->Is<sem::F16>());
 }
 
@@ -941,7 +941,7 @@
     auto* ctor = call->Target()->As<sem::TypeConstructor>();
     ASSERT_NE(ctor, nullptr);
     EXPECT_EQ(call->Type(), ctor->ReturnType());
-    ASSERT_EQ(ctor->Parameters().size(), 0u);
+    ASSERT_EQ(ctor->Parameters().Length(), 0u);
 }
 
 TEST_F(ResolverTypeConstructorValidationTest, Expr_Constructor_Vec2F32_Success_Scalar) {
@@ -960,7 +960,7 @@
     auto* ctor = call->Target()->As<sem::TypeConstructor>();
     ASSERT_NE(ctor, nullptr);
     EXPECT_EQ(call->Type(), ctor->ReturnType());
-    ASSERT_EQ(ctor->Parameters().size(), 2u);
+    ASSERT_EQ(ctor->Parameters().Length(), 2u);
     EXPECT_TRUE(ctor->Parameters()[0]->Type()->Is<sem::F32>());
     EXPECT_TRUE(ctor->Parameters()[1]->Type()->Is<sem::F32>());
 }
@@ -983,7 +983,7 @@
     auto* ctor = call->Target()->As<sem::TypeConstructor>();
     ASSERT_NE(ctor, nullptr);
     EXPECT_EQ(call->Type(), ctor->ReturnType());
-    ASSERT_EQ(ctor->Parameters().size(), 2u);
+    ASSERT_EQ(ctor->Parameters().Length(), 2u);
     EXPECT_TRUE(ctor->Parameters()[0]->Type()->Is<sem::F16>());
     EXPECT_TRUE(ctor->Parameters()[1]->Type()->Is<sem::F16>());
 }
@@ -1004,7 +1004,7 @@
     auto* ctor = call->Target()->As<sem::TypeConstructor>();
     ASSERT_NE(ctor, nullptr);
     EXPECT_EQ(call->Type(), ctor->ReturnType());
-    ASSERT_EQ(ctor->Parameters().size(), 2u);
+    ASSERT_EQ(ctor->Parameters().Length(), 2u);
     EXPECT_TRUE(ctor->Parameters()[0]->Type()->Is<sem::U32>());
     EXPECT_TRUE(ctor->Parameters()[1]->Type()->Is<sem::U32>());
 }
@@ -1025,7 +1025,7 @@
     auto* ctor = call->Target()->As<sem::TypeConstructor>();
     ASSERT_NE(ctor, nullptr);
     EXPECT_EQ(call->Type(), ctor->ReturnType());
-    ASSERT_EQ(ctor->Parameters().size(), 2u);
+    ASSERT_EQ(ctor->Parameters().Length(), 2u);
     EXPECT_TRUE(ctor->Parameters()[0]->Type()->Is<sem::I32>());
     EXPECT_TRUE(ctor->Parameters()[1]->Type()->Is<sem::I32>());
 }
@@ -1046,7 +1046,7 @@
     auto* ctor = call->Target()->As<sem::TypeConstructor>();
     ASSERT_NE(ctor, nullptr);
     EXPECT_EQ(call->Type(), ctor->ReturnType());
-    ASSERT_EQ(ctor->Parameters().size(), 2u);
+    ASSERT_EQ(ctor->Parameters().Length(), 2u);
     EXPECT_TRUE(ctor->Parameters()[0]->Type()->Is<sem::Bool>());
     EXPECT_TRUE(ctor->Parameters()[1]->Type()->Is<sem::Bool>());
 }
@@ -1067,7 +1067,7 @@
     auto* ctor = call->Target()->As<sem::TypeConstructor>();
     ASSERT_NE(ctor, nullptr);
     EXPECT_EQ(call->Type(), ctor->ReturnType());
-    ASSERT_EQ(ctor->Parameters().size(), 1u);
+    ASSERT_EQ(ctor->Parameters().Length(), 1u);
     EXPECT_TRUE(ctor->Parameters()[0]->Type()->Is<sem::Vector>());
 }
 
@@ -1087,7 +1087,7 @@
     auto* ctor = call->Target()->As<sem::TypeConversion>();
     ASSERT_NE(ctor, nullptr);
     EXPECT_EQ(call->Type(), ctor->ReturnType());
-    ASSERT_EQ(ctor->Parameters().size(), 1u);
+    ASSERT_EQ(ctor->Parameters().Length(), 1u);
     EXPECT_TRUE(ctor->Parameters()[0]->Type()->Is<sem::Vector>());
 }
 
@@ -1223,7 +1223,7 @@
     auto* ctor = call->Target()->As<sem::TypeConstructor>();
     ASSERT_NE(ctor, nullptr);
     EXPECT_EQ(call->Type(), ctor->ReturnType());
-    ASSERT_EQ(ctor->Parameters().size(), 0u);
+    ASSERT_EQ(ctor->Parameters().Length(), 0u);
 }
 
 TEST_F(ResolverTypeConstructorValidationTest, Expr_Constructor_Vec3F32_Success_Scalar) {
@@ -1242,7 +1242,7 @@
     auto* ctor = call->Target()->As<sem::TypeConstructor>();
     ASSERT_NE(ctor, nullptr);
     EXPECT_EQ(call->Type(), ctor->ReturnType());
-    ASSERT_EQ(ctor->Parameters().size(), 3u);
+    ASSERT_EQ(ctor->Parameters().Length(), 3u);
     EXPECT_TRUE(ctor->Parameters()[0]->Type()->Is<sem::F32>());
     EXPECT_TRUE(ctor->Parameters()[1]->Type()->Is<sem::F32>());
     EXPECT_TRUE(ctor->Parameters()[2]->Type()->Is<sem::F32>());
@@ -1266,7 +1266,7 @@
     auto* ctor = call->Target()->As<sem::TypeConstructor>();
     ASSERT_NE(ctor, nullptr);
     EXPECT_EQ(call->Type(), ctor->ReturnType());
-    ASSERT_EQ(ctor->Parameters().size(), 3u);
+    ASSERT_EQ(ctor->Parameters().Length(), 3u);
     EXPECT_TRUE(ctor->Parameters()[0]->Type()->Is<sem::F16>());
     EXPECT_TRUE(ctor->Parameters()[1]->Type()->Is<sem::F16>());
     EXPECT_TRUE(ctor->Parameters()[2]->Type()->Is<sem::F16>());
@@ -1288,7 +1288,7 @@
     auto* ctor = call->Target()->As<sem::TypeConstructor>();
     ASSERT_NE(ctor, nullptr);
     EXPECT_EQ(call->Type(), ctor->ReturnType());
-    ASSERT_EQ(ctor->Parameters().size(), 3u);
+    ASSERT_EQ(ctor->Parameters().Length(), 3u);
     EXPECT_TRUE(ctor->Parameters()[0]->Type()->Is<sem::U32>());
     EXPECT_TRUE(ctor->Parameters()[1]->Type()->Is<sem::U32>());
     EXPECT_TRUE(ctor->Parameters()[2]->Type()->Is<sem::U32>());
@@ -1310,7 +1310,7 @@
     auto* ctor = call->Target()->As<sem::TypeConstructor>();
     ASSERT_NE(ctor, nullptr);
     EXPECT_EQ(call->Type(), ctor->ReturnType());
-    ASSERT_EQ(ctor->Parameters().size(), 3u);
+    ASSERT_EQ(ctor->Parameters().Length(), 3u);
     EXPECT_TRUE(ctor->Parameters()[0]->Type()->Is<sem::I32>());
     EXPECT_TRUE(ctor->Parameters()[1]->Type()->Is<sem::I32>());
     EXPECT_TRUE(ctor->Parameters()[2]->Type()->Is<sem::I32>());
@@ -1332,7 +1332,7 @@
     auto* ctor = call->Target()->As<sem::TypeConstructor>();
     ASSERT_NE(ctor, nullptr);
     EXPECT_EQ(call->Type(), ctor->ReturnType());
-    ASSERT_EQ(ctor->Parameters().size(), 3u);
+    ASSERT_EQ(ctor->Parameters().Length(), 3u);
     EXPECT_TRUE(ctor->Parameters()[0]->Type()->Is<sem::Bool>());
     EXPECT_TRUE(ctor->Parameters()[1]->Type()->Is<sem::Bool>());
     EXPECT_TRUE(ctor->Parameters()[2]->Type()->Is<sem::Bool>());
@@ -1354,7 +1354,7 @@
     auto* ctor = call->Target()->As<sem::TypeConstructor>();
     ASSERT_NE(ctor, nullptr);
     EXPECT_EQ(call->Type(), ctor->ReturnType());
-    ASSERT_EQ(ctor->Parameters().size(), 2u);
+    ASSERT_EQ(ctor->Parameters().Length(), 2u);
     EXPECT_TRUE(ctor->Parameters()[0]->Type()->Is<sem::Vector>());
     EXPECT_TRUE(ctor->Parameters()[1]->Type()->Is<sem::F32>());
 }
@@ -1375,7 +1375,7 @@
     auto* ctor = call->Target()->As<sem::TypeConstructor>();
     ASSERT_NE(ctor, nullptr);
     EXPECT_EQ(call->Type(), ctor->ReturnType());
-    ASSERT_EQ(ctor->Parameters().size(), 2u);
+    ASSERT_EQ(ctor->Parameters().Length(), 2u);
     EXPECT_TRUE(ctor->Parameters()[0]->Type()->Is<sem::F32>());
     EXPECT_TRUE(ctor->Parameters()[1]->Type()->Is<sem::Vector>());
 }
@@ -1396,7 +1396,7 @@
     auto* ctor = call->Target()->As<sem::TypeConstructor>();
     ASSERT_NE(ctor, nullptr);
     EXPECT_EQ(call->Type(), ctor->ReturnType());
-    ASSERT_EQ(ctor->Parameters().size(), 1u);
+    ASSERT_EQ(ctor->Parameters().Length(), 1u);
     EXPECT_TRUE(ctor->Parameters()[0]->Type()->Is<sem::Vector>());
 }
 
@@ -1416,7 +1416,7 @@
     auto* ctor = call->Target()->As<sem::TypeConversion>();
     ASSERT_NE(ctor, nullptr);
     EXPECT_EQ(call->Type(), ctor->ReturnType());
-    ASSERT_EQ(ctor->Parameters().size(), 1u);
+    ASSERT_EQ(ctor->Parameters().Length(), 1u);
     EXPECT_TRUE(ctor->Parameters()[0]->Type()->Is<sem::Vector>());
 }
 
diff --git a/src/tint/resolver/validator.cc b/src/tint/resolver/validator.cc
index f962e3b..b7776fa 100644
--- a/src/tint/resolver/validator.cc
+++ b/src/tint/resolver/validator.cc
@@ -1602,9 +1602,14 @@
 bool Validator::BuiltinCall(const sem::Call* call) const {
     if (call->Type()->Is<sem::Void>()) {
         bool is_call_statement = false;
-        if (auto* call_stmt = As<ast::CallStatement>(call->Stmt()->Declaration())) {
-            if (call_stmt->expr == call->Declaration()) {
-                is_call_statement = true;
+        // Some built-in call are not owned by a statement, e.g. a built-in called in global
+        // variable declaration. Calling no-return-value built-in in these context is invalid as
+        // well.
+        if (auto* call_stmt = call->Stmt()) {
+            if (auto* call_stmt_ast = As<ast::CallStatement>(call_stmt->Declaration())) {
+                if (call_stmt_ast->expr == call->Declaration()) {
+                    is_call_statement = true;
+                }
             }
         }
         if (!is_call_statement) {
@@ -1734,17 +1739,17 @@
         return false;
     }
 
-    if (decl->args.size() != target->Parameters().size()) {
-        bool more = decl->args.size() > target->Parameters().size();
+    if (decl->args.size() != target->Parameters().Length()) {
+        bool more = decl->args.size() > target->Parameters().Length();
         AddError("too " + (more ? std::string("many") : std::string("few")) +
                      " arguments in call to '" + name + "', expected " +
-                     std::to_string(target->Parameters().size()) + ", got " +
-                     std::to_string(call->Arguments().size()),
+                     std::to_string(target->Parameters().Length()) + ", got " +
+                     std::to_string(call->Arguments().Length()),
                  decl->source);
         return false;
     }
 
-    for (size_t i = 0; i < call->Arguments().size(); ++i) {
+    for (size_t i = 0; i < call->Arguments().Length(); ++i) {
         const sem::Variable* param = target->Parameters()[i];
         const ast::Expression* arg_expr = decl->args[i];
         auto* param_type = param->Type();
diff --git a/src/tint/resolver/variable_validation_test.cc b/src/tint/resolver/variable_validation_test.cc
index 5625f00..e13585f 100644
--- a/src/tint/resolver/variable_validation_test.cc
+++ b/src/tint/resolver/variable_validation_test.cc
@@ -40,6 +40,24 @@
     EXPECT_EQ(r()->error(), "12:34 error: var declaration requires a type or initializer");
 }
 
+TEST_F(ResolverVariableValidationTest, VarInitializerNoReturnValueBuiltin) {
+    // fn f() { var a = storageBarrier(); }
+    auto* NoReturnValueBuiltin = Call(Source{{12, 34}}, "storageBarrier");
+    WrapInFunction(Var("a", nullptr, ast::StorageClass::kNone, NoReturnValueBuiltin));
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), "12:34 error: builtin 'storageBarrier' does not return a value");
+}
+
+TEST_F(ResolverVariableValidationTest, GlobalVarInitializerNoReturnValueBuiltin) {
+    // var a = storageBarrier();
+    auto* NoReturnValueBuiltin = Call(Source{{12, 34}}, "storageBarrier");
+    GlobalVar("a", nullptr, ast::StorageClass::kNone, NoReturnValueBuiltin);
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), "12:34 error: builtin 'storageBarrier' does not return a value");
+}
+
 TEST_F(ResolverVariableValidationTest, GlobalVarUsedAtModuleScope) {
     // var<private> a : i32;
     // var<private> b : i32 = a;
diff --git a/src/tint/sem/builtin.cc b/src/tint/sem/builtin.cc
index c688c4c..ee5a02b 100644
--- a/src/tint/sem/builtin.cc
+++ b/src/tint/sem/builtin.cc
@@ -17,13 +17,25 @@
 
 #include "src/tint/sem/builtin.h"
 
+#include <utility>
 #include <vector>
 
-#include "src/tint/utils/to_const_ptr_vec.h"
+#include "src/tint/utils/transform.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::sem::Builtin);
 
 namespace tint::sem {
+namespace {
+
+utils::VectorRef<const Parameter*> SetOwner(utils::VectorRef<Parameter*> parameters,
+                                            const tint::sem::CallTarget* owner) {
+    for (auto* parameter : parameters) {
+        parameter->SetOwner(owner);
+    }
+    return parameters;
+}
+
+}  // namespace
 
 const char* Builtin::str() const {
     return sem::str(type_);
@@ -89,18 +101,14 @@
 
 Builtin::Builtin(BuiltinType type,
                  const sem::Type* return_type,
-                 std::vector<Parameter*> parameters,
+                 utils::VectorRef<Parameter*> parameters,
                  EvaluationStage eval_stage,
                  PipelineStageSet supported_stages,
                  bool is_deprecated)
-    : Base(return_type, utils::ToConstPtrVec(parameters), eval_stage),
+    : Base(return_type, SetOwner(std::move(parameters), this), eval_stage),
       type_(type),
       supported_stages_(supported_stages),
-      is_deprecated_(is_deprecated) {
-    for (auto* parameter : parameters) {
-        parameter->SetOwner(this);
-    }
-}
+      is_deprecated_(is_deprecated) {}
 
 Builtin::~Builtin() = default;
 
diff --git a/src/tint/sem/builtin.h b/src/tint/sem/builtin.h
index 783e6ca..b63bece 100644
--- a/src/tint/sem/builtin.h
+++ b/src/tint/sem/builtin.h
@@ -90,7 +90,7 @@
     /// deprecated
     Builtin(BuiltinType type,
             const sem::Type* return_type,
-            std::vector<Parameter*> parameters,
+            utils::VectorRef<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 a20649d..1fb7de4 100644
--- a/src/tint/sem/call.cc
+++ b/src/tint/sem/call.cc
@@ -24,7 +24,7 @@
 Call::Call(const ast::CallExpression* declaration,
            const CallTarget* target,
            EvaluationStage stage,
-           std::vector<const sem::Expression*> arguments,
+           utils::VectorRef<const sem::Expression*> arguments,
            const Statement* statement,
            const Constant* constant,
            bool has_side_effects)
diff --git a/src/tint/sem/call.h b/src/tint/sem/call.h
index 2d82306..1213c6d 100644
--- a/src/tint/sem/call.h
+++ b/src/tint/sem/call.h
@@ -20,6 +20,7 @@
 #include "src/tint/ast/call_expression.h"
 #include "src/tint/sem/builtin.h"
 #include "src/tint/sem/expression.h"
+#include "src/tint/utils/vector.h"
 
 namespace tint::sem {
 
@@ -38,7 +39,7 @@
     Call(const ast::CallExpression* declaration,
          const CallTarget* target,
          EvaluationStage stage,
-         std::vector<const sem::Expression*> arguments,
+         utils::VectorRef<const sem::Expression*> arguments,
          const Statement* statement,
          const Constant* constant,
          bool has_side_effects);
@@ -50,7 +51,7 @@
     const CallTarget* Target() const { return target_; }
 
     /// @return the call arguments
-    const std::vector<const sem::Expression*>& Arguments() const { return arguments_; }
+    const auto& Arguments() const { return arguments_; }
 
     /// @returns the AST node
     const ast::CallExpression* Declaration() const {
@@ -59,7 +60,7 @@
 
   private:
     CallTarget const* const target_;
-    std::vector<const sem::Expression*> arguments_;
+    utils::Vector<const sem::Expression*, 8> arguments_;
 };
 
 }  // namespace tint::sem
diff --git a/src/tint/sem/call_target.cc b/src/tint/sem/call_target.cc
index f8bcdd8..362482c 100644
--- a/src/tint/sem/call_target.cc
+++ b/src/tint/sem/call_target.cc
@@ -14,6 +14,8 @@
 
 #include "src/tint/sem/call_target.h"
 
+#include <utility>
+
 #include "src/tint/symbol_table.h"
 #include "src/tint/utils/hash.h"
 
@@ -22,22 +24,23 @@
 namespace tint::sem {
 
 CallTarget::CallTarget(const sem::Type* return_type,
-                       const ParameterList& parameters,
+                       utils::VectorRef<const Parameter*> parameters,
                        EvaluationStage stage)
-    : signature_{return_type, parameters}, stage_(stage) {
+    : signature_{return_type, std::move(parameters)}, stage_(stage) {
     TINT_ASSERT(Semantic, return_type);
 }
 
 CallTarget::CallTarget(const CallTarget&) = default;
 CallTarget::~CallTarget() = default;
 
-CallTargetSignature::CallTargetSignature(const sem::Type* ret_ty, const ParameterList& params)
-    : return_type(ret_ty), parameters(params) {}
+CallTargetSignature::CallTargetSignature(const sem::Type* ret_ty,
+                                         utils::VectorRef<const Parameter*> params)
+    : return_type(ret_ty), parameters(std::move(params)) {}
 CallTargetSignature::CallTargetSignature(const CallTargetSignature&) = default;
 CallTargetSignature::~CallTargetSignature() = default;
 
 int CallTargetSignature::IndexOf(ParameterUsage usage) const {
-    for (size_t i = 0; i < parameters.size(); i++) {
+    for (size_t i = 0; i < parameters.Length(); i++) {
         if (parameters[i]->Usage() == usage) {
             return static_cast<int>(i);
         }
@@ -46,10 +49,10 @@
 }
 
 bool CallTargetSignature::operator==(const CallTargetSignature& other) const {
-    if (return_type != other.return_type || parameters.size() != other.parameters.size()) {
+    if (return_type != other.return_type || parameters.Length() != other.parameters.Length()) {
         return false;
     }
-    for (size_t i = 0; i < parameters.size(); i++) {
+    for (size_t i = 0; i < parameters.Length(); i++) {
         auto* a = parameters[i];
         auto* b = other.parameters[i];
         if (a->Type() != b->Type() || a->Usage() != b->Usage()) {
@@ -65,7 +68,7 @@
 
 std::size_t hash<tint::sem::CallTargetSignature>::operator()(
     const tint::sem::CallTargetSignature& sig) const {
-    size_t hash = tint::utils::Hash(sig.parameters.size());
+    size_t hash = tint::utils::Hash(sig.parameters.Length());
     for (auto* p : sig.parameters) {
         tint::utils::HashCombine(&hash, p->Type(), p->Usage());
     }
diff --git a/src/tint/sem/call_target.h b/src/tint/sem/call_target.h
index be1b96a..3f7fec1 100644
--- a/src/tint/sem/call_target.h
+++ b/src/tint/sem/call_target.h
@@ -21,6 +21,7 @@
 #include "src/tint/sem/sampler.h"
 #include "src/tint/sem/variable.h"
 #include "src/tint/utils/hash.h"
+#include "src/tint/utils/vector.h"
 
 // Forward declarations
 namespace tint::sem {
@@ -34,7 +35,7 @@
     /// Constructor
     /// @param ret_ty the call target return type
     /// @param params the call target parameters
-    CallTargetSignature(const sem::Type* ret_ty, const ParameterList& params);
+    CallTargetSignature(const sem::Type* ret_ty, utils::VectorRef<const Parameter*> params);
 
     /// Copy constructor
     CallTargetSignature(const CallTargetSignature&);
@@ -45,7 +46,7 @@
     /// The type of the call target return value
     const sem::Type* const return_type = nullptr;
     /// The parameters of the call target
-    const ParameterList parameters;
+    const utils::Vector<const sem::Parameter*, 8> parameters;
 
     /// Equality operator
     /// @param other the signature to compare this to
@@ -67,7 +68,7 @@
     /// @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,
+               utils::VectorRef<const Parameter*> parameters,
                EvaluationStage stage);
 
     /// Copy constructor
@@ -80,7 +81,7 @@
     const sem::Type* ReturnType() const { return signature_.return_type; }
 
     /// @return the parameters of the call target
-    const ParameterList& Parameters() const { return signature_.parameters; }
+    auto& Parameters() const { return signature_.parameters; }
 
     /// @return the signature of the call target
     const CallTargetSignature& Signature() const { return signature_; }
diff --git a/src/tint/sem/function.cc b/src/tint/sem/function.cc
index dcc80b0..ff3a2a7 100644
--- a/src/tint/sem/function.cc
+++ b/src/tint/sem/function.cc
@@ -21,22 +21,29 @@
 #include "src/tint/sem/sampled_texture.h"
 #include "src/tint/sem/storage_texture.h"
 #include "src/tint/sem/variable.h"
-#include "src/tint/utils/to_const_ptr_vec.h"
+#include "src/tint/utils/transform.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::sem::Function);
 
 namespace tint::sem {
+namespace {
+
+utils::VectorRef<const Parameter*> SetOwner(utils::VectorRef<Parameter*> parameters,
+                                            const tint::sem::CallTarget* owner) {
+    for (auto* parameter : parameters) {
+        parameter->SetOwner(owner);
+    }
+    return parameters;
+}
+
+}  // namespace
 
 Function::Function(const ast::Function* declaration,
                    Type* return_type,
-                   std::vector<Parameter*> parameters)
-    : Base(return_type, utils::ToConstPtrVec(parameters), EvaluationStage::kRuntime),
+                   utils::VectorRef<Parameter*> parameters)
+    : Base(return_type, SetOwner(std::move(parameters), this), EvaluationStage::kRuntime),
       declaration_(declaration),
-      workgroup_size_{WorkgroupDimension{1}, WorkgroupDimension{1}, WorkgroupDimension{1}} {
-    for (auto* parameter : parameters) {
-        parameter->SetOwner(this);
-    }
-}
+      workgroup_size_{WorkgroupDimension{1}, WorkgroupDimension{1}, WorkgroupDimension{1}} {}
 
 Function::~Function() = default;
 
diff --git a/src/tint/sem/function.h b/src/tint/sem/function.h
index f95920a..c90d749 100644
--- a/src/tint/sem/function.h
+++ b/src/tint/sem/function.h
@@ -22,6 +22,7 @@
 #include "src/tint/ast/variable.h"
 #include "src/tint/sem/call.h"
 #include "src/tint/utils/unique_vector.h"
+#include "src/tint/utils/vector.h"
 
 // Forward declarations
 namespace tint::ast {
@@ -62,7 +63,7 @@
     /// @param parameters the parameters to the function
     Function(const ast::Function* declaration,
              Type* return_type,
-             std::vector<Parameter*> parameters);
+             utils::VectorRef<Parameter*> parameters);
 
     /// Destructor
     ~Function() override;
diff --git a/src/tint/sem/member_accessor_expression.cc b/src/tint/sem/member_accessor_expression.cc
index 74b4833..4d194a9 100644
--- a/src/tint/sem/member_accessor_expression.cc
+++ b/src/tint/sem/member_accessor_expression.cc
@@ -61,7 +61,7 @@
                  const Statement* statement,
                  const Constant* constant,
                  const Expression* object,
-                 std::vector<uint32_t> indices,
+                 utils::VectorRef<uint32_t> indices,
                  bool has_side_effects,
                  const Variable* source_var /* = nullptr */)
     : Base(declaration,
diff --git a/src/tint/sem/member_accessor_expression.h b/src/tint/sem/member_accessor_expression.h
index 43e1466..b8144f1 100644
--- a/src/tint/sem/member_accessor_expression.h
+++ b/src/tint/sem/member_accessor_expression.h
@@ -15,9 +15,8 @@
 #ifndef SRC_TINT_SEM_MEMBER_ACCESSOR_EXPRESSION_H_
 #define SRC_TINT_SEM_MEMBER_ACCESSOR_EXPRESSION_H_
 
-#include <vector>
-
 #include "src/tint/sem/expression.h"
+#include "src/tint/utils/vector.h"
 
 // Forward declarations
 namespace tint::ast {
@@ -113,7 +112,7 @@
             const Statement* statement,
             const Constant* constant,
             const Expression* object,
-            std::vector<uint32_t> indices,
+            utils::VectorRef<uint32_t> indices,
             bool has_side_effects,
             const Variable* source_var = nullptr);
 
@@ -121,10 +120,10 @@
     ~Swizzle() override;
 
     /// @return the swizzle indices, if this is a vector swizzle
-    const std::vector<uint32_t>& Indices() const { return indices_; }
+    const auto& Indices() const { return indices_; }
 
   private:
-    std::vector<uint32_t> const indices_;
+    utils::Vector<uint32_t, 4> const indices_;
 };
 
 }  // namespace tint::sem
diff --git a/src/tint/sem/type_constructor.cc b/src/tint/sem/type_constructor.cc
index d85e1be..2acf74f 100644
--- a/src/tint/sem/type_constructor.cc
+++ b/src/tint/sem/type_constructor.cc
@@ -14,14 +14,16 @@
 
 #include "src/tint/sem/type_constructor.h"
 
+#include <utility>
+
 TINT_INSTANTIATE_TYPEINFO(tint::sem::TypeConstructor);
 
 namespace tint::sem {
 
 TypeConstructor::TypeConstructor(const sem::Type* type,
-                                 const ParameterList& parameters,
+                                 utils::VectorRef<const Parameter*> parameters,
                                  EvaluationStage stage)
-    : Base(type, parameters, stage) {}
+    : Base(type, std::move(parameters), stage) {}
 
 TypeConstructor::~TypeConstructor() = default;
 
diff --git a/src/tint/sem/type_constructor.h b/src/tint/sem/type_constructor.h
index 74b7858..1995db8 100644
--- a/src/tint/sem/type_constructor.h
+++ b/src/tint/sem/type_constructor.h
@@ -16,6 +16,7 @@
 #define SRC_TINT_SEM_TYPE_CONSTRUCTOR_H_
 
 #include "src/tint/sem/call_target.h"
+#include "src/tint/utils/vector.h"
 
 namespace tint::sem {
 
@@ -26,7 +27,9 @@
     /// @param type the type that's being constructed
     /// @param parameters the type constructor parameters
     /// @param stage the earliest evaluation stage for the expression
-    TypeConstructor(const sem::Type* type, const ParameterList& parameters, EvaluationStage stage);
+    TypeConstructor(const sem::Type* type,
+                    utils::VectorRef<const Parameter*> parameters,
+                    EvaluationStage stage);
 
     /// Destructor
     ~TypeConstructor() override;
diff --git a/src/tint/sem/type_conversion.cc b/src/tint/sem/type_conversion.cc
index 262e3a0..42fa2e0 100644
--- a/src/tint/sem/type_conversion.cc
+++ b/src/tint/sem/type_conversion.cc
@@ -21,7 +21,7 @@
 TypeConversion::TypeConversion(const sem::Type* type,
                                const sem::Parameter* parameter,
                                EvaluationStage stage)
-    : Base(type, ParameterList{parameter}, stage) {}
+    : Base(type, utils::Vector<const sem::Parameter*, 1>{parameter}, stage) {}
 
 TypeConversion::~TypeConversion() = default;
 
diff --git a/src/tint/sem/variable.h b/src/tint/sem/variable.h
index fe54f50..ecc536b 100644
--- a/src/tint/sem/variable.h
+++ b/src/tint/sem/variable.h
@@ -222,9 +222,6 @@
     const sem::Node* shadows_ = nullptr;
 };
 
-/// ParameterList is a list of Parameter
-using ParameterList = std::vector<const Parameter*>;
-
 /// VariableUser holds the semantic information for an identifier expression
 /// node that resolves to a variable.
 class VariableUser final : public Castable<VariableUser, Expression> {
diff --git a/src/tint/text/unicode.cc b/src/tint/text/unicode.cc
index 7339297..e23f3dd 100644
--- a/src/tint/text/unicode.cc
+++ b/src/tint/text/unicode.cc
@@ -306,15 +306,26 @@
 }  // namespace
 
 bool CodePoint::IsXIDStart() const {
-    // Short circuit ascii. It will end up being at the end of the binary search
-    // but is our, currently, common case.
+    // Short circuit ASCII. The binary search will find these last, but most
+    // of our current source is ASCII, so handle them quicker.
     if ((value >= 'a' && value <= 'z') || (value >= 'A' && value <= 'Z')) {
         return true;
     }
+    // With [a-zA-Z] handled, nothing less then the next sequence start can be
+    // XIDStart, so filter them all out. This catches most of the common symbols
+    // that are used in ASCII.
+    if (value < 0x000aa) {
+        return false;
+    }
     return std::binary_search(kXIDStartRanges, kXIDStartRanges + kNumXIDStartRanges, *this);
 }
 
 bool CodePoint::IsXIDContinue() const {
+    // Short circuit ASCII. The binary search will find these last, but most
+    // of our current source is ASCII, so handle them quicker.
+    if ((value >= '0' && value <= '9') || value == '_') {
+        return true;
+    }
     return IsXIDStart() || std::binary_search(kXIDContinueRanges,
                                               kXIDContinueRanges + kNumXIDContinueRanges, *this);
 }
diff --git a/src/tint/transform/canonicalize_entry_point_io.cc b/src/tint/transform/canonicalize_entry_point_io.cc
index 6f84029..a24aa0a 100644
--- a/src/tint/transform/canonicalize_entry_point_io.cc
+++ b/src/tint/transform/canonicalize_entry_point_io.cc
@@ -509,7 +509,7 @@
         }
 
         // Exit early if there is no shader IO to handle.
-        if (func_sem->Parameters().size() == 0 && func_sem->ReturnType()->Is<sem::Void>() &&
+        if (func_sem->Parameters().Length() == 0 && func_sem->ReturnType()->Is<sem::Void>() &&
             !needs_fixed_sample_mask && !needs_vertex_point_size &&
             cfg.shader_style != ShaderStyle::kGlsl) {
             return;
@@ -517,7 +517,7 @@
 
         // Process the entry point parameters, collecting those that need to be
         // aggregated into a single structure.
-        if (!func_sem->Parameters().empty()) {
+        if (!func_sem->Parameters().IsEmpty()) {
             for (auto* param : func_sem->Parameters()) {
                 if (param->Type()->Is<sem::Struct>()) {
                     ProcessStructParameter(param);
diff --git a/src/tint/transform/decompose_memory_access.cc b/src/tint/transform/decompose_memory_access.cc
index a714581..135a42a 100644
--- a/src/tint/transform/decompose_memory_access.cc
+++ b/src/tint/transform/decompose_memory_access.cc
@@ -635,7 +635,7 @@
             };
 
             // Other parameters are copied as-is:
-            for (size_t i = 1; i < intrinsic->Parameters().size(); i++) {
+            for (size_t i = 1; i < intrinsic->Parameters().Length(); i++) {
                 auto* param = intrinsic->Parameters()[i];
                 auto* ty = CreateASTTypeFor(ctx, param->Type());
                 params.emplace_back(b.Param("param_" + std::to_string(i), ty));
@@ -834,7 +834,7 @@
             // X.Y
             auto* accessor_sem = sem.Get(accessor);
             if (auto* swizzle = accessor_sem->As<sem::Swizzle>()) {
-                if (swizzle->Indices().size() == 1) {
+                if (swizzle->Indices().Length() == 1) {
                     if (auto access = state.TakeAccess(accessor->structure)) {
                         auto* vec_ty = access.type->As<sem::Vector>();
                         auto* offset = state.Mul(vec_ty->type()->Size(), swizzle->Indices()[0u]);
diff --git a/src/tint/transform/multiplanar_external_texture.cc b/src/tint/transform/multiplanar_external_texture.cc
index c3aba9b..a554616 100644
--- a/src/tint/transform/multiplanar_external_texture.cc
+++ b/src/tint/transform/multiplanar_external_texture.cc
@@ -187,7 +187,7 @@
             auto* call = sem.Get(expr)->UnwrapMaterialize()->As<sem::Call>();
             auto* builtin = call->Target()->As<sem::Builtin>();
 
-            if (builtin && !builtin->Parameters().empty() &&
+            if (builtin && !builtin->Parameters().IsEmpty() &&
                 builtin->Parameters()[0]->Type()->Is<sem::ExternalTexture>() &&
                 builtin->Type() != sem::BuiltinType::kTextureDimensions) {
                 if (auto* var_user = sem.Get<sem::VariableUser>(expr->args[0])) {
diff --git a/src/tint/transform/vectorize_scalar_matrix_constructors.cc b/src/tint/transform/vectorize_scalar_matrix_constructors.cc
index e7c76e3..3bba57c 100644
--- a/src/tint/transform/vectorize_scalar_matrix_constructors.cc
+++ b/src/tint/transform/vectorize_scalar_matrix_constructors.cc
@@ -36,7 +36,7 @@
         if (auto* call = program->Sem().Get<sem::Call>(node)) {
             if (call->Target()->Is<sem::TypeConstructor>() && call->Type()->Is<sem::Matrix>()) {
                 auto& args = call->Arguments();
-                if (args.size() > 0 && args[0]->Type()->UnwrapRef()->is_scalar()) {
+                if (!args.IsEmpty() && args[0]->Type()->UnwrapRef()->is_scalar()) {
                     return true;
                 }
             }
@@ -61,7 +61,7 @@
         }
 
         auto& args = call->Arguments();
-        if (args.size() == 0) {
+        if (args.IsEmpty()) {
             return nullptr;
         }
         if (!args[0]->Type()->UnwrapRef()->is_scalar()) {
@@ -85,7 +85,7 @@
             return ctx.dst->Construct(CreateASTTypeFor(ctx, mat_type), columns);
         };
 
-        if (args.size() == 1) {
+        if (args.Length() == 1) {
             // Generate a helper function for constructing the matrix.
             // This is done to ensure that the single argument value is only evaluated once, and
             // with the correct expression evaluation order.
@@ -109,7 +109,7 @@
             return ctx.dst->Call(fn, ctx.Clone(args[0]->Declaration()));
         }
 
-        if (args.size() == mat_type->columns() * mat_type->rows()) {
+        if (args.Length() == mat_type->columns() * mat_type->rows()) {
             return build_mat([&](uint32_t c, uint32_t r) {
                 return ctx.Clone(args[c * mat_type->rows() + r]->Declaration());
             });
diff --git a/src/tint/utils/hash_test.cc b/src/tint/utils/hash_test.cc
index 9e60a66..6ce6820 100644
--- a/src/tint/utils/hash_test.cc
+++ b/src/tint/utils/hash_test.cc
@@ -44,10 +44,10 @@
 }
 
 TEST(HashTests, TintVector) {
-    EXPECT_EQ(Hash(Vector<int>({})), Hash(Vector<int>({})));
-    EXPECT_EQ(Hash(Vector<int>({1, 2, 3})), Hash(Vector<int>({1, 2, 3})));
-    EXPECT_NE(Hash(Vector<int>({1, 2, 3})), Hash(Vector<int>({1, 2, 4})));
-    EXPECT_NE(Hash(Vector<int>({1, 2, 3})), Hash(Vector<int>({1, 2, 3, 4})));
+    EXPECT_EQ(Hash(Vector<int, 0>({})), Hash(Vector<int, 0>({})));
+    EXPECT_EQ(Hash(Vector<int, 0>({1, 2, 3})), Hash(Vector<int, 0>({1, 2, 3})));
+    EXPECT_NE(Hash(Vector<int, 0>({1, 2, 3})), Hash(Vector<int, 0>({1, 2, 4})));
+    EXPECT_NE(Hash(Vector<int, 0>({1, 2, 3})), Hash(Vector<int, 0>({1, 2, 3, 4})));
     EXPECT_EQ(Hash(Vector<int, 3>({1, 2, 3})), Hash(Vector<int, 4>({1, 2, 3})));
     EXPECT_EQ(Hash(Vector<int, 3>({1, 2, 3})), Hash(Vector<int, 2>({1, 2, 3})));
 }
diff --git a/src/tint/utils/to_const_ptr_vec.h b/src/tint/utils/to_const_ptr_vec.h
deleted file mode 100644
index 02cc984..0000000
--- a/src/tint/utils/to_const_ptr_vec.h
+++ /dev/null
@@ -1,37 +0,0 @@
-
-// Copyright 2021 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_UTILS_TO_CONST_PTR_VEC_H_
-#define SRC_TINT_UTILS_TO_CONST_PTR_VEC_H_
-
-#include <vector>
-
-namespace tint::utils {
-
-/// @param in a vector of `T*`
-/// @returns a vector of `const T*` with the content of `in`.
-template <typename T>
-std::vector<const T*> ToConstPtrVec(const std::vector<T*>& in) {
-    std::vector<const T*> out;
-    out.reserve(in.size());
-    for (auto* ptr : in) {
-        out.emplace_back(ptr);
-    }
-    return out;
-}
-
-}  // namespace tint::utils
-
-#endif  // SRC_TINT_UTILS_TO_CONST_PTR_VEC_H_
diff --git a/src/tint/utils/transform.h b/src/tint/utils/transform.h
index 0c91263..412dbed 100644
--- a/src/tint/utils/transform.h
+++ b/src/tint/utils/transform.h
@@ -59,10 +59,12 @@
 /// @returns a new vector with each element of the source vector transformed by `transform`.
 template <typename IN, size_t N, typename TRANSFORMER>
 auto Transform(const Vector<IN, N>& in, TRANSFORMER&& transform)
-    -> Vector<decltype(transform(in[0]))> {
-    Vector<decltype(transform(in[0])), N> result(in.Length());
-    for (size_t i = 0; i < result.Length(); ++i) {
-        result[i] = transform(in[i]);
+    -> 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;
 }
@@ -74,9 +76,11 @@
 template <typename IN, size_t N, typename TRANSFORMER>
 auto Transform(const Vector<IN, N>& in, TRANSFORMER&& transform)
     -> Vector<decltype(transform(in[0], 1u)), N> {
-    Vector<decltype(transform(in[0], 1u)), N> result(in.Length());
-    for (size_t i = 0; i < result.Length(); ++i) {
-        result[i] = transform(in[i], i);
+    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;
 }
@@ -89,9 +93,11 @@
 template <size_t N, typename IN, typename TRANSFORMER>
 auto Transform(const VectorRef<IN>& in, TRANSFORMER&& transform)
     -> Vector<decltype(transform(in[0])), N> {
-    Vector<decltype(transform(in[0])), N> result(in.Length());
-    for (size_t i = 0; i < result.Length(); ++i) {
-        result[i] = transform(in[i]);
+    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;
 }
@@ -104,9 +110,11 @@
 template <size_t N, typename IN, typename TRANSFORMER>
 auto Transform(const VectorRef<IN>& in, TRANSFORMER&& transform)
     -> Vector<decltype(transform(in[0], 1u)), N> {
-    Vector<decltype(transform(in[0], 1u)), N> result(in.Length());
-    for (size_t i = 0; i < result.Length(); ++i) {
-        result[i] = transform(in[i], i);
+    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;
 }
@@ -119,9 +127,11 @@
 template <size_t N, typename IN, typename TRANSFORMER>
 auto Transform(ConstVectorRef<IN> in, TRANSFORMER&& transform)
     -> Vector<decltype(transform(in[0])), N> {
-    Vector<decltype(transform(in[0])), N> result(in.Length());
-    for (size_t i = 0; i < result.Length(); ++i) {
-        result[i] = transform(in[i]);
+    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;
 }
@@ -134,9 +144,11 @@
 template <size_t N, typename IN, typename TRANSFORMER>
 auto Transform(ConstVectorRef<IN> in, TRANSFORMER&& transform)
     -> Vector<decltype(transform(in[0], 1u)), N> {
-    Vector<decltype(transform(in[0], 1u)), N> result(in.Length());
-    for (size_t i = 0; i < result.Length(); ++i) {
-        result[i] = transform(in[i], i);
+    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;
 }
diff --git a/src/tint/utils/vector.h b/src/tint/utils/vector.h
index daac42c..d339a49 100644
--- a/src/tint/utils/vector.h
+++ b/src/tint/utils/vector.h
@@ -22,6 +22,8 @@
 #include <utility>
 #include <vector>
 
+#include "src/tint/castable.h"
+#include "src/tint/traits.h"
 #include "src/tint/utils/bitcast.h"
 
 namespace tint::utils {
@@ -36,8 +38,6 @@
 
 namespace tint::utils {
 
-namespace detail {
-
 /// A slice represents a contigious array of elements of type T.
 template <typename T>
 struct Slice {
@@ -97,7 +97,42 @@
     auto rend() const { return std::reverse_iterator<const T*>(begin()); }
 };
 
-}  // namespace detail
+/// Evaluates whether a `vector<FROM>` and be reinterpreted as a `vector<TO>`.
+/// Vectors can be reinterpreted if both `FROM` and `TO` are pointers to a type that derives from
+/// CastableBase, and the pointee type of `TO` is of the same type as, or is an ancestor of the
+/// pointee type of `FROM`. Vectors of non-`const` Castable pointers can be converted to a vector of
+/// `const` Castable pointers.
+template <typename TO, typename FROM>
+static constexpr bool CanReinterpretSlice =
+    // TO and FROM are both pointer types
+    std::is_pointer_v<TO> && std::is_pointer_v<FROM> &&  //
+    // const can only be applied, not removed
+    (std::is_const_v<std::remove_pointer_t<TO>> ||
+     !std::is_const_v<std::remove_pointer_t<FROM>>)&&  //
+    // TO and FROM are both Castable
+    IsCastable<std::remove_pointer_t<FROM>, std::remove_pointer_t<TO>> &&
+    // FROM is of, or derives from TO
+    traits::IsTypeOrDerived<std::remove_pointer_t<FROM>, std::remove_pointer_t<TO>>;
+
+/// Reinterprets `const Slice<FROM>*` as `const Slice<TO>*`
+/// @param slice a pointer to the slice to reinterpret
+/// @returns the reinterpreted slice
+/// @see CanReinterpretSlice
+template <typename TO, typename FROM>
+const Slice<TO>* ReinterpretSlice(const Slice<FROM>* slice) {
+    static_assert(CanReinterpretSlice<TO, FROM>);
+    return Bitcast<const Slice<TO>*>(slice);
+}
+
+/// Reinterprets `Slice<FROM>*` as `Slice<TO>*`
+/// @param slice a pointer to the slice to reinterpret
+/// @returns the reinterpreted slice
+/// @see CanReinterpretSlice
+template <typename TO, typename FROM>
+Slice<TO>* ReinterpretSlice(Slice<FROM>* slice) {
+    static_assert(CanReinterpretSlice<TO, FROM>);
+    return Bitcast<Slice<TO>*>(slice);
+}
 
 /// Vector is a small-object-optimized, dynamically-sized vector of contigious elements of type T.
 ///
@@ -118,39 +153,20 @@
 ///   array'. This reduces memory copying, but may incur additional memory usage.
 /// * Resizing, or popping elements from a vector that has spilled to a heap allocation does not
 ///   revert back to using the 'small array'. Again, this is to reduce memory copying.
-template <typename T, size_t N = 0>
+template <typename T, size_t N>
 class Vector {
   public:
     /// Type of `T`.
     using value_type = T;
+    /// Value of `N`
+    static constexpr size_t static_length = N;
 
     /// Constructor
     Vector() = default;
 
     /// Constructor
-    /// @param length the initial length of the vector. Elements will be zero-initialized.
-    explicit Vector(size_t length) {
-        Reserve(length);
-        for (size_t i = 0; i < length; i++) {
-            new (&impl_.slice.data[i]) T{};
-        }
-        impl_.slice.len = length;
-    }
-
-    /// Constructor
-    /// @param length the initial length of the vector
-    /// @param value the value to copy into each element of the vector
-    Vector(size_t length, const T& value) {
-        Reserve(length);
-        for (size_t i = 0; i < length; i++) {
-            new (&impl_.slice.data[i]) T{value};
-        }
-        impl_.slice.len = length;
-    }
-
-    /// Constructor
     /// @param elements the elements to place into the vector
-    explicit Vector(std::initializer_list<T> elements) {
+    Vector(std::initializer_list<T> elements) {
         Reserve(elements.size());
         for (auto& el : elements) {
             new (&impl_.slice.data[impl_.slice.len++]) T{el};
@@ -179,17 +195,32 @@
         MoveOrCopy(VectorRef<T>(std::move(other)));
     }
 
+    /// Copy constructor with covariance / const conversion
+    /// @param other the vector to copy
+    /// @see CanReinterpretSlice for rules about conversion
+    template <typename U, size_t N2, typename = std::enable_if_t<CanReinterpretSlice<T, U>>>
+    Vector(const Vector<U, N2>& other) {  // NOLINT(runtime/explicit)
+        Copy(*ReinterpretSlice<T>(&other.impl_.slice));
+    }
+
+    /// Move constructor with covariance / const conversion
+    /// @param other the vector to move
+    /// @see CanReinterpretSlice for rules about conversion
+    template <typename U, size_t N2, typename = std::enable_if_t<CanReinterpretSlice<T, U>>>
+    Vector(Vector<U, N2>&& other) {  // NOLINT(runtime/explicit)
+        MoveOrCopy(VectorRef<T>(std::move(other)));
+    }
+
     /// Move constructor from a mutable vector reference
     /// @param other the vector reference to move
-    Vector(VectorRef<T>&& other) {  // NOLINT(runtime/explicit)
-        MoveOrCopy(std::move(other));
-    }
+    explicit Vector(VectorRef<T>&& other) { MoveOrCopy(std::move(other)); }
 
     /// Copy constructor from an immutable vector reference
     /// @param other the vector reference to copy
-    Vector(const ConstVectorRef<T>& other) {  // NOLINT(runtime/explicit)
-        Copy(other.slice_);
-    }
+    explicit Vector(const ConstVectorRef<T>& other) { Copy(other.slice_); }
+
+    /// Move constructor from an immutable vector reference (invalid)
+    Vector(ConstVectorRef<T>&&) = delete;  // NOLINT(runtime/explicit)
 
     /// Destructor
     ~Vector() { ClearAndFree(); }
@@ -277,6 +308,20 @@
         impl_.slice.len = new_len;
     }
 
+    /// Resizes the vector to the given length, expanding capacity if necessary.
+    /// @param new_len the new vector length
+    /// @param value the value to copy into the new elements
+    void Resize(size_t new_len, const T& value) {
+        Reserve(new_len);
+        for (size_t i = impl_.slice.len; i > new_len; i--) {  // Shrink
+            impl_.slice.data[i - 1].~T();
+        }
+        for (size_t i = impl_.slice.len; i < new_len; i++) {  // Grow
+            new (&impl_.slice.data[i]) T{value};
+        }
+        impl_.slice.len = new_len;
+    }
+
     /// Copies all the elements from `other` to this vector, replacing the content of this vector.
     /// @param other the
     template <typename T2, size_t N2>
@@ -317,7 +362,7 @@
         if (impl_.slice.len >= impl_.slice.cap) {
             Grow();
         }
-        new (&impl_.slice.data[impl_.slice.len++]) T(std::forward<ARGS>(args)...);
+        new (&impl_.slice.data[impl_.slice.len++]) T{std::forward<ARGS>(args)...};
     }
 
     /// Removes and returns the last element from the vector.
@@ -377,7 +422,12 @@
     friend class ConstVectorRef;
 
     /// The slice type used by this vector
-    using Slice = detail::Slice<T>;
+    using Slice = utils::Slice<T>;
+
+    template <typename... Ts>
+    void AppendVariadic(Ts&&... args) {
+        ((new (&impl_.slice.data[impl_.slice.len++]) T(std::forward<Ts>(args))), ...);
+    }
 
     /// Expands the capacity of the vector
     void Grow() { Reserve(impl_.slice.cap * 2); }
@@ -484,6 +534,43 @@
     std::conditional_t<HasSmallArray, ImplWithSmallArray, ImplWithoutSmallArray> impl_;
 };
 
+namespace detail {
+
+/// Helper for determining the Vector element type (`T`) from the vector's constuctor arguments
+/// @tparam IS_CASTABLE true if the types of `Ts` derive from CastableBase
+/// @tparam Ts the vector constructor argument types to infer the vector element type from.
+template <bool IS_CASTABLE, typename... Ts>
+struct VectorCommonType;
+
+/// VectorCommonType specialization for non-castable types.
+template <typename... Ts>
+struct VectorCommonType</*IS_CASTABLE*/ false, Ts...> {
+    /// The common T type to use for the vector
+    using type = std::common_type_t<Ts...>;
+};
+
+/// VectorCommonType specialization for castable types.
+template <typename... Ts>
+struct VectorCommonType</*IS_CASTABLE*/ true, Ts...> {
+    /// The common Castable type (excluding pointer)
+    using common_ty = CastableCommonBase<std::remove_pointer_t<Ts>...>;
+    /// The common T type to use for the vector
+    using type = std::conditional_t<(std::is_const_v<std::remove_pointer_t<Ts>> || ...),
+                                    const common_ty*,
+                                    common_ty*>;
+};
+
+}  // namespace detail
+
+/// Helper for determining the Vector element type (`T`) from the vector's constuctor arguments
+template <typename... Ts>
+using VectorCommonType =
+    typename detail::VectorCommonType<IsCastable<std::remove_pointer_t<Ts>...>, Ts...>::type;
+
+/// Deduction guide for Vector
+template <typename... Ts>
+Vector(Ts...) -> Vector<VectorCommonType<Ts...>, sizeof...(Ts)>;
+
 /// VectorRef is a weak reference to a Vector, used to pass vectors as parameters, avoiding copies
 /// between the caller and the callee. VectorRef can accept a Vector of any 'N' value, decoupling
 /// the caller's vector internal size from the callee's vector size.
@@ -507,16 +594,16 @@
 template <typename T>
 class VectorRef {
     /// The slice type used by this vector reference
-    using Slice = detail::Slice<T>;
+    using Slice = utils::Slice<T>;
 
   public:
-    /// Constructor from a Vector.
-    /// @param vector the vector reference
+    /// Constructor from a Vector
+    /// @param vector the vector to create a reference of
     template <size_t N>
     VectorRef(Vector<T, N>& vector)  // NOLINT(runtime/explicit)
         : slice_(vector.impl_.slice), can_move_(false) {}
 
-    /// Constructor from a std::move()'d Vector
+    /// Constructor from a moved Vector
     /// @param vector the vector being moved
     template <size_t N>
     VectorRef(Vector<T, N>&& vector)  // NOLINT(runtime/explicit)
@@ -530,6 +617,32 @@
     /// @param other the vector reference
     VectorRef(VectorRef&& other) = default;
 
+    /// Copy constructor with covariance / const conversion
+    /// @param other the other vector reference
+    template <typename U, typename = std::enable_if_t<CanReinterpretSlice<T, U>>>
+    VectorRef(const VectorRef<U>& other)  // NOLINT(runtime/explicit)
+        : slice_(*ReinterpretSlice<T>(&other.slice_)), can_move_(false) {}
+
+    /// Move constructor with covariance / const conversion
+    /// @param other the vector reference
+    template <typename U, typename = std::enable_if_t<CanReinterpretSlice<T, U>>>
+    VectorRef(VectorRef<U>&& other)  // NOLINT(runtime/explicit)
+        : slice_(*ReinterpretSlice<T>(&other.slice_)), can_move_(other.can_move_) {}
+
+    /// Constructor from a Vector with covariance / const conversion
+    /// @param vector the vector to create a reference of
+    /// @see CanReinterpretSlice for rules about conversion
+    template <typename U, size_t N, typename = std::enable_if_t<CanReinterpretSlice<T, U>>>
+    VectorRef(Vector<U, N>& vector)  // NOLINT(runtime/explicit)
+        : slice_(*ReinterpretSlice<T>(&vector.impl_.slice)), can_move_(false) {}
+
+    /// Constructor from a moved Vector with covariance / const conversion
+    /// @param vector the vector to create a reference of
+    /// @see CanReinterpretSlice for rules about conversion
+    template <typename U, size_t N, typename = std::enable_if_t<CanReinterpretSlice<T, U>>>
+    VectorRef(Vector<U, N>&& vector)  // NOLINT(runtime/explicit)
+        : slice_(*ReinterpretSlice<T>(&vector.impl_.slice)), can_move_(vector.impl_.CanMove()) {}
+
     /// Index operator
     /// @param i the element index. Must be less than `len`.
     /// @returns a reference to the i'th element.
@@ -587,10 +700,18 @@
     auto rend() const { return slice_.rend(); }
 
   private:
-    /// Friend classes
+    /// Friend class
     template <typename, size_t>
     friend class Vector;
 
+    /// Friend class
+    template <typename>
+    friend class VectorRef;
+
+    /// Friend class
+    template <typename>
+    friend class ConstVectorRef;
+
     /// The slice of the vector being referenced.
     Slice& slice_;
     /// Whether the slice data is passed by r-value reference, and can be moved.
@@ -603,7 +724,7 @@
 template <typename T>
 class ConstVectorRef {
     /// The slice type used by this vector reference
-    using Slice = detail::Slice<T>;
+    using Slice = utils::Slice<T>;
 
   public:
     /// Constructor from a Vector.
@@ -616,6 +737,34 @@
     /// @param other the vector reference
     ConstVectorRef(const ConstVectorRef& other) = default;
 
+    /// Conversion constructor to convert from a non-const to const vector reference
+    /// @param other the vector reference
+    ConstVectorRef(const VectorRef<T>& other) : slice_(other.slice_) {}  // NOLINT(runtime/explicit)
+
+    /// Move constructor. Deleted as this won't move anything.
+    ConstVectorRef(ConstVectorRef&&) = delete;
+
+    /// Constructor from a Vector with covariance / const conversion
+    /// @param vector the vector to create a reference of
+    /// @see CanReinterpretSlice for rules about conversion
+    template <typename U, size_t N, typename = std::enable_if_t<CanReinterpretSlice<T, U>>>
+    ConstVectorRef(const Vector<U, N>& vector)  // NOLINT(runtime/explicit)
+        : slice_(*ReinterpretSlice<T>(&vector.impl_.slice)) {}
+
+    /// Constructor from a VectorRef with covariance / const conversion
+    /// @param other the vector reference
+    /// @see CanReinterpretSlice for rules about conversion
+    template <typename U, typename = std::enable_if_t<CanReinterpretSlice<T, U>>>
+    ConstVectorRef(const VectorRef<U>& other)  // NOLINT(runtime/explicit)
+        : slice_(*ReinterpretSlice<T>(&other.slice_)) {}
+
+    /// Constructor from a ConstVectorRef with covariance / const conversion
+    /// @param other the vector reference
+    /// @see CanReinterpretSlice for rules about conversion
+    template <typename U, typename = std::enable_if_t<CanReinterpretSlice<T, U>>>
+    ConstVectorRef(const ConstVectorRef<U>& other)  // NOLINT(runtime/explicit)
+        : slice_(*ReinterpretSlice<T>(&other.slice_)) {}
+
     /// Index operator
     /// @param i the element index. Must be less than `len`.
     /// @returns a reference to the i'th element.
@@ -650,10 +799,14 @@
     auto rend() const { return slice_.rend(); }
 
   private:
-    /// Friend classes
+    /// Friend class
     template <typename, size_t>
     friend class Vector;
 
+    /// Friend class
+    template <typename>
+    friend class ConstVectorRef;
+
     /// The slice of the vector being referenced.
     const Slice& slice_;
 };
@@ -682,14 +835,6 @@
     return out;
 }
 
-/// Helper for constructing a Vector from a set of elements.
-/// The returned Vector's small-array size (`N`) is equal to the number of provided elements.
-/// @param elements the elements used to construct the vector.
-template <typename T, typename... Ts>
-auto MakeVector(Ts&&... elements) {
-    return Vector<T, sizeof...(Ts)>({std::forward<Ts>(elements)...});
-}
-
 }  // namespace tint::utils
 
 #endif  // SRC_TINT_UTILS_VECTOR_H_
diff --git a/src/tint/utils/vector_test.cc b/src/tint/utils/vector_test.cc
index 8c2c0cf..6e529dc 100644
--- a/src/tint/utils/vector_test.cc
+++ b/src/tint/utils/vector_test.cc
@@ -24,6 +24,11 @@
 namespace tint::utils {
 namespace {
 
+class C0 : public Castable<C0> {};
+class C1 : public Castable<C1, C0> {};
+class C2a : public Castable<C2a, C1> {};
+class C2b : public Castable<C2b, C1> {};
+
 /// @returns true if the address of el is within the memory of the vector vec.
 template <typename T, size_t N, typename E>
 bool IsInternal(Vector<T, N>& vec, E& el) {
@@ -54,6 +59,46 @@
     return true;
 }
 
+////////////////////////////////////////////////////////////////////////////////
+// Static asserts
+////////////////////////////////////////////////////////////////////////////////
+static_assert(std::is_same_v<VectorCommonType<int>, int>);
+static_assert(std::is_same_v<VectorCommonType<int, int>, int>);
+static_assert(std::is_same_v<VectorCommonType<int, float>, float>);
+
+static_assert(std::is_same_v<VectorCommonType<C0*>, C0*>);
+static_assert(std::is_same_v<VectorCommonType<const C0*>, const C0*>);
+
+static_assert(std::is_same_v<VectorCommonType<C0*, C1*>, C0*>);
+static_assert(std::is_same_v<VectorCommonType<const C0*, C1*>, const C0*>);
+static_assert(std::is_same_v<VectorCommonType<C0*, const C1*>, const C0*>);
+static_assert(std::is_same_v<VectorCommonType<const C0*, const C1*>, const C0*>);
+
+static_assert(std::is_same_v<VectorCommonType<C2a*, C2b*>, C1*>);
+static_assert(std::is_same_v<VectorCommonType<const C2a*, C2b*>, const C1*>);
+static_assert(std::is_same_v<VectorCommonType<C2a*, const C2b*>, const C1*>);
+static_assert(std::is_same_v<VectorCommonType<const C2a*, const C2b*>, const C1*>);
+
+static_assert(CanReinterpretSlice<const C0*, C0*>, "apply const");
+static_assert(!CanReinterpretSlice<C0*, const C0*>, "remove const");
+static_assert(CanReinterpretSlice<C0*, C1*>, "up cast");
+static_assert(CanReinterpretSlice<const C0*, const C1*>, "up cast");
+static_assert(CanReinterpretSlice<const C0*, C1*>, "up cast, apply const");
+static_assert(!CanReinterpretSlice<C0*, const C1*>, "up cast, remove const");
+static_assert(!CanReinterpretSlice<C1*, C0*>, "down cast");
+static_assert(!CanReinterpretSlice<const C1*, const C0*>, "down cast");
+static_assert(!CanReinterpretSlice<const C1*, C0*>, "down cast, apply const");
+static_assert(!CanReinterpretSlice<C1*, const C0*>, "down cast, remove const");
+static_assert(!CanReinterpretSlice<const C1*, C0*>, "down cast, apply const");
+static_assert(!CanReinterpretSlice<C1*, const C0*>, "down cast, remove const");
+static_assert(!CanReinterpretSlice<C2a*, C2b*>, "sideways cast");
+static_assert(!CanReinterpretSlice<const C2a*, const C2b*>, "sideways cast");
+static_assert(!CanReinterpretSlice<const C2a*, C2b*>, "sideways cast, apply const");
+static_assert(!CanReinterpretSlice<C2a*, const C2b*>, "sideways cast, remove const");
+
+////////////////////////////////////////////////////////////////////////////////
+// TintVectorTest
+////////////////////////////////////////////////////////////////////////////////
 TEST(TintVectorTest, SmallArray_Empty) {
     Vector<int, 2> vec;
     EXPECT_EQ(vec.Length(), 0u);
@@ -61,59 +106,12 @@
 }
 
 TEST(TintVectorTest, Empty_NoSmallArray) {
-    Vector<int> vec;
+    Vector<int, 0> vec;
     EXPECT_EQ(vec.Length(), 0u);
     EXPECT_EQ(vec.Capacity(), 0u);
 }
 
-TEST(TintVectorTest, SmallArray_ConstructLength_NoSpill) {
-    Vector<int, 2> vec(2);
-    EXPECT_EQ(vec.Length(), 2u);
-    EXPECT_EQ(vec.Capacity(), 2u);
-    EXPECT_EQ(vec[0], 0);
-    EXPECT_EQ(vec[1], 0);
-    EXPECT_TRUE(AllInternallyHeld(vec));
-}
-
-TEST(TintVectorTest, SmallArray_ConstructLength_WithSpill) {
-    Vector<int, 2> vec(3);
-    EXPECT_EQ(vec.Length(), 3u);
-    EXPECT_EQ(vec.Capacity(), 3u);
-    EXPECT_EQ(vec[0], 0);
-    EXPECT_EQ(vec[1], 0);
-    EXPECT_EQ(vec[2], 0);
-    EXPECT_TRUE(AllExternallyHeld(vec));
-}
-
-TEST(TintVectorTest, SmallArray_ConstructLengthValue_NoSpill) {
-    Vector<std::string, 2> vec(2, "abc");
-    EXPECT_EQ(vec.Length(), 2u);
-    EXPECT_EQ(vec.Capacity(), 2u);
-    EXPECT_EQ(vec[0], "abc");
-    EXPECT_EQ(vec[1], "abc");
-    EXPECT_TRUE(AllInternallyHeld(vec));
-}
-
-TEST(TintVectorTest, SmallArray_ConstructLengthValue_WithSpill) {
-    Vector<std::string, 2> vec(3, "abc");
-    EXPECT_EQ(vec.Length(), 3u);
-    EXPECT_EQ(vec.Capacity(), 3u);
-    EXPECT_EQ(vec[0], "abc");
-    EXPECT_EQ(vec[1], "abc");
-    EXPECT_EQ(vec[2], "abc");
-    EXPECT_TRUE(AllExternallyHeld(vec));
-}
-
-TEST(TintVectorTest, ConstructLength_NoSmallArray) {
-    Vector<int> vec(2);
-    EXPECT_EQ(vec.Length(), 2u);
-    EXPECT_EQ(vec.Capacity(), 2u);
-    EXPECT_EQ(vec[0], 0);
-    EXPECT_EQ(vec[1], 0);
-    EXPECT_TRUE(AllExternallyHeld(vec));
-}
-
-TEST(TintVectorTest, ConstructInitializerList_NoSpill) {
+TEST(TintVectorTest, InitializerList_NoSpill) {
     Vector<std::string, 2> vec{"one", "two"};
     EXPECT_EQ(vec.Length(), 2u);
     EXPECT_EQ(vec.Capacity(), 2u);
@@ -122,7 +120,7 @@
     EXPECT_TRUE(AllInternallyHeld(vec));
 }
 
-TEST(TintVectorTest, ConstructInitializerList_WithSpill) {
+TEST(TintVectorTest, InitializerList_WithSpill) {
     Vector<std::string, 2> vec{"one", "two", "three"};
     EXPECT_EQ(vec.Length(), 3u);
     EXPECT_EQ(vec.Capacity(), 3u);
@@ -132,8 +130,8 @@
     EXPECT_TRUE(AllExternallyHeld(vec));
 }
 
-TEST(TintVectorTest, ConstructInitializerList_NoSmallArray) {
-    Vector<std::string> vec{"one", "two"};
+TEST(TintVectorTest, InitializerList_NoSmallArray) {
+    Vector<std::string, 0> vec{"one", "two"};
     EXPECT_EQ(vec.Length(), 2u);
     EXPECT_EQ(vec.Capacity(), 2u);
     EXPECT_EQ(vec[0], "one");
@@ -141,9 +139,180 @@
     EXPECT_TRUE(AllExternallyHeld(vec));
 }
 
-TEST(TintVectorTest, CopyCtor_NoSpill_N2_to_N2) {
+TEST(TintVectorTest, InferTN_1CString) {
+    auto vec = Vector{"one"};
+    static_assert(std::is_same_v<decltype(vec)::value_type, const char*>);
+    static_assert(decltype(vec)::static_length == 1u);
+    EXPECT_EQ(vec.Length(), 1u);
+    EXPECT_EQ(vec.Capacity(), 1u);
+    EXPECT_STREQ(vec[0], "one");
+    EXPECT_TRUE(AllInternallyHeld(vec));
+}
+
+TEST(TintVectorTest, InferTN_2CStrings) {
+    auto vec = Vector{"one", "two"};
+    static_assert(std::is_same_v<decltype(vec)::value_type, const char*>);
+    static_assert(decltype(vec)::static_length == 2u);
+    EXPECT_EQ(vec.Length(), 2u);
+    EXPECT_EQ(vec.Capacity(), 2u);
+    EXPECT_STREQ(vec[0], "one");
+    EXPECT_STREQ(vec[1], "two");
+    EXPECT_TRUE(AllInternallyHeld(vec));
+}
+
+TEST(TintVectorTest, InferTN_IntFloat) {
+    auto vec = Vector{1, 2.0f};
+    static_assert(std::is_same_v<decltype(vec)::value_type, float>);
+    static_assert(decltype(vec)::static_length == 2u);
+    EXPECT_EQ(vec.Length(), 2u);
+    EXPECT_EQ(vec.Capacity(), 2u);
+    EXPECT_EQ(vec[0], 1.0f);
+    EXPECT_EQ(vec[1], 2.0f);
+    EXPECT_TRUE(AllInternallyHeld(vec));
+}
+
+TEST(TintVectorTest, InferTN_IntDoubleIntDouble) {
+    auto vec = Vector{1, 2.0, 3, 4.0};
+    static_assert(std::is_same_v<decltype(vec)::value_type, double>);
+    static_assert(decltype(vec)::static_length == 4u);
+    EXPECT_EQ(vec.Length(), 4u);
+    EXPECT_EQ(vec.Capacity(), 4u);
+    EXPECT_EQ(vec[0], 1.0);
+    EXPECT_EQ(vec[1], 2.0);
+    EXPECT_EQ(vec[2], 3.0);
+    EXPECT_EQ(vec[3], 4.0);
+    EXPECT_TRUE(AllInternallyHeld(vec));
+}
+
+TEST(TintVectorTest, InferTN_C0) {
+    C0 c0;
+    auto vec = Vector{&c0};
+    static_assert(std::is_same_v<decltype(vec)::value_type, C0*>);
+    static_assert(decltype(vec)::static_length == 1u);
+    EXPECT_EQ(vec.Length(), 1u);
+    EXPECT_EQ(vec.Capacity(), 1u);
+    EXPECT_EQ(vec[0], &c0);
+    EXPECT_TRUE(AllInternallyHeld(vec));
+}
+
+TEST(TintVectorTest, InferTN_ConstC0) {
+    const C0 c0;
+    auto vec = Vector{&c0};
+    static_assert(std::is_same_v<decltype(vec)::value_type, const C0*>);
+    static_assert(decltype(vec)::static_length == 1u);
+    EXPECT_EQ(vec.Length(), 1u);
+    EXPECT_EQ(vec.Capacity(), 1u);
+    EXPECT_EQ(vec[0], &c0);
+    EXPECT_TRUE(AllInternallyHeld(vec));
+}
+
+TEST(TintVectorTest, InferTN_C0C1) {
+    C0 c0;
+    C1 c1;
+    auto vec = Vector{&c0, &c1};
+    static_assert(std::is_same_v<decltype(vec)::value_type, C0*>);
+    static_assert(decltype(vec)::static_length == 2u);
+    EXPECT_EQ(vec.Length(), 2u);
+    EXPECT_EQ(vec.Capacity(), 2u);
+    EXPECT_EQ(vec[0], &c0);
+    EXPECT_EQ(vec[1], &c1);
+    EXPECT_TRUE(AllInternallyHeld(vec));
+}
+
+TEST(TintVectorTest, InferTN_ConstC0C1) {
+    const C0 c0;
+    C1 c1;
+    auto vec = Vector{&c0, &c1};
+    static_assert(std::is_same_v<decltype(vec)::value_type, const C0*>);
+    static_assert(decltype(vec)::static_length == 2u);
+    EXPECT_EQ(vec.Length(), 2u);
+    EXPECT_EQ(vec.Capacity(), 2u);
+    EXPECT_EQ(vec[0], &c0);
+    EXPECT_EQ(vec[1], &c1);
+    EXPECT_TRUE(AllInternallyHeld(vec));
+}
+
+TEST(TintVectorTest, InferTN_C0ConstC1) {
+    C0 c0;
+    const C1 c1;
+    auto vec = Vector{&c0, &c1};
+    static_assert(std::is_same_v<decltype(vec)::value_type, const C0*>);
+    static_assert(decltype(vec)::static_length == 2u);
+    EXPECT_EQ(vec.Length(), 2u);
+    EXPECT_EQ(vec.Capacity(), 2u);
+    EXPECT_EQ(vec[0], &c0);
+    EXPECT_EQ(vec[1], &c1);
+    EXPECT_TRUE(AllInternallyHeld(vec));
+}
+
+TEST(TintVectorTest, InferTN_ConstC0ConstC1) {
+    const C0 c0;
+    const C1 c1;
+    auto vec = Vector{&c0, &c1};
+    static_assert(std::is_same_v<decltype(vec)::value_type, const C0*>);
+    static_assert(decltype(vec)::static_length == 2u);
+    EXPECT_EQ(vec.Length(), 2u);
+    EXPECT_EQ(vec.Capacity(), 2u);
+    EXPECT_EQ(vec[0], &c0);
+    EXPECT_EQ(vec[1], &c1);
+    EXPECT_TRUE(AllInternallyHeld(vec));
+}
+
+TEST(TintVectorTest, InferTN_C2aC2b) {
+    C2a c2a;
+    C2b c2b;
+    auto vec = Vector{&c2a, &c2b};
+    static_assert(std::is_same_v<decltype(vec)::value_type, C1*>);
+    static_assert(decltype(vec)::static_length == 2u);
+    EXPECT_EQ(vec.Length(), 2u);
+    EXPECT_EQ(vec.Capacity(), 2u);
+    EXPECT_EQ(vec[0], &c2a);
+    EXPECT_EQ(vec[1], &c2b);
+    EXPECT_TRUE(AllInternallyHeld(vec));
+}
+
+TEST(TintVectorTest, InferTN_ConstC2aC2b) {
+    const C2a c2a;
+    C2b c2b;
+    auto vec = Vector{&c2a, &c2b};
+    static_assert(std::is_same_v<decltype(vec)::value_type, const C1*>);
+    static_assert(decltype(vec)::static_length == 2u);
+    EXPECT_EQ(vec.Length(), 2u);
+    EXPECT_EQ(vec.Capacity(), 2u);
+    EXPECT_EQ(vec[0], &c2a);
+    EXPECT_EQ(vec[1], &c2b);
+    EXPECT_TRUE(AllInternallyHeld(vec));
+}
+
+TEST(TintVectorTest, InferTN_C2aConstC2b) {
+    C2a c2a;
+    const C2b c2b;
+    auto vec = Vector{&c2a, &c2b};
+    static_assert(std::is_same_v<decltype(vec)::value_type, const C1*>);
+    static_assert(decltype(vec)::static_length == 2u);
+    EXPECT_EQ(vec.Length(), 2u);
+    EXPECT_EQ(vec.Capacity(), 2u);
+    EXPECT_EQ(vec[0], &c2a);
+    EXPECT_EQ(vec[1], &c2b);
+    EXPECT_TRUE(AllInternallyHeld(vec));
+}
+
+TEST(TintVectorTest, InferTN_ConstC2aConstC2b) {
+    const C2a c2a;
+    const C2b c2b;
+    auto vec = Vector{&c2a, &c2b};
+    static_assert(std::is_same_v<decltype(vec)::value_type, const C1*>);
+    static_assert(decltype(vec)::static_length == 2u);
+    EXPECT_EQ(vec.Length(), 2u);
+    EXPECT_EQ(vec.Capacity(), 2u);
+    EXPECT_EQ(vec[0], &c2a);
+    EXPECT_EQ(vec[1], &c2b);
+    EXPECT_TRUE(AllInternallyHeld(vec));
+}
+
+TEST(TintVectorTest, CopyVector_NoSpill_N2_to_N2) {
     Vector<std::string, 2> vec_a{"hello", "world"};
-    Vector<std::string, 2> vec_b{vec_a};
+    Vector<std::string, 2> vec_b(vec_a);
     EXPECT_EQ(vec_b.Length(), 2u);
     EXPECT_EQ(vec_b.Capacity(), 2u);
     EXPECT_EQ(vec_b[0], "hello");
@@ -151,9 +320,9 @@
     EXPECT_TRUE(AllInternallyHeld(vec_b));
 }
 
-TEST(TintVectorTest, CopyCtor_WithSpill_N2_to_N2) {
+TEST(TintVectorTest, CopyVector_WithSpill_N2_to_N2) {
     Vector<std::string, 2> vec_a{"hello", "world", "spill"};
-    Vector<std::string, 2> vec_b{vec_a};
+    Vector<std::string, 2> vec_b(vec_a);
     EXPECT_EQ(vec_b.Length(), 3u);
     EXPECT_EQ(vec_b.Capacity(), 3u);
     EXPECT_EQ(vec_b[0], "hello");
@@ -162,30 +331,9 @@
     EXPECT_TRUE(AllExternallyHeld(vec_b));
 }
 
-TEST(TintVectorTest, MoveCtor_NoSpill_N2_to_N2) {
+TEST(TintVectorTest, CopyVector_NoSpill_N2_to_N1) {
     Vector<std::string, 2> vec_a{"hello", "world"};
-    Vector<std::string, 2> vec_b{std::move(vec_a)};
-    EXPECT_EQ(vec_b.Length(), 2u);
-    EXPECT_EQ(vec_b.Capacity(), 2u);
-    EXPECT_EQ(vec_b[0], "hello");
-    EXPECT_EQ(vec_b[1], "world");
-    EXPECT_TRUE(AllInternallyHeld(vec_b));
-}
-
-TEST(TintVectorTest, MoveCtor_WithSpill_N2_to_N2) {
-    Vector<std::string, 2> vec_a{"hello", "world", "spill"};
-    Vector<std::string, 2> vec_b{std::move(vec_a)};
-    EXPECT_EQ(vec_b.Length(), 3u);
-    EXPECT_EQ(vec_b.Capacity(), 3u);
-    EXPECT_EQ(vec_b[0], "hello");
-    EXPECT_EQ(vec_b[1], "world");
-    EXPECT_EQ(vec_b[2], "spill");
-    EXPECT_TRUE(AllExternallyHeld(vec_b));
-}
-
-TEST(TintVectorTest, CopyCtor_NoSpill_N2_to_N1) {
-    Vector<std::string, 2> vec_a{"hello", "world"};
-    Vector<std::string, 1> vec_b{vec_a};
+    Vector<std::string, 1> vec_b(vec_a);
     EXPECT_EQ(vec_b.Length(), 2u);
     EXPECT_EQ(vec_b.Capacity(), 2u);
     EXPECT_EQ(vec_b[0], "hello");
@@ -193,9 +341,9 @@
     EXPECT_TRUE(AllExternallyHeld(vec_b));
 }
 
-TEST(TintVectorTest, CopyCtor_WithSpill_N2_to_N1) {
+TEST(TintVectorTest, CopyVector_WithSpill_N2_to_N1) {
     Vector<std::string, 2> vec_a{"hello", "world", "spill"};
-    Vector<std::string, 1> vec_b{vec_a};
+    Vector<std::string, 1> vec_b(vec_a);
     EXPECT_EQ(vec_b.Length(), 3u);
     EXPECT_EQ(vec_b.Capacity(), 3u);
     EXPECT_EQ(vec_b[0], "hello");
@@ -204,9 +352,111 @@
     EXPECT_TRUE(AllExternallyHeld(vec_b));
 }
 
-TEST(TintVectorTest, MoveCtor_NoSpill_N2_to_N1) {
+TEST(TintVectorTest, CopyVector_NoSpill_N2_to_N3) {
     Vector<std::string, 2> vec_a{"hello", "world"};
-    Vector<std::string, 1> vec_b{std::move(vec_a)};
+    Vector<std::string, 3> vec_b(vec_a);
+    EXPECT_EQ(vec_b.Length(), 2u);
+    EXPECT_EQ(vec_b.Capacity(), 3u);
+    EXPECT_EQ(vec_b[0], "hello");
+    EXPECT_EQ(vec_b[1], "world");
+    EXPECT_TRUE(AllInternallyHeld(vec_b));
+}
+
+TEST(TintVectorTest, CopyVector_WithSpill_N2_to_N3) {
+    Vector<std::string, 2> vec_a{"hello", "world", "spill"};
+    Vector<std::string, 3> vec_b(vec_a);
+    EXPECT_EQ(vec_b.Length(), 3u);
+    EXPECT_EQ(vec_b.Capacity(), 3u);
+    EXPECT_EQ(vec_b[0], "hello");
+    EXPECT_EQ(vec_b[1], "world");
+    EXPECT_EQ(vec_b[2], "spill");
+    EXPECT_TRUE(AllInternallyHeld(vec_b));
+}
+
+TEST(TintVectorTest, CopyVector_NoMoveUpcast_NoSpill) {
+    C2a c2a;
+    C2b c2b;
+    Vector<C1*, 2> vec_a{&c2a, &c2b};
+    Vector<C0*, 2> vec_b(vec_a);  // No move
+    EXPECT_EQ(vec_b[0], &c2a);
+    EXPECT_EQ(vec_b[1], &c2b);
+    EXPECT_TRUE(AllInternallyHeld(vec_b));  // Copied, not moved
+}
+
+TEST(TintVectorTest, CopyVector_NoMoveUpcast_WithSpill) {
+    C2a c2a;
+    C2b c2b;
+    Vector<C1*, 1> vec_a{&c2a, &c2b};
+    Vector<C0*, 2> vec_b(vec_a);  // No move
+    EXPECT_EQ(vec_b[0], &c2a);
+    EXPECT_EQ(vec_b[1], &c2b);
+    EXPECT_TRUE(AllInternallyHeld(vec_b));  // Copied, not moved
+}
+
+TEST(TintVectorTest, CopyVector_NoMoveAddConst_NoSpill) {
+    C2a c2a;
+    C2b c2b;
+    Vector<C1*, 2> vec_a{&c2a, &c2b};
+    Vector<const C1*, 2> vec_b(vec_a);  // No move
+    EXPECT_EQ(vec_b[0], &c2a);
+    EXPECT_EQ(vec_b[1], &c2b);
+    EXPECT_TRUE(AllInternallyHeld(vec_b));  // Copied, not moved
+}
+
+TEST(TintVectorTest, CopyVector_NoMoveAddConst_WithSpill) {
+    C2a c2a;
+    C2b c2b;
+    Vector<C1*, 1> vec_a{&c2a, &c2b};
+    Vector<const C1*, 2> vec_b(vec_a);  // No move
+    EXPECT_EQ(vec_b[0], &c2a);
+    EXPECT_EQ(vec_b[1], &c2b);
+    EXPECT_TRUE(AllInternallyHeld(vec_b));  // Copied, not moved
+}
+
+TEST(TintVectorTest, CopyVector_NoMoveUpcastAndAddConst_NoSpill) {
+    C2a c2a;
+    C2b c2b;
+    Vector<C1*, 2> vec_a{&c2a, &c2b};
+    Vector<const C0*, 2> vec_b(vec_a);  // No move
+    EXPECT_EQ(vec_b[0], &c2a);
+    EXPECT_EQ(vec_b[1], &c2b);
+    EXPECT_TRUE(AllInternallyHeld(vec_b));  // Copied, not moved
+}
+
+TEST(TintVectorTest, CopyVector_NoMoveUpcastAndAddConst_WithSpill) {
+    C2a c2a;
+    C2b c2b;
+    Vector<C1*, 1> vec_a{&c2a, &c2b};
+    Vector<const C0*, 2> vec_b(vec_a);  // No move
+    EXPECT_EQ(vec_b[0], &c2a);
+    EXPECT_EQ(vec_b[1], &c2b);
+    EXPECT_TRUE(AllInternallyHeld(vec_b));  // Copied, not moved
+}
+
+TEST(TintVectorTest, MoveVector_NoSpill_N2_to_N2) {
+    Vector<std::string, 2> vec_a{"hello", "world"};
+    Vector<std::string, 2> vec_b(std::move(vec_a));
+    EXPECT_EQ(vec_b.Length(), 2u);
+    EXPECT_EQ(vec_b.Capacity(), 2u);
+    EXPECT_EQ(vec_b[0], "hello");
+    EXPECT_EQ(vec_b[1], "world");
+    EXPECT_TRUE(AllInternallyHeld(vec_b));
+}
+
+TEST(TintVectorTest, MoveVector_WithSpill_N2_to_N2) {
+    Vector<std::string, 2> vec_a{"hello", "world", "spill"};
+    Vector<std::string, 2> vec_b(std::move(vec_a));
+    EXPECT_EQ(vec_b.Length(), 3u);
+    EXPECT_EQ(vec_b.Capacity(), 3u);
+    EXPECT_EQ(vec_b[0], "hello");
+    EXPECT_EQ(vec_b[1], "world");
+    EXPECT_EQ(vec_b[2], "spill");
+    EXPECT_TRUE(AllExternallyHeld(vec_b));
+}
+
+TEST(TintVectorTest, MoveVector_NoSpill_N2_to_N1) {
+    Vector<std::string, 2> vec_a{"hello", "world"};
+    Vector<std::string, 1> vec_b(std::move(vec_a));
     EXPECT_EQ(vec_b.Length(), 2u);
     EXPECT_EQ(vec_b.Capacity(), 2u);
     EXPECT_EQ(vec_b[0], "hello");
@@ -214,9 +464,9 @@
     EXPECT_TRUE(AllExternallyHeld(vec_b));
 }
 
-TEST(TintVectorTest, MoveCtor_WithSpill_N2_to_N1) {
+TEST(TintVectorTest, MoveVector_WithSpill_N2_to_N1) {
     Vector<std::string, 2> vec_a{"hello", "world", "spill"};
-    Vector<std::string, 1> vec_b{std::move(vec_a)};
+    Vector<std::string, 1> vec_b(std::move(vec_a));
     EXPECT_EQ(vec_b.Length(), 3u);
     EXPECT_EQ(vec_b.Capacity(), 3u);
     EXPECT_EQ(vec_b[0], "hello");
@@ -225,9 +475,9 @@
     EXPECT_TRUE(AllExternallyHeld(vec_b));
 }
 
-TEST(TintVectorTest, CopyCtor_NoSpill_N2_to_N3) {
+TEST(TintVectorTest, MoveVector_NoSpill_N2_to_N3) {
     Vector<std::string, 2> vec_a{"hello", "world"};
-    Vector<std::string, 3> vec_b{vec_a};
+    Vector<std::string, 3> vec_b(std::move(vec_a));
     EXPECT_EQ(vec_b.Length(), 2u);
     EXPECT_EQ(vec_b.Capacity(), 3u);
     EXPECT_EQ(vec_b[0], "hello");
@@ -235,30 +485,9 @@
     EXPECT_TRUE(AllInternallyHeld(vec_b));
 }
 
-TEST(TintVectorTest, CopyCtor_WithSpill_N2_to_N3) {
+TEST(TintVectorTest, MoveVector_WithSpill_N2_to_N3) {
     Vector<std::string, 2> vec_a{"hello", "world", "spill"};
-    Vector<std::string, 3> vec_b{vec_a};
-    EXPECT_EQ(vec_b.Length(), 3u);
-    EXPECT_EQ(vec_b.Capacity(), 3u);
-    EXPECT_EQ(vec_b[0], "hello");
-    EXPECT_EQ(vec_b[1], "world");
-    EXPECT_EQ(vec_b[2], "spill");
-    EXPECT_TRUE(AllInternallyHeld(vec_b));
-}
-
-TEST(TintVectorTest, MoveCtor_NoSpill_N2_to_N3) {
-    Vector<std::string, 2> vec_a{"hello", "world"};
-    Vector<std::string, 3> vec_b{std::move(vec_a)};
-    EXPECT_EQ(vec_b.Length(), 2u);
-    EXPECT_EQ(vec_b.Capacity(), 3u);
-    EXPECT_EQ(vec_b[0], "hello");
-    EXPECT_EQ(vec_b[1], "world");
-    EXPECT_TRUE(AllInternallyHeld(vec_b));
-}
-
-TEST(TintVectorTest, MoveCtor_WithSpill_N2_to_N3) {
-    Vector<std::string, 2> vec_a{"hello", "world", "spill"};
-    Vector<std::string, 3> vec_b{std::move(vec_a)};
+    Vector<std::string, 3> vec_b(std::move(vec_a));
     EXPECT_EQ(vec_b.Length(), 3u);
     EXPECT_EQ(vec_b.Capacity(), 3u);
     EXPECT_EQ(vec_b[0], "hello");
@@ -267,6 +496,66 @@
     EXPECT_TRUE(AllExternallyHeld(vec_b));
 }
 
+TEST(TintVectorTest, MoveVector_Upcast_NoSpill) {
+    C2a c2a;
+    C2b c2b;
+    Vector<C1*, 2> vec_a{&c2a, &c2b};
+    Vector<C0*, 2> vec_b(std::move(vec_a));  // Move
+    EXPECT_EQ(vec_b[0], &c2a);
+    EXPECT_EQ(vec_b[1], &c2b);
+    EXPECT_TRUE(AllInternallyHeld(vec_b));  // Copied, not moved
+}
+
+TEST(TintVectorTest, MoveVector_Upcast_WithSpill) {
+    C2a c2a;
+    C2b c2b;
+    Vector<C1*, 1> vec_a{&c2a, &c2b};
+    Vector<C0*, 2> vec_b(std::move(vec_a));  // Move
+    EXPECT_EQ(vec_b[0], &c2a);
+    EXPECT_EQ(vec_b[1], &c2b);
+    EXPECT_TRUE(AllExternallyHeld(vec_b));  // Moved, not copied
+}
+
+TEST(TintVectorTest, MoveVector_AddConst_NoSpill) {
+    C2a c2a;
+    C2b c2b;
+    Vector<C1*, 2> vec_a{&c2a, &c2b};
+    Vector<const C1*, 2> vec_b(std::move(vec_a));  // Move
+    EXPECT_EQ(vec_b[0], &c2a);
+    EXPECT_EQ(vec_b[1], &c2b);
+    EXPECT_TRUE(AllInternallyHeld(vec_b));  // Copied, not moved
+}
+
+TEST(TintVectorTest, MoveVector_AddConst_WithSpill) {
+    C2a c2a;
+    C2b c2b;
+    Vector<C1*, 1> vec_a{&c2a, &c2b};
+    Vector<const C1*, 2> vec_b(std::move(vec_a));  // Move
+    EXPECT_EQ(vec_b[0], &c2a);
+    EXPECT_EQ(vec_b[1], &c2b);
+    EXPECT_TRUE(AllExternallyHeld(vec_b));  // Moved, not copied
+}
+
+TEST(TintVectorTest, MoveVector_UpcastAndAddConst_NoSpill) {
+    C2a c2a;
+    C2b c2b;
+    Vector<C1*, 2> vec_a{&c2a, &c2b};
+    Vector<const C0*, 2> vec_b(std::move(vec_a));  // Move
+    EXPECT_EQ(vec_b[0], &c2a);
+    EXPECT_EQ(vec_b[1], &c2b);
+    EXPECT_TRUE(AllInternallyHeld(vec_b));  // Copied, not moved
+}
+
+TEST(TintVectorTest, MoveVector_UpcastAndAddConst_WithSpill) {
+    C2a c2a;
+    C2b c2b;
+    Vector<C1*, 1> vec_a{&c2a, &c2b};
+    Vector<const C0*, 2> vec_b(std::move(vec_a));  // Move
+    EXPECT_EQ(vec_b[0], &c2a);
+    EXPECT_EQ(vec_b[1], &c2b);
+    EXPECT_TRUE(AllExternallyHeld(vec_b));  // Moved, not copied
+}
+
 TEST(TintVectorTest, CopyAssign_NoSpill_N2_to_N2) {
     Vector<std::string, 2> vec_a{"hello", "world"};
     Vector<std::string, 2> vec_b;
@@ -290,29 +579,6 @@
     EXPECT_TRUE(AllExternallyHeld(vec_b));
 }
 
-TEST(TintVectorTest, MoveAssign_NoSpill_N2_to_N2) {
-    Vector<std::string, 2> vec_a{"hello", "world"};
-    Vector<std::string, 2> vec_b;
-    vec_b = std::move(vec_a);
-    EXPECT_EQ(vec_b.Length(), 2u);
-    EXPECT_EQ(vec_b.Capacity(), 2u);
-    EXPECT_EQ(vec_b[0], "hello");
-    EXPECT_EQ(vec_b[1], "world");
-    EXPECT_TRUE(AllInternallyHeld(vec_b));
-}
-
-TEST(TintVectorTest, MoveAssign_WithSpill_N2_to_N2) {
-    Vector<std::string, 2> vec_a{"hello", "world", "spill"};
-    Vector<std::string, 2> vec_b;
-    vec_b = std::move(vec_a);
-    EXPECT_EQ(vec_b.Length(), 3u);
-    EXPECT_EQ(vec_b.Capacity(), 3u);
-    EXPECT_EQ(vec_b[0], "hello");
-    EXPECT_EQ(vec_b[1], "world");
-    EXPECT_EQ(vec_b[2], "spill");
-    EXPECT_TRUE(AllExternallyHeld(vec_b));
-}
-
 TEST(TintVectorTest, CopyAssign_NoSpill_N2_to_N1) {
     Vector<std::string, 2> vec_a{"hello", "world"};
     Vector<std::string, 1> vec_b;
@@ -336,29 +602,6 @@
     EXPECT_TRUE(AllExternallyHeld(vec_b));
 }
 
-TEST(TintVectorTest, MoveAssign_NoSpill_N2_to_N1) {
-    Vector<std::string, 2> vec_a{"hello", "world"};
-    Vector<std::string, 1> vec_b;
-    vec_b = std::move(vec_a);
-    EXPECT_EQ(vec_b.Length(), 2u);
-    EXPECT_EQ(vec_b.Capacity(), 2u);
-    EXPECT_EQ(vec_b[0], "hello");
-    EXPECT_EQ(vec_b[1], "world");
-    EXPECT_TRUE(AllExternallyHeld(vec_b));
-}
-
-TEST(TintVectorTest, MoveAssign_SpillSpill_N2_to_N1) {
-    Vector<std::string, 2> vec_a{"hello", "world", "spill"};
-    Vector<std::string, 1> vec_b;
-    vec_b = std::move(vec_a);
-    EXPECT_EQ(vec_b.Length(), 3u);
-    EXPECT_EQ(vec_b.Capacity(), 3u);
-    EXPECT_EQ(vec_b[0], "hello");
-    EXPECT_EQ(vec_b[1], "world");
-    EXPECT_EQ(vec_b[2], "spill");
-    EXPECT_TRUE(AllExternallyHeld(vec_b));
-}
-
 TEST(TintVectorTest, CopyAssign_NoSpill_N2_to_N3) {
     Vector<std::string, 2> vec_a{"hello", "world"};
     Vector<std::string, 3> vec_b;
@@ -382,32 +625,9 @@
     EXPECT_TRUE(AllInternallyHeld(vec_b));
 }
 
-TEST(TintVectorTest, MoveAssign_NoSpill_N2_to_N3) {
-    Vector<std::string, 2> vec_a{"hello", "world"};
-    Vector<std::string, 3> vec_b;
-    vec_b = std::move(vec_a);
-    EXPECT_EQ(vec_b.Length(), 2u);
-    EXPECT_EQ(vec_b.Capacity(), 3u);
-    EXPECT_EQ(vec_b[0], "hello");
-    EXPECT_EQ(vec_b[1], "world");
-    EXPECT_TRUE(AllInternallyHeld(vec_b));
-}
-
-TEST(TintVectorTest, MoveAssign_WithSpill_N2_to_N3) {
-    Vector<std::string, 2> vec_a{"hello", "world", "spill"};
-    Vector<std::string, 3> vec_b;
-    vec_b = std::move(vec_a);
-    EXPECT_EQ(vec_b.Length(), 3u);
-    EXPECT_EQ(vec_b.Capacity(), 3u);
-    EXPECT_EQ(vec_b[0], "hello");
-    EXPECT_EQ(vec_b[1], "world");
-    EXPECT_EQ(vec_b[2], "spill");
-    EXPECT_TRUE(AllExternallyHeld(vec_b));
-}
-
 TEST(TintVectorTest, CopyAssign_NoSpill_N2_to_N0) {
     Vector<std::string, 2> vec_a{"hello", "world"};
-    Vector<std::string> vec_b;
+    Vector<std::string, 0> vec_b;
     vec_b = vec_a;
     EXPECT_EQ(vec_b.Length(), 2u);
     EXPECT_EQ(vec_b.Capacity(), 2u);
@@ -418,7 +638,7 @@
 
 TEST(TintVectorTest, CopyAssign_WithSpill_N2_to_N0) {
     Vector<std::string, 2> vec_a{"hello", "world", "spill"};
-    Vector<std::string> vec_b;
+    Vector<std::string, 0> vec_b;
     vec_b = vec_a;
     EXPECT_EQ(vec_b.Length(), 3u);
     EXPECT_EQ(vec_b.Capacity(), 3u);
@@ -428,29 +648,6 @@
     EXPECT_TRUE(AllExternallyHeld(vec_b));
 }
 
-TEST(TintVectorTest, MoveAssign_NoSpill_N2_to_N0) {
-    Vector<std::string, 2> vec_a{"hello", "world"};
-    Vector<std::string> vec_b;
-    vec_b = std::move(vec_a);
-    EXPECT_EQ(vec_b.Length(), 2u);
-    EXPECT_EQ(vec_b.Capacity(), 2u);
-    EXPECT_EQ(vec_b[0], "hello");
-    EXPECT_EQ(vec_b[1], "world");
-    EXPECT_TRUE(AllExternallyHeld(vec_b));
-}
-
-TEST(TintVectorTest, MoveAssign_WithSpill_N2_to_N0) {
-    Vector<std::string, 2> vec_a{"hello", "world", "spill"};
-    Vector<std::string> vec_b;
-    vec_b = std::move(vec_a);
-    EXPECT_EQ(vec_b.Length(), 3u);
-    EXPECT_EQ(vec_b.Capacity(), 3u);
-    EXPECT_EQ(vec_b[0], "hello");
-    EXPECT_EQ(vec_b[1], "world");
-    EXPECT_EQ(vec_b[2], "spill");
-    EXPECT_TRUE(AllExternallyHeld(vec_b));
-}
-
 TEST(TintVectorTest, CopyAssign_Self_NoSpill) {
     Vector<std::string, 2> vec{"hello", "world"};
     auto* vec_ptr = &vec;  // Used to avoid -Wself-assign-overloaded
@@ -473,6 +670,98 @@
     EXPECT_TRUE(AllExternallyHeld(vec));
 }
 
+TEST(TintVectorTest, MoveAssign_NoSpill_N2_to_N2) {
+    Vector<std::string, 2> vec_a{"hello", "world"};
+    Vector<std::string, 2> vec_b;
+    vec_b = std::move(vec_a);
+    EXPECT_EQ(vec_b.Length(), 2u);
+    EXPECT_EQ(vec_b.Capacity(), 2u);
+    EXPECT_EQ(vec_b[0], "hello");
+    EXPECT_EQ(vec_b[1], "world");
+    EXPECT_TRUE(AllInternallyHeld(vec_b));
+}
+
+TEST(TintVectorTest, MoveAssign_WithSpill_N2_to_N2) {
+    Vector<std::string, 2> vec_a{"hello", "world", "spill"};
+    Vector<std::string, 2> vec_b;
+    vec_b = std::move(vec_a);
+    EXPECT_EQ(vec_b.Length(), 3u);
+    EXPECT_EQ(vec_b.Capacity(), 3u);
+    EXPECT_EQ(vec_b[0], "hello");
+    EXPECT_EQ(vec_b[1], "world");
+    EXPECT_EQ(vec_b[2], "spill");
+    EXPECT_TRUE(AllExternallyHeld(vec_b));
+}
+
+TEST(TintVectorTest, MoveAssign_NoSpill_N2_to_N1) {
+    Vector<std::string, 2> vec_a{"hello", "world"};
+    Vector<std::string, 1> vec_b;
+    vec_b = std::move(vec_a);
+    EXPECT_EQ(vec_b.Length(), 2u);
+    EXPECT_EQ(vec_b.Capacity(), 2u);
+    EXPECT_EQ(vec_b[0], "hello");
+    EXPECT_EQ(vec_b[1], "world");
+    EXPECT_TRUE(AllExternallyHeld(vec_b));
+}
+
+TEST(TintVectorTest, MoveAssign_SpillSpill_N2_to_N1) {
+    Vector<std::string, 2> vec_a{"hello", "world", "spill"};
+    Vector<std::string, 1> vec_b;
+    vec_b = std::move(vec_a);
+    EXPECT_EQ(vec_b.Length(), 3u);
+    EXPECT_EQ(vec_b.Capacity(), 3u);
+    EXPECT_EQ(vec_b[0], "hello");
+    EXPECT_EQ(vec_b[1], "world");
+    EXPECT_EQ(vec_b[2], "spill");
+    EXPECT_TRUE(AllExternallyHeld(vec_b));
+}
+
+TEST(TintVectorTest, MoveAssign_NoSpill_N2_to_N3) {
+    Vector<std::string, 2> vec_a{"hello", "world"};
+    Vector<std::string, 3> vec_b;
+    vec_b = std::move(vec_a);
+    EXPECT_EQ(vec_b.Length(), 2u);
+    EXPECT_EQ(vec_b.Capacity(), 3u);
+    EXPECT_EQ(vec_b[0], "hello");
+    EXPECT_EQ(vec_b[1], "world");
+    EXPECT_TRUE(AllInternallyHeld(vec_b));
+}
+
+TEST(TintVectorTest, MoveAssign_WithSpill_N2_to_N3) {
+    Vector<std::string, 2> vec_a{"hello", "world", "spill"};
+    Vector<std::string, 3> vec_b;
+    vec_b = std::move(vec_a);
+    EXPECT_EQ(vec_b.Length(), 3u);
+    EXPECT_EQ(vec_b.Capacity(), 3u);
+    EXPECT_EQ(vec_b[0], "hello");
+    EXPECT_EQ(vec_b[1], "world");
+    EXPECT_EQ(vec_b[2], "spill");
+    EXPECT_TRUE(AllExternallyHeld(vec_b));
+}
+
+TEST(TintVectorTest, MoveAssign_NoSpill_N2_to_N0) {
+    Vector<std::string, 2> vec_a{"hello", "world"};
+    Vector<std::string, 0> vec_b;
+    vec_b = std::move(vec_a);
+    EXPECT_EQ(vec_b.Length(), 2u);
+    EXPECT_EQ(vec_b.Capacity(), 2u);
+    EXPECT_EQ(vec_b[0], "hello");
+    EXPECT_EQ(vec_b[1], "world");
+    EXPECT_TRUE(AllExternallyHeld(vec_b));
+}
+
+TEST(TintVectorTest, MoveAssign_WithSpill_N2_to_N0) {
+    Vector<std::string, 2> vec_a{"hello", "world", "spill"};
+    Vector<std::string, 0> vec_b;
+    vec_b = std::move(vec_a);
+    EXPECT_EQ(vec_b.Length(), 3u);
+    EXPECT_EQ(vec_b.Capacity(), 3u);
+    EXPECT_EQ(vec_b[0], "hello");
+    EXPECT_EQ(vec_b[1], "world");
+    EXPECT_EQ(vec_b[2], "spill");
+    EXPECT_TRUE(AllExternallyHeld(vec_b));
+}
+
 TEST(TintVectorTest, MoveAssign_Self_NoSpill) {
     Vector<std::string, 2> vec{"hello", "world"};
     auto* vec_ptr = &vec;  // Used to avoid -Wself-move
@@ -541,7 +830,7 @@
     EXPECT_EQ(vec[1], "world");
 }
 
-TEST(TintVectorTest, SmallArray_Reserve_NoSpill) {
+TEST(TintVectorTest, Reserve_NoSpill) {
     Vector<std::string, 2> vec;
     EXPECT_EQ(vec.Length(), 0u);
     EXPECT_EQ(vec.Capacity(), 2u);
@@ -562,7 +851,7 @@
     EXPECT_TRUE(AllInternallyHeld(vec));
 }
 
-TEST(TintVectorTest, SmallArray_Reserve_WithSpill) {
+TEST(TintVectorTest, Reserve_WithSpill) {
     Vector<std::string, 1> vec;
     EXPECT_EQ(vec.Length(), 0u);
     EXPECT_EQ(vec.Capacity(), 1u);
@@ -584,7 +873,7 @@
     EXPECT_TRUE(AllExternallyHeld(vec));
 }
 
-TEST(TintVectorTest, SmallArray_Resize_NoSpill) {
+TEST(TintVectorTest, ResizeZero_NoSpill) {
     Vector<std::string, 2> vec;
     EXPECT_EQ(vec.Length(), 0u);
     EXPECT_EQ(vec.Capacity(), 2u);
@@ -614,7 +903,7 @@
     EXPECT_TRUE(AllInternallyHeld(vec));
 }
 
-TEST(TintVectorTest, SmallArray_Resize_WithSpill) {
+TEST(TintVectorTest, ResizeZero_WithSpill) {
     Vector<std::string, 1> vec;
     EXPECT_EQ(vec.Length(), 0u);
     EXPECT_EQ(vec.Capacity(), 1u);
@@ -644,8 +933,68 @@
     EXPECT_TRUE(AllExternallyHeld(vec));
 }
 
+TEST(TintVectorTest, ResizeValue_NoSpill) {
+    Vector<std::string, 2> vec;
+    EXPECT_EQ(vec.Length(), 0u);
+    EXPECT_EQ(vec.Capacity(), 2u);
+    vec.Resize(1, "meow");
+    EXPECT_EQ(vec.Length(), 1u);
+    EXPECT_EQ(vec.Capacity(), 2u);
+    EXPECT_EQ(vec[0], "meow");
+    EXPECT_TRUE(AllInternallyHeld(vec));
+    vec[0] = "hello";
+    vec.Resize(2, "woof");
+    EXPECT_EQ(vec.Length(), 2u);
+    EXPECT_EQ(vec.Capacity(), 2u);
+    EXPECT_EQ(vec[0], "hello");
+    EXPECT_EQ(vec[1], "woof");
+    EXPECT_TRUE(AllInternallyHeld(vec));
+    vec[1] = "world";
+    vec.Resize(1, "quack");
+    EXPECT_EQ(vec.Length(), 1u);
+    EXPECT_EQ(vec.Capacity(), 2u);
+    EXPECT_EQ(vec[0], "hello");
+    EXPECT_TRUE(AllInternallyHeld(vec));
+    vec.Resize(2, "hiss");
+    EXPECT_EQ(vec.Length(), 2u);
+    EXPECT_EQ(vec.Capacity(), 2u);
+    EXPECT_EQ(vec[0], "hello");
+    EXPECT_EQ(vec[1], "hiss");
+    EXPECT_TRUE(AllInternallyHeld(vec));
+}
+
+TEST(TintVectorTest, ResizeValue_WithSpill) {
+    Vector<std::string, 1> vec;
+    EXPECT_EQ(vec.Length(), 0u);
+    EXPECT_EQ(vec.Capacity(), 1u);
+    vec.Resize(1, "meow");
+    EXPECT_EQ(vec.Length(), 1u);
+    EXPECT_EQ(vec.Capacity(), 1u);
+    EXPECT_EQ(vec[0], "meow");
+    EXPECT_TRUE(AllInternallyHeld(vec));
+    vec[0] = "hello";
+    vec.Resize(2, "woof");
+    EXPECT_EQ(vec.Length(), 2u);
+    EXPECT_EQ(vec.Capacity(), 2u);
+    EXPECT_EQ(vec[0], "hello");
+    EXPECT_EQ(vec[1], "woof");
+    EXPECT_TRUE(AllExternallyHeld(vec));
+    vec[1] = "world";
+    vec.Resize(1, "quack");
+    EXPECT_EQ(vec.Length(), 1u);
+    EXPECT_EQ(vec.Capacity(), 2u);
+    EXPECT_EQ(vec[0], "hello");
+    EXPECT_TRUE(AllExternallyHeld(vec));
+    vec.Resize(2, "hiss");
+    EXPECT_EQ(vec.Length(), 2u);
+    EXPECT_EQ(vec.Capacity(), 2u);
+    EXPECT_EQ(vec[0], "hello");
+    EXPECT_EQ(vec[1], "hiss");
+    EXPECT_TRUE(AllExternallyHeld(vec));
+}
+
 TEST(TintVectorTest, Reserve_NoSmallArray) {
-    Vector<std::string> vec;
+    Vector<std::string, 0> vec;
     EXPECT_EQ(vec.Length(), 0u);
     EXPECT_EQ(vec.Capacity(), 0u);
     vec.Reserve(1);
@@ -667,7 +1016,7 @@
 }
 
 TEST(TintVectorTest, Resize_NoSmallArray) {
-    Vector<std::string> vec;
+    Vector<std::string, 0> vec;
     EXPECT_EQ(vec.Length(), 0u);
     EXPECT_EQ(vec.Capacity(), 0u);
     vec.Resize(1);
@@ -925,60 +1274,67 @@
 }
 
 TEST(TintVectorTest, PushPop_StringNoSpill) {
+    const std::string hello = "hello";
+    const std::string world = "world";
+
     Vector<std::string, 2> vec;
     EXPECT_EQ(vec.Length(), 0u);
     EXPECT_TRUE(AllInternallyHeld(vec));
 
-    vec.Push("hello");
+    vec.Push(hello);
     EXPECT_EQ(vec.Length(), 1u);
     EXPECT_TRUE(AllInternallyHeld(vec));
 
-    vec.Push("world");
+    vec.Push(world);
     EXPECT_EQ(vec.Length(), 2u);
     EXPECT_TRUE(AllInternallyHeld(vec));
 
-    EXPECT_EQ(vec.Pop(), "world");
+    EXPECT_EQ(vec.Pop(), world);
     EXPECT_EQ(vec.Length(), 1u);
     EXPECT_TRUE(AllInternallyHeld(vec));
 
-    EXPECT_EQ(vec.Pop(), "hello");
+    EXPECT_EQ(vec.Pop(), hello);
     EXPECT_EQ(vec.Length(), 0u);
     EXPECT_TRUE(AllInternallyHeld(vec));
 }
 
 TEST(TintVectorTest, PushPop_StringWithSpill) {
+    const std::string hello = "hello";
+    const std::string world = "world";
+
     Vector<std::string, 1> vec;
     EXPECT_EQ(vec.Length(), 0u);
     EXPECT_TRUE(AllInternallyHeld(vec));
 
-    vec.Push("hello");
+    vec.Push(hello);
     EXPECT_EQ(vec.Length(), 1u);
     EXPECT_TRUE(AllInternallyHeld(vec));
 
-    vec.Push("world");
+    vec.Push(world);
     EXPECT_EQ(vec.Length(), 2u);
     EXPECT_TRUE(AllExternallyHeld(vec));
 
-    EXPECT_EQ(vec.Pop(), "world");
+    EXPECT_EQ(vec.Pop(), world);
     EXPECT_EQ(vec.Length(), 1u);
     EXPECT_TRUE(AllExternallyHeld(vec));
 
-    EXPECT_EQ(vec.Pop(), "hello");
+    EXPECT_EQ(vec.Pop(), hello);
     EXPECT_EQ(vec.Length(), 0u);
     EXPECT_TRUE(AllExternallyHeld(vec));
 }
 
 TEST(TintVectorTest, PushPop_StringMoveNoSpill) {
+    std::string hello = "hello";
+    std::string world = "world";
+
     Vector<std::string, 2> vec;
     EXPECT_EQ(vec.Length(), 0u);
     EXPECT_TRUE(AllInternallyHeld(vec));
 
-    std::string hello = "hello";
     vec.Push(std::move(hello));
     EXPECT_EQ(vec.Length(), 1u);
     EXPECT_TRUE(AllInternallyHeld(vec));
 
-    std::string world = "world";
     vec.Push(std::move(world));
     EXPECT_EQ(vec.Length(), 2u);
     EXPECT_TRUE(AllInternallyHeld(vec));
@@ -993,15 +1349,18 @@
 }
 
 TEST(TintVectorTest, PushPop_StringMoveWithSpill) {
+    std::string hello = "hello";
+    std::string world = "world";
+
     Vector<std::string, 1> vec;
     EXPECT_EQ(vec.Length(), 0u);
     EXPECT_TRUE(AllInternallyHeld(vec));
 
-    vec.Push("hello");
+    vec.Push(std::move(hello));
     EXPECT_EQ(vec.Length(), 1u);
     EXPECT_TRUE(AllInternallyHeld(vec));
 
-    vec.Push("world");
+    vec.Push(std::move(world));
     EXPECT_EQ(vec.Length(), 2u);
     EXPECT_TRUE(AllExternallyHeld(vec));
 
@@ -1131,42 +1490,195 @@
     EXPECT_EQ(vec.end(), &vec[0] + 3);
 }
 
-TEST(TintVectorRefTest, CtorVectorNoMove) {
-    Vector<std::string, 1> vec_a{"one", "two"};
-    VectorRef<std::string> vec_ref(vec_a);  // No move
-    Vector<std::string, 2> vec_b(std::move(vec_ref));
-    EXPECT_EQ(vec_b[0], "one");
-    EXPECT_EQ(vec_b[1], "two");
-    EXPECT_TRUE(AllInternallyHeld(vec_b));  // Copy, no move
-}
-
-TEST(TintVectorRefTest, CtorVectorMove) {
-    Vector<std::string, 1> vec_a{"one", "two"};
-    VectorRef<std::string> vec_ref(std::move(vec_a));  // Move
-    Vector<std::string, 2> vec_b(std::move(vec_ref));
-    EXPECT_EQ(vec_b[0], "one");
-    EXPECT_EQ(vec_b[1], "two");
-    EXPECT_TRUE(AllExternallyHeld(vec_b));  // Move, no copy
-}
-
-TEST(TintVectorRefTest, CopyCtor) {
+////////////////////////////////////////////////////////////////////////////////
+// TintVectorRefTest
+////////////////////////////////////////////////////////////////////////////////
+TEST(TintVectorRefTest, CopyVectorRef) {
     Vector<std::string, 1> vec_a{"one", "two"};
     VectorRef<std::string> vec_ref_a(std::move(vec_a));
     VectorRef<std::string> vec_ref_b(vec_ref_a);  // No move
     Vector<std::string, 2> vec_b(std::move(vec_ref_b));
     EXPECT_EQ(vec_b[0], "one");
     EXPECT_EQ(vec_b[1], "two");
-    EXPECT_TRUE(AllInternallyHeld(vec_b));  // Copy, no move
+    EXPECT_TRUE(AllInternallyHeld(vec_b));  // Copied, not moved
 }
 
-TEST(TintVectorRefTest, MoveCtor) {
+TEST(TintVectorRefTest, CopyVectorRef_Upcast) {
+    C2a c2a;
+    C2b c2b;
+    Vector<C1*, 1> vec_a{&c2a, &c2b};
+    VectorRef<C1*> vec_ref_a(std::move(vec_a));
+    VectorRef<C0*> vec_ref_b(vec_ref_a);  // No-move. Up-cast
+    Vector<C0*, 2> vec_b(std::move(vec_ref_b));
+    EXPECT_EQ(vec_b[0], &c2a);
+    EXPECT_EQ(vec_b[1], &c2b);
+    EXPECT_TRUE(AllInternallyHeld(vec_b));  // Copied, not moved
+}
+
+TEST(TintVectorRefTest, CopyVectorRef_AddConst) {
+    C2a c2a;
+    C2b c2b;
+    Vector<C1*, 1> vec_a{&c2a, &c2b};
+    VectorRef<C1*> vec_ref_a(std::move(vec_a));
+    VectorRef<const C1*> vec_ref_b(vec_ref_a);  // No-move. Up-cast
+    Vector<const C1*, 2> vec_b(std::move(vec_ref_b));
+    EXPECT_EQ(vec_b[0], &c2a);
+    EXPECT_EQ(vec_b[1], &c2b);
+    EXPECT_TRUE(AllInternallyHeld(vec_b));  // Copied, not moved
+}
+
+TEST(TintVectorRefTest, CopyVectorRef_UpcastAndAddConst) {
+    C2a c2a;
+    C2b c2b;
+    Vector<C1*, 1> vec_a{&c2a, &c2b};
+    VectorRef<C1*> vec_ref_a(std::move(vec_a));
+    VectorRef<const C0*> vec_ref_b(vec_ref_a);  // No-move. Up-cast
+    Vector<const C0*, 2> vec_b(std::move(vec_ref_b));
+    EXPECT_EQ(vec_b[0], &c2a);
+    EXPECT_EQ(vec_b[1], &c2b);
+    EXPECT_TRUE(AllInternallyHeld(vec_b));  // Copied, not moved
+}
+
+TEST(TintVectorRefTest, MoveVectorRef) {
     Vector<std::string, 1> vec_a{"one", "two"};
     VectorRef<std::string> vec_ref_a(std::move(vec_a));  // Move
     VectorRef<std::string> vec_ref_b(std::move(vec_ref_a));
     Vector<std::string, 2> vec_b(std::move(vec_ref_b));
     EXPECT_EQ(vec_b[0], "one");
     EXPECT_EQ(vec_b[1], "two");
-    EXPECT_TRUE(AllExternallyHeld(vec_b));  // Move, no copy
+    EXPECT_TRUE(AllExternallyHeld(vec_b));  // Moved, not copied
+}
+
+TEST(TintVectorRefTest, MoveVectorRef_Upcast) {
+    C2a c2a;
+    C2b c2b;
+    Vector<C1*, 1> vec_a{&c2a, &c2b};
+    VectorRef<C1*> vec_ref_a(std::move(vec_a));
+    VectorRef<C0*> vec_ref_b(std::move(vec_ref_a));  // Moved. Up-cast
+    Vector<C0*, 2> vec_b(std::move(vec_ref_b));
+    EXPECT_EQ(vec_b[0], &c2a);
+    EXPECT_EQ(vec_b[1], &c2b);
+    EXPECT_TRUE(AllExternallyHeld(vec_b));  // Moved, not copied
+}
+
+TEST(TintVectorRefTest, MoveVectorRef_AddConst) {
+    C2a c2a;
+    C2b c2b;
+    Vector<C1*, 1> vec_a{&c2a, &c2b};
+    VectorRef<C1*> vec_ref_a(std::move(vec_a));
+    VectorRef<const C1*> vec_ref_b(std::move(vec_ref_a));  // Moved. Up-cast
+    Vector<const C1*, 2> vec_b(std::move(vec_ref_b));
+    EXPECT_EQ(vec_b[0], &c2a);
+    EXPECT_EQ(vec_b[1], &c2b);
+    EXPECT_TRUE(AllExternallyHeld(vec_b));  // Moved, not copied
+}
+
+TEST(TintVectorRefTest, MoveVectorRef_UpcastAndAddConst) {
+    C2a c2a;
+    C2b c2b;
+    Vector<C1*, 1> vec_a{&c2a, &c2b};
+    VectorRef<C1*> vec_ref_a(std::move(vec_a));
+    VectorRef<const C0*> vec_ref_b(std::move(vec_ref_a));  // Moved. Up-cast
+    Vector<const C0*, 2> vec_b(std::move(vec_ref_b));
+    EXPECT_EQ(vec_b[0], &c2a);
+    EXPECT_EQ(vec_b[1], &c2b);
+    EXPECT_TRUE(AllExternallyHeld(vec_b));  // Moved, not copied
+}
+
+TEST(TintVectorRefTest, CopyVector) {
+    Vector<std::string, 1> vec_a{"one", "two"};
+    VectorRef<std::string> vec_ref(vec_a);  // No move
+    Vector<std::string, 2> vec_b(std::move(vec_ref));
+    EXPECT_EQ(vec_b[0], "one");
+    EXPECT_EQ(vec_b[1], "two");
+    EXPECT_TRUE(AllInternallyHeld(vec_b));  // Copied, not moved
+}
+
+TEST(TintVectorRefTest, CopyVector_Upcast) {
+    C2a c2a;
+    C2b c2b;
+    Vector<C1*, 1> vec_a{&c2a, &c2b};
+    VectorRef<C0*> vec_ref(vec_a);  // No move
+    EXPECT_EQ(vec_ref[0], &c2a);
+    EXPECT_EQ(vec_ref[1], &c2b);
+    Vector<C0*, 2> vec_b(std::move(vec_ref));
+    EXPECT_EQ(vec_b[0], &c2a);
+    EXPECT_EQ(vec_b[1], &c2b);
+    EXPECT_TRUE(AllInternallyHeld(vec_b));  // Copied, not moved
+}
+
+TEST(TintVectorRefTest, CopyVector_AddConst) {
+    C2a c2a;
+    C2b c2b;
+    Vector<C1*, 1> vec_a{&c2a, &c2b};
+    VectorRef<const C1*> vec_ref(vec_a);  // No move
+    EXPECT_EQ(vec_ref[0], &c2a);
+    EXPECT_EQ(vec_ref[1], &c2b);
+    Vector<const C1*, 2> vec_b(std::move(vec_ref));
+    EXPECT_EQ(vec_b[0], &c2a);
+    EXPECT_EQ(vec_b[1], &c2b);
+    EXPECT_TRUE(AllInternallyHeld(vec_b));  // Copied, not moved
+}
+
+TEST(TintVectorRefTest, CopyVector_UpcastAndAddConst) {
+    C2a c2a;
+    C2b c2b;
+    Vector<C1*, 1> vec_a{&c2a, &c2b};
+    VectorRef<const C0*> vec_ref(vec_a);  // No move
+    EXPECT_EQ(vec_ref[0], &c2a);
+    EXPECT_EQ(vec_ref[1], &c2b);
+    Vector<const C0*, 2> vec_b(std::move(vec_ref));
+    EXPECT_EQ(vec_b[0], &c2a);
+    EXPECT_EQ(vec_b[1], &c2b);
+    EXPECT_TRUE(AllInternallyHeld(vec_b));  // Copied, not moved
+}
+
+TEST(TintVectorRefTest, MoveVector) {
+    Vector<std::string, 1> vec_a{"one", "two"};
+    VectorRef<std::string> vec_ref(std::move(vec_a));  // Move
+    Vector<std::string, 2> vec_b(std::move(vec_ref));
+    EXPECT_EQ(vec_b[0], "one");
+    EXPECT_EQ(vec_b[1], "two");
+    EXPECT_TRUE(AllExternallyHeld(vec_b));  // Moved, not copied
+}
+
+TEST(TintVectorRefTest, MoveVector_Upcast) {
+    C2a c2a;
+    C2b c2b;
+    Vector<C1*, 1> vec_a{&c2a, &c2b};
+    VectorRef<C0*> vec_ref(std::move(vec_a));  // Move
+    EXPECT_EQ(vec_ref[0], &c2a);
+    EXPECT_EQ(vec_ref[1], &c2b);
+    Vector<C0*, 2> vec_b(std::move(vec_ref));
+    EXPECT_EQ(vec_b[0], &c2a);
+    EXPECT_EQ(vec_b[1], &c2b);
+    EXPECT_TRUE(AllExternallyHeld(vec_b));  // Moved, not copied
+}
+
+TEST(TintVectorRefTest, MoveVector_AddConst) {
+    C2a c2a;
+    C2b c2b;
+    Vector<C1*, 1> vec_a{&c2a, &c2b};
+    VectorRef<const C1*> vec_ref(std::move(vec_a));  // Move
+    EXPECT_EQ(vec_ref[0], &c2a);
+    EXPECT_EQ(vec_ref[1], &c2b);
+    Vector<const C1*, 2> vec_b(std::move(vec_ref));
+    EXPECT_EQ(vec_b[0], &c2a);
+    EXPECT_EQ(vec_b[1], &c2b);
+    EXPECT_TRUE(AllExternallyHeld(vec_b));  // Moved, not copied
+}
+
+TEST(TintVectorRefTest, MoveVector_UpcastAndAddConst) {
+    C2a c2a;
+    C2b c2b;
+    Vector<C1*, 1> vec_a{&c2a, &c2b};
+    VectorRef<const C0*> vec_ref(std::move(vec_a));  // Move
+    EXPECT_EQ(vec_ref[0], &c2a);
+    EXPECT_EQ(vec_ref[1], &c2b);
+    Vector<const C0*, 2> vec_b(std::move(vec_ref));
+    EXPECT_EQ(vec_b[0], &c2a);
+    EXPECT_EQ(vec_b[1], &c2b);
+    EXPECT_TRUE(AllExternallyHeld(vec_b));  // Moved, not copied
 }
 
 TEST(TintVectorRefTest, Index) {
@@ -1243,42 +1755,130 @@
     EXPECT_EQ(vec_ref.end(), &vec[0] + 3);
 }
 
-TEST(TintVectorConstRefTest, CtorVectorNoMove) {
+////////////////////////////////////////////////////////////////////////////////
+// TintVectorConstRefTest
+////////////////////////////////////////////////////////////////////////////////
+TEST(TintVectorConstRefTest, CopyVectorConstRef) {
     Vector<std::string, 1> vec_a{"one", "two"};
-    ConstVectorRef<std::string> vec_ref(vec_a);  // No move
-    Vector<std::string, 2> vec_b(std::move(vec_ref));
+    ConstVectorRef<std::string> vec_ref_a(vec_a);
+    ConstVectorRef<std::string> vec_ref_b(vec_ref_a);
+    Vector<std::string, 2> vec_b(vec_ref_b);
     EXPECT_EQ(vec_b[0], "one");
     EXPECT_EQ(vec_b[1], "two");
-    EXPECT_TRUE(AllInternallyHeld(vec_b));  // Copy, no move
+    EXPECT_TRUE(AllInternallyHeld(vec_b));  // Copied, not moved
 }
 
-TEST(TintVectorConstRefTest, CtorVectorMove) {
-    Vector<std::string, 1> vec_a{"one", "two"};
-    ConstVectorRef<std::string> vec_ref(std::move(vec_a));  // Move
-    Vector<std::string, 2> vec_b(std::move(vec_ref));
-    EXPECT_EQ(vec_b[0], "one");
-    EXPECT_EQ(vec_b[1], "two");
-    EXPECT_TRUE(AllInternallyHeld(vec_b));  // Copy, no move
+TEST(TintVectorConstRefTest, CopyVectorConstRef_Upcast) {
+    C2a c2a;
+    C2b c2b;
+    Vector<C1*, 1> vec_a{&c2a, &c2b};
+    ConstVectorRef<C1*> vec_ref_a(vec_a);
+    ConstVectorRef<C0*> vec_ref_b(vec_ref_a);  // Up-cast
+    Vector<C0*, 2> vec_b(vec_ref_b);
+    EXPECT_EQ(vec_b[0], &c2a);
+    EXPECT_EQ(vec_b[1], &c2b);
+    EXPECT_TRUE(AllInternallyHeld(vec_b));  // Copied, not moved
 }
 
-TEST(TintVectorConstRefTest, CopyCtor) {
-    Vector<std::string, 1> vec_a{"one", "two"};
-    ConstVectorRef<std::string> vec_ref_a(std::move(vec_a));
-    ConstVectorRef<std::string> vec_ref_b(vec_ref_a);  // No move
-    Vector<std::string, 2> vec_b(std::move(vec_ref_b));
-    EXPECT_EQ(vec_b[0], "one");
-    EXPECT_EQ(vec_b[1], "two");
-    EXPECT_TRUE(AllInternallyHeld(vec_b));  // Copy, no move
+TEST(TintVectorConstRefTest, CopyVectorConstRef_AddConst) {
+    C2a c2a;
+    C2b c2b;
+    Vector<C1*, 1> vec_a{&c2a, &c2b};
+    ConstVectorRef<C1*> vec_ref_a(vec_a);
+    ConstVectorRef<const C1*> vec_ref_b(vec_ref_a);  // Up-cast
+    Vector<const C1*, 2> vec_b(vec_ref_b);
+    EXPECT_EQ(vec_b[0], &c2a);
+    EXPECT_EQ(vec_b[1], &c2b);
+    EXPECT_TRUE(AllInternallyHeld(vec_b));  // Copied, not moved
 }
 
-TEST(TintVectorConstRefTest, MoveCtor) {
+TEST(TintVectorConstRefTest, CopyVectorConstRef_UpcastAndAddConst) {
+    C2a c2a;
+    C2b c2b;
+    Vector<C1*, 1> vec_a{&c2a, &c2b};
+    ConstVectorRef<C1*> vec_ref_a(vec_a);
+    ConstVectorRef<const C0*> vec_ref_b(vec_ref_a);  // Up-cast
+    Vector<const C0*, 2> vec_b(vec_ref_b);
+    EXPECT_EQ(vec_b[0], &c2a);
+    EXPECT_EQ(vec_b[1], &c2b);
+    EXPECT_TRUE(AllInternallyHeld(vec_b));  // Copied, not moved
+}
+
+TEST(TintVectorConstRefTest, CopyVector) {
     Vector<std::string, 1> vec_a{"one", "two"};
-    ConstVectorRef<std::string> vec_ref_a(std::move(vec_a));  // Move
-    ConstVectorRef<std::string> vec_ref_b(std::move(vec_ref_a));
-    Vector<std::string, 2> vec_b(std::move(vec_ref_b));
+    ConstVectorRef<std::string> vec_ref(vec_a);
+    Vector<std::string, 2> vec_b(vec_ref);
     EXPECT_EQ(vec_b[0], "one");
     EXPECT_EQ(vec_b[1], "two");
-    EXPECT_TRUE(AllInternallyHeld(vec_b));  // Copy, no move
+    EXPECT_TRUE(AllInternallyHeld(vec_b));  // Copied, not moved
+}
+
+TEST(TintVectorConstRefTest, CopyVector_Upcast) {
+    C2a c2a;
+    C2b c2b;
+    Vector<C1*, 1> vec_a{&c2a, &c2b};
+    ConstVectorRef<C0*> vec_ref(vec_a);
+    EXPECT_EQ(vec_ref[0], &c2a);
+    EXPECT_EQ(vec_ref[1], &c2b);
+    Vector<C0*, 2> vec_b(vec_ref);
+    EXPECT_EQ(vec_b[0], &c2a);
+    EXPECT_EQ(vec_b[1], &c2b);
+    EXPECT_TRUE(AllInternallyHeld(vec_b));  // Copied, not moved
+}
+
+TEST(TintVectorConstRefTest, CopyVector_AddConst) {
+    C2a c2a;
+    C2b c2b;
+    Vector<C1*, 1> vec_a{&c2a, &c2b};
+    ConstVectorRef<const C1*> vec_ref(vec_a);
+    EXPECT_EQ(vec_ref[0], &c2a);
+    EXPECT_EQ(vec_ref[1], &c2b);
+    Vector<const C1*, 2> vec_b(vec_ref);
+    EXPECT_EQ(vec_b[0], &c2a);
+    EXPECT_EQ(vec_b[1], &c2b);
+    EXPECT_TRUE(AllInternallyHeld(vec_b));  // Copied, not moved
+}
+
+TEST(TintVectorConstRefTest, CopyVectorRef_Upcast) {
+    C2a c2a;
+    C2b c2b;
+    Vector<C1*, 1> vec_a{&c2a, &c2b};
+    VectorRef<C1*> vec_ref_a(vec_a);
+    ConstVectorRef<C0*> vec_ref_b(vec_ref_a);
+    EXPECT_EQ(vec_ref_b[0], &c2a);
+    EXPECT_EQ(vec_ref_b[1], &c2b);
+    Vector<C0*, 2> vec_b(vec_ref_b);
+    EXPECT_EQ(vec_b[0], &c2a);
+    EXPECT_EQ(vec_b[1], &c2b);
+    EXPECT_TRUE(AllInternallyHeld(vec_b));  // Copied, not moved
+}
+
+TEST(TintVectorConstRefTest, CopyVectorRef_AddConst) {
+    C2a c2a;
+    C2b c2b;
+    Vector<C1*, 1> vec_a{&c2a, &c2b};
+    VectorRef<C1*> vec_ref_a(vec_a);
+    ConstVectorRef<const C1*> vec_ref_b(vec_ref_a);
+    EXPECT_EQ(vec_ref_b[0], &c2a);
+    EXPECT_EQ(vec_ref_b[1], &c2b);
+    Vector<const C1*, 2> vec_b(vec_ref_b);
+    EXPECT_EQ(vec_b[0], &c2a);
+    EXPECT_EQ(vec_b[1], &c2b);
+    EXPECT_TRUE(AllInternallyHeld(vec_b));  // Copied, not moved
+}
+
+TEST(TintVectorConstRefTest, CopyVectorRef_UpcastAndAddConst) {
+    C2a c2a;
+    C2b c2b;
+    Vector<C1*, 1> vec_a{&c2a, &c2b};
+    VectorRef<C1*> vec_ref_a(vec_a);
+    ConstVectorRef<const C0*> vec_ref_b(vec_ref_a);
+    EXPECT_EQ(vec_ref_b[0], &c2a);
+    EXPECT_EQ(vec_ref_b[1], &c2b);
+    Vector<const C0*, 2> vec_b(vec_ref_b);
+    EXPECT_EQ(vec_b[0], &c2a);
+    EXPECT_EQ(vec_b[1], &c2b);
+    EXPECT_TRUE(AllInternallyHeld(vec_b));  // Copied, not moved
 }
 
 TEST(TintVectorConstRefTest, Index) {
@@ -1357,3 +1957,8 @@
 
 }  // namespace
 }  // namespace tint::utils
+
+TINT_INSTANTIATE_TYPEINFO(tint::utils::C0);
+TINT_INSTANTIATE_TYPEINFO(tint::utils::C1);
+TINT_INSTANTIATE_TYPEINFO(tint::utils::C2a);
+TINT_INSTANTIATE_TYPEINFO(tint::utils::C2b);
diff --git a/src/tint/writer/append_vector.cc b/src/tint/writer/append_vector.cc
index 28845d9..d6eb017 100644
--- a/src/tint/writer/append_vector.cc
+++ b/src/tint/writer/append_vector.cc
@@ -112,23 +112,23 @@
     // to convert a vector of a different type, e.g. vec2<i32>(vec2<u32>()).
     // In that case, preserve the original argument, or you'll get a type error.
 
-    std::vector<const sem::Expression*> packed;
+    utils::Vector<const sem::Expression*, 4> packed;
     if (auto vc = AsVectorConstructor(vector_sem)) {
-        const auto num_supplied = vc.call->Arguments().size();
+        const auto num_supplied = vc.call->Arguments().Length();
         if (num_supplied == 0) {
             // Zero-value vector constructor. Populate with zeros
             for (uint32_t i = 0; i < packed_size - 1; i++) {
                 auto* zero = Zero(*b, packed_el_sem_ty, statement);
-                packed.emplace_back(zero);
+                packed.Push(zero);
             }
         } else if (num_supplied + 1 == packed_size) {
             // All vector components were supplied as scalars.  Pass them through.
             packed = vc.call->Arguments();
         }
     }
-    if (packed.empty()) {
+    if (packed.IsEmpty()) {
         // The special cases didn't occur. Use the vector argument as-is.
-        packed.emplace_back(vector_sem);
+        packed.Push(vector_sem);
     }
 
     if (packed_el_sem_ty != scalar_sem->Type()->UnwrapRef()) {
@@ -141,17 +141,18 @@
             sem::EvaluationStage::kRuntime);
         auto* scalar_cast_sem = b->create<sem::Call>(
             scalar_cast_ast, scalar_cast_target, sem::EvaluationStage::kRuntime,
-            std::vector<const sem::Expression*>{scalar_sem}, statement,
+            utils::Vector<const sem::Expression*, 1>{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);
+        packed.Push(scalar_cast_sem);
     } else {
-        packed.emplace_back(scalar_sem);
+        packed.Push(scalar_sem);
     }
 
     auto* constructor_ast = b->Construct(
-        packed_ast_ty,
-        utils::Transform(packed, [&](const sem::Expression* expr) { return expr->Declaration(); }));
+        packed_ast_ty, utils::ToStdVector(utils::Transform(packed, [&](const sem::Expression* expr) {
+            return expr->Declaration();
+        })));
     auto* constructor_target = b->create<sem::TypeConstructor>(
         packed_sem_ty,
         utils::Transform(packed,
@@ -163,7 +164,8 @@
         sem::EvaluationStage::kRuntime);
     auto* constructor_sem =
         b->create<sem::Call>(constructor_ast, constructor_target, sem::EvaluationStage::kRuntime,
-                             packed, statement, /* constant_value */ nullptr,
+                             std::move(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/append_vector_test.cc b/src/tint/writer/append_vector_test.cc
index 2231003..b4ab0c7 100644
--- a/src/tint/writer/append_vector_test.cc
+++ b/src/tint/writer/append_vector_test.cc
@@ -48,7 +48,7 @@
 
     auto* call = Sem().Get<sem::Call>(vec_123);
     ASSERT_NE(call, nullptr);
-    ASSERT_EQ(call->Arguments().size(), 3u);
+    ASSERT_EQ(call->Arguments().Length(), 3u);
     EXPECT_EQ(call->Arguments()[0], Sem().Get(scalar_1));
     EXPECT_EQ(call->Arguments()[1], Sem().Get(scalar_2));
     EXPECT_EQ(call->Arguments()[2], Sem().Get(scalar_3));
@@ -60,7 +60,7 @@
     EXPECT_TRUE(ctor->ReturnType()->As<sem::Vector>()->type()->Is<sem::I32>());
     EXPECT_EQ(ctor->ReturnType(), call->Type());
 
-    ASSERT_EQ(ctor->Parameters().size(), 3u);
+    ASSERT_EQ(ctor->Parameters().Length(), 3u);
     EXPECT_TRUE(ctor->Parameters()[0]->Type()->Is<sem::I32>());
     EXPECT_TRUE(ctor->Parameters()[1]->Type()->Is<sem::I32>());
     EXPECT_TRUE(ctor->Parameters()[2]->Type()->Is<sem::I32>());
@@ -92,7 +92,7 @@
 
     auto* call = Sem().Get<sem::Call>(vec_123);
     ASSERT_NE(call, nullptr);
-    ASSERT_EQ(call->Arguments().size(), 3u);
+    ASSERT_EQ(call->Arguments().Length(), 3u);
     EXPECT_EQ(call->Arguments()[0], Sem().Get(scalar_1));
     EXPECT_EQ(call->Arguments()[1], Sem().Get(scalar_2));
     EXPECT_EQ(call->Arguments()[2], Sem().Get(u32_to_i32));
@@ -104,7 +104,7 @@
     EXPECT_TRUE(ctor->ReturnType()->As<sem::Vector>()->type()->Is<sem::I32>());
     EXPECT_EQ(ctor->ReturnType(), call->Type());
 
-    ASSERT_EQ(ctor->Parameters().size(), 3u);
+    ASSERT_EQ(ctor->Parameters().Length(), 3u);
     EXPECT_TRUE(ctor->Parameters()[0]->Type()->Is<sem::I32>());
     EXPECT_TRUE(ctor->Parameters()[1]->Type()->Is<sem::I32>());
     EXPECT_TRUE(ctor->Parameters()[2]->Type()->Is<sem::I32>());
@@ -144,7 +144,7 @@
 
     auto* call = Sem().Get<sem::Call>(vec_123);
     ASSERT_NE(call, nullptr);
-    ASSERT_EQ(call->Arguments().size(), 2u);
+    ASSERT_EQ(call->Arguments().Length(), 2u);
     EXPECT_EQ(call->Arguments()[0], Sem().Get(vec_12));
     EXPECT_EQ(call->Arguments()[1], Sem().Get(u32_to_i32));
 
@@ -155,7 +155,7 @@
     EXPECT_TRUE(ctor->ReturnType()->As<sem::Vector>()->type()->Is<sem::I32>());
     EXPECT_EQ(ctor->ReturnType(), call->Type());
 
-    ASSERT_EQ(ctor->Parameters().size(), 2u);
+    ASSERT_EQ(ctor->Parameters().Length(), 2u);
     EXPECT_TRUE(ctor->Parameters()[0]->Type()->Is<sem::Vector>());
     EXPECT_TRUE(ctor->Parameters()[1]->Type()->Is<sem::I32>());
 }
@@ -186,7 +186,7 @@
 
     auto* call = Sem().Get<sem::Call>(vec_123);
     ASSERT_NE(call, nullptr);
-    ASSERT_EQ(call->Arguments().size(), 3u);
+    ASSERT_EQ(call->Arguments().Length(), 3u);
     EXPECT_EQ(call->Arguments()[0], Sem().Get(scalar_1));
     EXPECT_EQ(call->Arguments()[1], Sem().Get(scalar_2));
     EXPECT_EQ(call->Arguments()[2], Sem().Get(f32_to_i32));
@@ -198,7 +198,7 @@
     EXPECT_TRUE(ctor->ReturnType()->As<sem::Vector>()->type()->Is<sem::I32>());
     EXPECT_EQ(ctor->ReturnType(), call->Type());
 
-    ASSERT_EQ(ctor->Parameters().size(), 3u);
+    ASSERT_EQ(ctor->Parameters().Length(), 3u);
     EXPECT_TRUE(ctor->Parameters()[0]->Type()->Is<sem::I32>());
     EXPECT_TRUE(ctor->Parameters()[1]->Type()->Is<sem::I32>());
     EXPECT_TRUE(ctor->Parameters()[2]->Type()->Is<sem::I32>());
@@ -228,7 +228,7 @@
 
     auto* call = Sem().Get<sem::Call>(vec_1234);
     ASSERT_NE(call, nullptr);
-    ASSERT_EQ(call->Arguments().size(), 4u);
+    ASSERT_EQ(call->Arguments().Length(), 4u);
     EXPECT_EQ(call->Arguments()[0], Sem().Get(scalar_1));
     EXPECT_EQ(call->Arguments()[1], Sem().Get(scalar_2));
     EXPECT_EQ(call->Arguments()[2], Sem().Get(scalar_3));
@@ -241,7 +241,7 @@
     EXPECT_TRUE(ctor->ReturnType()->As<sem::Vector>()->type()->Is<sem::I32>());
     EXPECT_EQ(ctor->ReturnType(), call->Type());
 
-    ASSERT_EQ(ctor->Parameters().size(), 4u);
+    ASSERT_EQ(ctor->Parameters().Length(), 4u);
     EXPECT_TRUE(ctor->Parameters()[0]->Type()->Is<sem::I32>());
     EXPECT_TRUE(ctor->Parameters()[1]->Type()->Is<sem::I32>());
     EXPECT_TRUE(ctor->Parameters()[2]->Type()->Is<sem::I32>());
@@ -268,7 +268,7 @@
 
     auto* call = Sem().Get<sem::Call>(vec_123);
     ASSERT_NE(call, nullptr);
-    ASSERT_EQ(call->Arguments().size(), 2u);
+    ASSERT_EQ(call->Arguments().Length(), 2u);
     EXPECT_EQ(call->Arguments()[0], Sem().Get(vec_12));
     EXPECT_EQ(call->Arguments()[1], Sem().Get(scalar_3));
 
@@ -279,7 +279,7 @@
     EXPECT_TRUE(ctor->ReturnType()->As<sem::Vector>()->type()->Is<sem::I32>());
     EXPECT_EQ(ctor->ReturnType(), call->Type());
 
-    ASSERT_EQ(ctor->Parameters().size(), 2u);
+    ASSERT_EQ(ctor->Parameters().Length(), 2u);
     EXPECT_TRUE(ctor->Parameters()[0]->Type()->Is<sem::Vector>());
     EXPECT_TRUE(ctor->Parameters()[1]->Type()->Is<sem::I32>());
 }
@@ -307,7 +307,7 @@
 
     auto* call = Sem().Get<sem::Call>(vec_123);
     ASSERT_NE(call, nullptr);
-    ASSERT_EQ(call->Arguments().size(), 3u);
+    ASSERT_EQ(call->Arguments().Length(), 3u);
     EXPECT_EQ(call->Arguments()[0], Sem().Get(scalar_1));
     EXPECT_EQ(call->Arguments()[1], Sem().Get(scalar_2));
     EXPECT_EQ(call->Arguments()[2], Sem().Get(scalar_3));
@@ -319,7 +319,7 @@
     EXPECT_TRUE(ctor->ReturnType()->As<sem::Vector>()->type()->Is<sem::I32>());
     EXPECT_EQ(ctor->ReturnType(), call->Type());
 
-    ASSERT_EQ(ctor->Parameters().size(), 3u);
+    ASSERT_EQ(ctor->Parameters().Length(), 3u);
     EXPECT_TRUE(ctor->Parameters()[0]->Type()->Is<sem::I32>());
     EXPECT_TRUE(ctor->Parameters()[1]->Type()->Is<sem::I32>());
     EXPECT_TRUE(ctor->Parameters()[2]->Type()->Is<sem::I32>());
@@ -346,7 +346,7 @@
 
     auto* call = Sem().Get<sem::Call>(vec_123);
     ASSERT_NE(call, nullptr);
-    ASSERT_EQ(call->Arguments().size(), 2u);
+    ASSERT_EQ(call->Arguments().Length(), 2u);
     EXPECT_EQ(call->Arguments()[0], Sem().Get(vec_12));
     EXPECT_EQ(call->Arguments()[1], Sem().Get(scalar_3));
 
@@ -357,7 +357,7 @@
     EXPECT_TRUE(ctor->ReturnType()->As<sem::Vector>()->type()->Is<sem::I32>());
     EXPECT_EQ(ctor->ReturnType(), call->Type());
 
-    ASSERT_EQ(ctor->Parameters().size(), 2u);
+    ASSERT_EQ(ctor->Parameters().Length(), 2u);
     EXPECT_TRUE(ctor->Parameters()[0]->Type()->Is<sem::Vector>());
     EXPECT_TRUE(ctor->Parameters()[1]->Type()->Is<sem::I32>());
 }
@@ -387,7 +387,7 @@
 
     auto* call = Sem().Get<sem::Call>(vec_123);
     ASSERT_NE(call, nullptr);
-    ASSERT_EQ(call->Arguments().size(), 2u);
+    ASSERT_EQ(call->Arguments().Length(), 2u);
     EXPECT_EQ(call->Arguments()[0], Sem().Get(vec_12));
     EXPECT_EQ(call->Arguments()[1], Sem().Get(f32_to_i32));
 
@@ -398,7 +398,7 @@
     EXPECT_TRUE(ctor->ReturnType()->As<sem::Vector>()->type()->Is<sem::I32>());
     EXPECT_EQ(ctor->ReturnType(), call->Type());
 
-    ASSERT_EQ(ctor->Parameters().size(), 2u);
+    ASSERT_EQ(ctor->Parameters().Length(), 2u);
     EXPECT_TRUE(ctor->Parameters()[0]->Type()->Is<sem::Vector>());
     EXPECT_TRUE(ctor->Parameters()[1]->Type()->Is<sem::I32>());
 }
@@ -424,7 +424,7 @@
 
     auto* call = Sem().Get<sem::Call>(vec_123);
     ASSERT_NE(call, nullptr);
-    ASSERT_EQ(call->Arguments().size(), 2u);
+    ASSERT_EQ(call->Arguments().Length(), 2u);
     EXPECT_EQ(call->Arguments()[0], Sem().Get(vec_12));
     EXPECT_EQ(call->Arguments()[1], Sem().Get(scalar_3));
 
@@ -435,7 +435,7 @@
     EXPECT_TRUE(ctor->ReturnType()->As<sem::Vector>()->type()->Is<sem::Bool>());
     EXPECT_EQ(ctor->ReturnType(), call->Type());
 
-    ASSERT_EQ(ctor->Parameters().size(), 2u);
+    ASSERT_EQ(ctor->Parameters().Length(), 2u);
     EXPECT_TRUE(ctor->Parameters()[0]->Type()->Is<sem::Vector>());
     EXPECT_TRUE(ctor->Parameters()[1]->Type()->Is<sem::Bool>());
 }
@@ -463,7 +463,7 @@
 
     auto* call = Sem().Get<sem::Call>(vec_0004);
     ASSERT_NE(call, nullptr);
-    ASSERT_EQ(call->Arguments().size(), 4u);
+    ASSERT_EQ(call->Arguments().Length(), 4u);
     EXPECT_EQ(call->Arguments()[0], Sem().Get(vec_0004->args[0]));
     EXPECT_EQ(call->Arguments()[1], Sem().Get(vec_0004->args[1]));
     EXPECT_EQ(call->Arguments()[2], Sem().Get(vec_0004->args[2]));
@@ -476,7 +476,7 @@
     EXPECT_TRUE(ctor->ReturnType()->As<sem::Vector>()->type()->Is<sem::I32>());
     EXPECT_EQ(ctor->ReturnType(), call->Type());
 
-    ASSERT_EQ(ctor->Parameters().size(), 4u);
+    ASSERT_EQ(ctor->Parameters().Length(), 4u);
     EXPECT_TRUE(ctor->Parameters()[0]->Type()->Is<sem::I32>());
     EXPECT_TRUE(ctor->Parameters()[1]->Type()->Is<sem::I32>());
     EXPECT_TRUE(ctor->Parameters()[2]->Type()->Is<sem::I32>());
diff --git a/src/tint/writer/glsl/generator_impl.cc b/src/tint/writer/glsl/generator_impl.cc
index e9c0c4a..9abe6ce 100644
--- a/src/tint/writer/glsl/generator_impl.cc
+++ b/src/tint/writer/glsl/generator_impl.cc
@@ -861,7 +861,7 @@
 
     // If the type constructor is empty then we need to construct with the zero
     // value for all components.
-    if (call->Arguments().empty()) {
+    if (call->Arguments().IsEmpty()) {
         return EmitZeroValue(out, type);
     }
 
diff --git a/src/tint/writer/hlsl/generator_impl.cc b/src/tint/writer/hlsl/generator_impl.cc
index 4516a98..a9fddc3 100644
--- a/src/tint/writer/hlsl/generator_impl.cc
+++ b/src/tint/writer/hlsl/generator_impl.cc
@@ -1120,13 +1120,13 @@
 
     // If the type constructor is empty then we need to construct with the zero
     // value for all components.
-    if (call->Arguments().empty()) {
+    if (call->Arguments().IsEmpty()) {
         return EmitZeroValue(out, type);
     }
 
     // Single parameter matrix initializers must be identity constructor.
     // It could also be conversions between f16 and f32 matrix when f16 is properly supported.
-    if (type->Is<sem::Matrix>() && call->Arguments().size() == 1) {
+    if (type->Is<sem::Matrix>() && call->Arguments().Length() == 1) {
         if (!ctor->Parameters()[0]->Type()->UnwrapRef()->is_float_matrix()) {
             TINT_UNREACHABLE(Writer, diagnostics_)
                 << "found a single-parameter matrix constructor that is not identity constructor";
@@ -1139,7 +1139,7 @@
     // For single-value vector initializers, swizzle the scalar to the right
     // vector dimension using .x
     const bool is_single_value_vector_init = type->is_scalar_vector() &&
-                                             call->Arguments().size() == 1 &&
+                                             call->Arguments().Length() == 1 &&
                                              ctor->Parameters()[0]->Type()->is_scalar();
 
     if (brackets) {
diff --git a/src/tint/writer/msl/generator_impl.cc b/src/tint/writer/msl/generator_impl.cc
index 68151e1..626a11d 100644
--- a/src/tint/writer/msl/generator_impl.cc
+++ b/src/tint/writer/msl/generator_impl.cc
@@ -2304,7 +2304,7 @@
         // For multi-element swizzles, we need to cast to a regular vector type
         // first. Note that we do not currently allow assignments to swizzles, so
         // the casting which will convert the l-value to r-value is fine.
-        if (swizzle->Indices().size() == 1) {
+        if (swizzle->Indices().Length() == 1) {
             if (!write_lhs()) {
                 return false;
             }
diff --git a/src/tint/writer/spirv/builder.cc b/src/tint/writer/spirv/builder.cc
index 31ed539..934520a 100644
--- a/src/tint/writer/spirv/builder.cc
+++ b/src/tint/writer/spirv/builder.cc
@@ -1023,7 +1023,7 @@
     if (auto* swizzle = expr_sem->As<sem::Swizzle>()) {
         // Single element swizzle is either an access chain or a composite extract
         auto& indices = swizzle->Indices();
-        if (indices.size() == 1) {
+        if (indices.Length() == 1) {
             if (info->source_type->Is<sem::Reference>()) {
                 auto idx_id = GenerateConstantIfNeeded(ScalarConstant::U32(indices[0]));
                 if (idx_id == 0) {
@@ -1338,7 +1338,7 @@
     auto* result_type = call->Type();
 
     // Generate the zero initializer if there are no values provided.
-    if (args.empty()) {
+    if (args.IsEmpty()) {
         if (global_var && global_var->Declaration()->Is<ast::Override>()) {
             auto constant_id = global_var->ConstantId();
             if (result_type->Is<sem::I32>()) {
@@ -1491,7 +1491,7 @@
 
     // For a single-value vector initializer, splat the initializer value.
     auto* const init_result_type = call->Type()->UnwrapRef();
-    if (args.size() == 1 && init_result_type->is_scalar_vector() &&
+    if (args.Length() == 1 && init_result_type->is_scalar_vector() &&
         args[0]->Type()->UnwrapRef()->is_scalar()) {
         size_t vec_size = init_result_type->As<sem::Vector>()->Width();
         for (size_t i = 0; i < (vec_size - 1); ++i) {
@@ -2667,7 +2667,7 @@
         return 0;
     }
 
-    for (size_t i = 0; i < call->Arguments().size(); i++) {
+    for (size_t i = 0; i < call->Arguments().Length(); i++) {
         if (auto val_id = get_arg_as_value_id(i)) {
             params.emplace_back(Operand(val_id));
         } else {
@@ -3172,8 +3172,8 @@
     }
 
     uint32_t value_id = 0;
-    if (call->Arguments().size() > 1) {
-        value_id = GenerateExpressionWithLoadIfNeeded(call->Arguments().back());
+    if (call->Arguments().Length() > 1) {
+        value_id = GenerateExpressionWithLoadIfNeeded(call->Arguments().Back());
         if (value_id == 0) {
             return false;
         }