Import Tint changes from Dawn

Changes:
  - 64b09959f7ae6c78ef8f3d7d55b5b746ffb22af9 Update `type_decl` and `primary_expression` by dan sinclair <dsinclair@chromium.org>
  - 08d735e6a91d327e1ddddb3d1b955422ad09d885 Add missing relational_expression.post.unary_expression c... by dan sinclair <dsinclair@chromium.org>
  - a838e34954c32b115ba3de177672a35193152c4d Handle peeking past placeholder elements. by dan sinclair <dsinclair@chromium.org>
  - 393de082d3a3e8c64127ab168735efe61632e601 Add `mat_prefix`, `vec_prefix`, `callable` and `type_decl... by dan sinclair <dsinclair@chromium.org>
  - 9284f8ab55f64f8e035ece0ffc54d093bddb6d2d Add test for let with complex constructor by dan sinclair <dsinclair@chromium.org>
  - 96a7d5a6bd2506112a12e2dc4e435e19631a2be0 Add some parenthesis. by dan sinclair <dsinclair@chromium.org>
  - e76521153f3f0f4341ee89323565bb978b2062dd Fixup shift_expression parsing. by dan sinclair <dsinclair@chromium.org>
  - ee25586aec81e62f22a594fc3ab2669ecfd92c2b Sync `expression` grammar element with WGSL spec. by dan sinclair <dsinclair@chromium.org>
GitOrigin-RevId: 64b09959f7ae6c78ef8f3d7d55b5b746ffb22af9
Change-Id: Ia8a35c3c61b935bf0b814090f95a92b55303f31c
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/100121
Reviewed-by: Ben Clayton <bclayton@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/src/tint/BUILD.gn b/src/tint/BUILD.gn
index 9b2fd35..8283b45 100644
--- a/src/tint/BUILD.gn
+++ b/src/tint/BUILD.gn
@@ -1357,6 +1357,7 @@
       "reader/wgsl/parser_impl_break_stmt_test.cc",
       "reader/wgsl/parser_impl_bug_cases_test.cc",
       "reader/wgsl/parser_impl_call_stmt_test.cc",
+      "reader/wgsl/parser_impl_callable_test.cc",
       "reader/wgsl/parser_impl_case_body_test.cc",
       "reader/wgsl/parser_impl_compound_stmt_test.cc",
       "reader/wgsl/parser_impl_const_literal_test.cc",
@@ -1370,6 +1371,7 @@
       "reader/wgsl/parser_impl_error_msg_test.cc",
       "reader/wgsl/parser_impl_error_resync_test.cc",
       "reader/wgsl/parser_impl_exclusive_or_expression_test.cc",
+      "reader/wgsl/parser_impl_expression_test.cc",
       "reader/wgsl/parser_impl_external_texture_test.cc",
       "reader/wgsl/parser_impl_for_stmt_test.cc",
       "reader/wgsl/parser_impl_function_attribute_list_test.cc",
@@ -1417,6 +1419,7 @@
       "reader/wgsl/parser_impl_texture_sampler_test.cc",
       "reader/wgsl/parser_impl_type_alias_test.cc",
       "reader/wgsl/parser_impl_type_decl_test.cc",
+      "reader/wgsl/parser_impl_type_decl_without_ident_test.cc",
       "reader/wgsl/parser_impl_unary_expression_test.cc",
       "reader/wgsl/parser_impl_variable_attribute_list_test.cc",
       "reader/wgsl/parser_impl_variable_attribute_test.cc",
@@ -1424,6 +1427,7 @@
       "reader/wgsl/parser_impl_variable_ident_decl_test.cc",
       "reader/wgsl/parser_impl_variable_qualifier_test.cc",
       "reader/wgsl/parser_impl_variable_stmt_test.cc",
+      "reader/wgsl/parser_impl_vec_mat_prefix_test.cc",
       "reader/wgsl/parser_impl_while_stmt_test.cc",
       "reader/wgsl/parser_test.cc",
       "reader/wgsl/token_test.cc",
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index 288735a..6703d65 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -952,6 +952,7 @@
       reader/wgsl/parser_impl_break_stmt_test.cc
       reader/wgsl/parser_impl_bug_cases_test.cc
       reader/wgsl/parser_impl_call_stmt_test.cc
+      reader/wgsl/parser_impl_callable_test.cc
       reader/wgsl/parser_impl_case_body_test.cc
       reader/wgsl/parser_impl_compound_stmt_test.cc
       reader/wgsl/parser_impl_const_literal_test.cc
@@ -961,11 +962,12 @@
       reader/wgsl/parser_impl_depth_texture_test.cc
       reader/wgsl/parser_impl_element_count_expression_test.cc
       reader/wgsl/parser_impl_enable_directive_test.cc
-      reader/wgsl/parser_impl_external_texture_test.cc
       reader/wgsl/parser_impl_equality_expression_test.cc
       reader/wgsl/parser_impl_error_msg_test.cc
       reader/wgsl/parser_impl_error_resync_test.cc
       reader/wgsl/parser_impl_exclusive_or_expression_test.cc
+      reader/wgsl/parser_impl_expression_test.cc
+      reader/wgsl/parser_impl_external_texture_test.cc
       reader/wgsl/parser_impl_for_stmt_test.cc
       reader/wgsl/parser_impl_function_decl_test.cc
       reader/wgsl/parser_impl_function_attribute_list_test.cc
@@ -1012,6 +1014,7 @@
       reader/wgsl/parser_impl_texture_sampler_test.cc
       reader/wgsl/parser_impl_type_alias_test.cc
       reader/wgsl/parser_impl_type_decl_test.cc
+      reader/wgsl/parser_impl_type_decl_without_ident_test.cc
       reader/wgsl/parser_impl_unary_expression_test.cc
       reader/wgsl/parser_impl_variable_decl_test.cc
       reader/wgsl/parser_impl_variable_attribute_list_test.cc
@@ -1019,6 +1022,7 @@
       reader/wgsl/parser_impl_variable_ident_decl_test.cc
       reader/wgsl/parser_impl_variable_stmt_test.cc
       reader/wgsl/parser_impl_variable_qualifier_test.cc
+      reader/wgsl/parser_impl_vec_mat_prefix_test.cc
       reader/wgsl/parser_impl_while_stmt_test.cc
       reader/wgsl/token_test.cc
     )
diff --git a/src/tint/reader/wgsl/parser_impl.cc b/src/tint/reader/wgsl/parser_impl.cc
index cf684d3..9335356 100644
--- a/src/tint/reader/wgsl/parser_impl.cc
+++ b/src/tint/reader/wgsl/parser_impl.cc
@@ -271,23 +271,18 @@
     return tokens_[last_source_idx_];
 }
 
-const Token& ParserImpl::peek(size_t idx) {
-    if (next_token_idx_ + idx >= tokens_.size()) {
-        return tokens_[tokens_.size() - 1];
-    }
-
-    // Skip over any placeholder elements
-    while (true) {
-        if (!tokens_[next_token_idx_ + idx].IsPlaceholder()) {
-            break;
+const Token& ParserImpl::peek(size_t count) {
+    for (size_t idx = next_token_idx_; idx < tokens_.size(); idx++) {
+        if (tokens_[idx].IsPlaceholder()) {
+            continue;
         }
-        idx++;
+        if (count == 0) {
+            return tokens_[idx];
+        }
+        count--;
     }
-    if (next_token_idx_ + idx >= tokens_.size()) {
-        return tokens_[tokens_.size() - 1];
-    }
-
-    return tokens_[next_token_idx_ + idx];
+    // Walked off the end of the token list, return last token.
+    return tokens_[tokens_.size() - 1];
 }
 
 bool ParserImpl::peek_is(Token::Type tok, size_t idx) {
@@ -679,6 +674,9 @@
 
 // variable_decl
 //   : VAR variable_qualifier? optionally_typed_ident
+//
+// Note, the `( LESS_THAN address_space ( COMMA access_mode )? GREATER_THAN ) is pulled out into
+// a `variable_qualifier` helper.
 Maybe<ParserImpl::VarDeclInfo> ParserImpl::variable_decl() {
     Source source;
     if (!match(Token::Type::kVar, &source)) {
@@ -995,7 +993,7 @@
 }
 
 // variable_qualifier
-//   : LESS_THAN storage_class (COMMA access_mode)? GREATER_THAN
+//   : LESS_THAN address_spaces (COMMA access_mode)? GREATER_THAN
 Maybe<ParserImpl::VariableQualifier> ParserImpl::variable_qualifier() {
     if (!peek_is(Token::Type::kLessThan)) {
         return Failure::kNoMatch;
@@ -1056,75 +1054,119 @@
     return builder_.ty.alias(make_source_range_from(t.source()), name.value, type.value);
 }
 
-// type_decl
-//   : IDENTIFIER
-//   | BOOL
-//   | FLOAT32
-//   | INT32
-//   | UINT32
-//   | VEC2 LESS_THAN type_decl GREATER_THAN
-//   | VEC3 LESS_THAN type_decl GREATER_THAN
-//   | VEC4 LESS_THAN type_decl GREATER_THAN
-//   | PTR LESS_THAN storage_class, type_decl (COMMA access_mode)? GREATER_THAN
-//   | array_attribute_list* ARRAY LESS_THAN type_decl COMMA element_count_expression GREATER_THAN
-//   | array_attribute_list* ARRAY LESS_THAN type_decl GREATER_THAN
-//   | MAT2x2 LESS_THAN type_decl GREATER_THAN
-//   | MAT2x3 LESS_THAN type_decl GREATER_THAN
-//   | MAT2x4 LESS_THAN type_decl GREATER_THAN
-//   | MAT3x2 LESS_THAN type_decl GREATER_THAN
-//   | MAT3x3 LESS_THAN type_decl GREATER_THAN
-//   | MAT3x4 LESS_THAN type_decl GREATER_THAN
-//   | MAT4x2 LESS_THAN type_decl GREATER_THAN
-//   | MAT4x3 LESS_THAN type_decl GREATER_THAN
-//   | MAT4x4 LESS_THAN type_decl GREATER_THAN
-//   | texture_and_sampler_types
-Maybe<const ast::Type*> ParserImpl::type_decl() {
+// vec_prefix
+//   : 'vec2'
+//   | 'vec3'
+//   | 'vec4'
+Maybe<uint32_t> ParserImpl::vec_prefix() {
     auto& t = peek();
-    Source source;
-    if (match(Token::Type::kIdentifier, &source)) {
-        return builder_.create<ast::TypeName>(source, builder_.Symbols().Register(t.to_str()));
+    if (!t.IsVector()) {
+        return Failure::kNoMatch;
+    }
+    next();
+
+    if (t.Is(Token::Type::kVec3)) {
+        return 3u;
+    }
+    if (t.Is(Token::Type::kVec4)) {
+        return 4u;
+    }
+    return 2u;
+}
+
+// mat_prefix
+//   : 'mat2x2'
+//   | 'mat2x3'
+//   | 'mat2x4'
+//   | 'mat3x2'
+//   | 'mat3x3'
+//   | 'mat3x4'
+//   | 'mat4x2'
+//   | 'mat4x3'
+//   | 'mat4x4'
+Maybe<ParserImpl::MatrixDimensions> ParserImpl::mat_prefix() {
+    auto& t = peek();
+    if (!t.IsMatrix()) {
+        return Failure::kNoMatch;
+    }
+    next();
+
+    uint32_t columns = 2;
+    if (t.IsMat3xN()) {
+        columns = 3;
+    } else if (t.IsMat4xN()) {
+        columns = 4;
+    }
+    if (t.IsMatNx3()) {
+        return MatrixDimensions{columns, 3};
+    }
+    if (t.IsMatNx4()) {
+        return MatrixDimensions{columns, 4};
+    }
+    return MatrixDimensions{columns, 2};
+}
+
+// type_decl_without_ident:
+//   : BOOL
+//   | F16
+//   | F32
+//   | I32
+//   | U32
+//   | ARRAY LESS_THAN type_decl ( COMMA element_count_expression )? GREATER_THAN
+//   | ATOMIC LESS_THAN type_decl GREATER_THAN
+//   | PTR LESS_THAN address_space COMMA type_decl ( COMMA access_mode )? GREATER_THAN
+//   | mat_prefix LESS_THAN type_decl GREATER_THAN
+//   | vec_prefix LESS_THAN type_decl GREATER_THAN
+//   | texture_and_sampler_types
+Maybe<const ast::Type*> ParserImpl::type_decl_without_ident() {
+    auto& t = peek();
+
+    if (match(Token::Type::kBool)) {
+        return builder_.ty.bool_(t.source());
     }
 
-    if (match(Token::Type::kBool, &source)) {
-        return builder_.ty.bool_(source);
+    if (match(Token::Type::kF16)) {
+        return builder_.ty.f16(t.source());
     }
 
-    if (match(Token::Type::kF16, &source)) {
-        return builder_.ty.f16(source);
+    if (match(Token::Type::kF32)) {
+        return builder_.ty.f32(t.source());
     }
 
-    if (match(Token::Type::kF32, &source)) {
-        return builder_.ty.f32(source);
+    if (match(Token::Type::kI32)) {
+        return builder_.ty.i32(t.source());
     }
 
-    if (match(Token::Type::kI32, &source)) {
-        return builder_.ty.i32(source);
+    if (match(Token::Type::kU32)) {
+        return builder_.ty.u32(t.source());
     }
 
-    if (match(Token::Type::kU32, &source)) {
-        return builder_.ty.u32(source);
-    }
-
-    if (t.IsVector()) {
-        next();  // Consume the peek
-        return expect_type_decl_vector(t);
-    }
-
-    if (match(Token::Type::kPtr)) {
-        return expect_type_decl_pointer(t);
+    if (t.Is(Token::Type::kArray) && peek_is(Token::Type::kLessThan, 1)) {
+        if (match(Token::Type::kArray)) {
+            return expect_type_decl_array(t.source());
+        }
     }
 
     if (match(Token::Type::kAtomic)) {
-        return expect_type_decl_atomic(t);
+        return expect_type_decl_atomic(t.source());
     }
 
-    if (match(Token::Type::kArray, &source)) {
-        return expect_type_decl_array(t);
+    if (match(Token::Type::kPtr)) {
+        return expect_type_decl_pointer(t.source());
     }
 
-    if (t.IsMatrix()) {
-        next();  // Consume the peek
-        return expect_type_decl_matrix(t);
+    if (t.IsMatrix() && peek_is(Token::Type::kLessThan, 1)) {
+        auto mat = mat_prefix();
+        if (mat.matched) {
+            return expect_type_decl_matrix(t.source(), mat.value);
+        }
+    }
+
+    if (t.IsVector() && peek_is(Token::Type::kLessThan, 1)) {
+        auto vec = vec_prefix();
+        if (vec.matched) {
+            return expect_type_decl_vector(t.source(), vec.value);
+        }
     }
 
     auto texture_or_sampler = texture_and_sampler_types();
@@ -1138,6 +1180,19 @@
     return Failure::kNoMatch;
 }
 
+// type_decl
+//   : IDENTIFIER
+//   | type_decl_without_ident
+Maybe<const ast::Type*> ParserImpl::type_decl() {
+    auto& t = peek();
+    Source source;
+    if (match(Token::Type::kIdentifier, &source)) {
+        return builder_.create<ast::TypeName>(source, builder_.Symbols().Register(t.to_str()));
+    }
+
+    return type_decl_without_ident();
+}
+
 Expect<const ast::Type*> ParserImpl::expect_type(std::string_view use) {
     auto type = type_decl();
     if (type.errored) {
@@ -1149,7 +1204,8 @@
     return type.value;
 }
 
-Expect<const ast::Type*> ParserImpl::expect_type_decl_pointer(const Token& t) {
+// LESS_THAN address_space COMMA type_decl ( COMMA access_mode )? GREATER_THAN
+Expect<const ast::Type*> ParserImpl::expect_type_decl_pointer(const Source& s) {
     const char* use = "ptr declaration";
 
     auto storage_class = ast::StorageClass::kNone;
@@ -1186,11 +1242,11 @@
         return Failure::kErrored;
     }
 
-    return builder_.ty.pointer(make_source_range_from(t.source()), subtype.value, storage_class,
-                               access);
+    return builder_.ty.pointer(make_source_range_from(s), subtype.value, storage_class, access);
 }
 
-Expect<const ast::Type*> ParserImpl::expect_type_decl_atomic(const Token& t) {
+// LESS_THAN type_decl GREATER_THAN
+Expect<const ast::Type*> ParserImpl::expect_type_decl_atomic(const Source& s) {
     const char* use = "atomic declaration";
 
     auto subtype = expect_lt_gt_block(use, [&] { return expect_type(use); });
@@ -1198,31 +1254,22 @@
         return Failure::kErrored;
     }
 
-    return builder_.ty.atomic(make_source_range_from(t.source()), subtype.value);
+    return builder_.ty.atomic(make_source_range_from(s), subtype.value);
 }
 
-Expect<const ast::Type*> ParserImpl::expect_type_decl_vector(const Token& t) {
-    uint32_t count = 2;
-    if (t.Is(Token::Type::kVec3)) {
-        count = 3;
-    } else if (t.Is(Token::Type::kVec4)) {
-        count = 4;
+// LESS_THAN type_decl GREATER_THAN
+Expect<const ast::Type*> ParserImpl::expect_type_decl_vector(const Source& s, uint32_t count) {
+    const char* use = "vector";
+    auto ty = expect_lt_gt_block(use, [&] { return expect_type(use); });
+    if (ty.errored) {
+        return Failure::kErrored;
     }
 
-    const ast::Type* subtype = nullptr;
-    if (peek_is(Token::Type::kLessThan)) {
-        const char* use = "vector";
-        auto ty = expect_lt_gt_block(use, [&] { return expect_type(use); });
-        if (ty.errored) {
-            return Failure::kErrored;
-        }
-        subtype = ty.value;
-    }
-
-    return builder_.ty.vec(make_source_range_from(t.source()), subtype, count);
+    return builder_.ty.vec(make_source_range_from(s), ty.value, count);
 }
 
-Expect<const ast::Type*> ParserImpl::expect_type_decl_array(const Token& t) {
+// LESS_THAN type_decl ( COMMA element_count_expression )? GREATER_THAN
+Expect<const ast::Type*> ParserImpl::expect_type_decl_array(const Source& s) {
     const char* use = "array declaration";
 
     struct TypeAndSize {
@@ -1231,7 +1278,7 @@
     };
 
     if (!peek_is(Token::Type::kLessThan)) {
-        return builder_.ty.array(make_source_range_from(t.source()), nullptr, nullptr);
+        return add_error(peek(), "expected < for array");
     }
 
     auto type_size = expect_lt_gt_block(use, [&]() -> Expect<TypeAndSize> {
@@ -1259,34 +1306,19 @@
         return Failure::kErrored;
     }
 
-    return builder_.ty.array(make_source_range_from(t.source()), type_size->type, type_size->size);
+    return builder_.ty.array(make_source_range_from(s), type_size->type, type_size->size);
 }
 
-Expect<const ast::Type*> ParserImpl::expect_type_decl_matrix(const Token& t) {
-    uint32_t rows = 2;
-    uint32_t columns = 2;
-    if (t.IsMat3xN()) {
-        columns = 3;
-    } else if (t.IsMat4xN()) {
-        columns = 4;
-    }
-    if (t.IsMatNx3()) {
-        rows = 3;
-    } else if (t.IsMatNx4()) {
-        rows = 4;
+// LESS_THAN type_decl GREATER_THAN
+Expect<const ast::Type*> ParserImpl::expect_type_decl_matrix(const Source& s,
+                                                             const MatrixDimensions& dims) {
+    const char* use = "matrix";
+    auto ty = expect_lt_gt_block(use, [&] { return expect_type(use); });
+    if (ty.errored) {
+        return Failure::kErrored;
     }
 
-    const ast::Type* subtype = nullptr;
-    if (peek_is(Token::Type::kLessThan)) {
-        const char* use = "matrix";
-        auto ty = expect_lt_gt_block(use, [&] { return expect_type(use); });
-        if (ty.errored) {
-            return Failure::kErrored;
-        }
-        subtype = ty.value;
-    }
-
-    return builder_.ty.mat(make_source_range_from(t.source()), subtype, columns, rows);
+    return builder_.ty.mat(make_source_range_from(s), ty.value, dims.columns, dims.rows);
 }
 
 // address_space
@@ -1888,7 +1920,7 @@
             return Failure::kErrored;
         }
 
-        auto initializer = expression();
+        auto initializer = maybe_expression();
         if (initializer.errored) {
             return Failure::kErrored;
         }
@@ -1915,7 +1947,7 @@
             return Failure::kErrored;
         }
 
-        auto initializer = expression();
+        auto initializer = maybe_expression();
         if (initializer.errored) {
             return Failure::kErrored;
         }
@@ -1942,7 +1974,7 @@
 
     const ast::Expression* initializer = nullptr;
     if (match(Token::Type::kEqual)) {
-        auto initializer_expr = expression();
+        auto initializer_expr = maybe_expression();
         if (initializer_expr.errored) {
             return Failure::kErrored;
         }
@@ -2435,32 +2467,62 @@
     return continuing_compound_statement();
 }
 
-// primary_expression
-//   : IDENT argument_expression_list?
-//   | type_decl argument_expression_list
-//   | const_literal
-//   | paren_expression
-//   | BITCAST LESS_THAN type_decl GREATER_THAN paren_expression
-Maybe<const ast::Expression*> ParserImpl::primary_expression() {
+// callable
+//   : type_decl_without_ident
+//   | ARRAY
+//   | mat_prefix
+//   | vec_prefix
+//
+//  Note, `ident` is pulled out to `primary_expression` as it's the only one that
+//  doesn't create a `type`. Then we can just return a `type` from here on match and
+//  deal with `ident` in `primary_expression.
+Maybe<const ast::Type*> ParserImpl::callable() {
     auto& t = peek();
 
-    auto lit = const_literal();
-    if (lit.errored) {
+    //  This _must_ match `type_decl_without_ident` before any of the other types as they're all
+    //  prefixes of the types and we want to match the longer `vec3<f32>` then the shorter
+    //  prefix match of `vec3`.
+    auto ty = type_decl_without_ident();
+    if (ty.errored) {
         return Failure::kErrored;
     }
-    if (lit.matched) {
-        return lit.value;
+    if (ty.matched) {
+        return ty.value;
     }
 
-    if (t.Is(Token::Type::kParenLeft)) {
-        auto paren = expect_paren_expression();
-        if (paren.errored) {
-            return Failure::kErrored;
-        }
-
-        return paren.value;
+    if (match(Token::Type::kArray)) {
+        return builder_.ty.array(make_source_range_from(t.source()), nullptr, nullptr);
     }
 
+    auto vec = vec_prefix();
+    if (vec.matched) {
+        return builder_.ty.vec(make_source_range_from(t.source()), nullptr, vec.value);
+    }
+
+    auto mat = mat_prefix();
+    if (mat.matched) {
+        return builder_.ty.mat(make_source_range_from(t.source()), nullptr, mat.value.columns,
+                               mat.value.rows);
+    }
+
+    return Failure::kNoMatch;
+}
+
+// primary_expression
+//   : BITCAST LESS_THAN type_decl GREATER_THAN paren_expression
+//   | callable argument_expression_list
+//   | const_literal
+//   | IDENT argument_expression_list?
+//   | paren_expression
+//
+// Note, PAREN_LEFT ( expression ( COMMA expression ) * COMMA? )? PAREN_RIGHT is replaced
+// with `argument_expression_list`.
+//
+// Note, this is matching the `callable` ident here instead of having to come from
+// callable so we can return a `type` from callable.
+Maybe<const ast::Expression*> ParserImpl::primary_expression() {
+    auto& t = peek();
+
     if (match(Token::Type::kBitcast)) {
         const char* use = "bitcast expression";
 
@@ -2477,6 +2539,27 @@
         return create<ast::BitcastExpression>(t.source(), type.value, params.value);
     }
 
+    auto call = callable();
+    if (call.errored) {
+        return Failure::kErrored;
+    }
+    if (call.matched) {
+        auto params = expect_argument_expression_list("type constructor");
+        if (params.errored) {
+            return Failure::kErrored;
+        }
+
+        return builder_.Construct(t.source(), call.value, std::move(params.value));
+    }
+
+    auto lit = const_literal();
+    if (lit.errored) {
+        return Failure::kErrored;
+    }
+    if (lit.matched) {
+        return lit.value;
+    }
+
     if (t.IsIdentifier()) {
         next();
 
@@ -2495,17 +2578,13 @@
         return ident;
     }
 
-    auto type = type_decl();
-    if (type.errored) {
-        return Failure::kErrored;
-    }
-    if (type.matched) {
-        auto params = expect_argument_expression_list("type constructor");
-        if (params.errored) {
+    if (t.Is(Token::Type::kParenLeft)) {
+        auto paren = expect_paren_expression();
+        if (paren.errored) {
             return Failure::kErrored;
         }
 
-        return builder_.Construct(t.source(), type.value, std::move(params.value));
+        return paren.value;
     }
 
     return Failure::kNoMatch;
@@ -2814,11 +2893,11 @@
 }
 
 // shift_expression.post.unary_expression
-//   : multiplicative_expression.post.unary_expression?
+//   : math_expression.post.unary_expression?
 //   | SHIFT_LEFT unary_expression
 //   | SHIFT_RIGHT unary_expression
 //
-// Note, add the `multiplicative_expression.post.unary_expression` is added here to make
+// Note, add the `math_expression.post.unary_expression` is added here to make
 // implementation simpler.
 Expect<const ast::Expression*> ParserImpl::expect_shift_expression_post_unary_expression(
     const ast::Expression* lhs) {
@@ -2845,7 +2924,7 @@
         return create<ast::BinaryExpression>(t.source(), op, lhs, rhs.value);
     }
 
-    return expect_multiplicative_expression_post_unary_expression(lhs);
+    return expect_math_expression_post_unary_expression(lhs);
 }
 
 // relational_expression
@@ -2892,6 +2971,10 @@
             op = ast::BinaryOp::kLessThanEqual;
         } else if (t.Is(Token::Type::kGreaterThanEqual)) {
             op = ast::BinaryOp::kGreaterThanEqual;
+        } else if (t.Is(Token::Type::kEqualEqual)) {
+            op = ast::BinaryOp::kEqual;
+        } else if (t.Is(Token::Type::kNotEqual)) {
+            op = ast::BinaryOp::kNotEqual;
         }
 
         auto& next = peek();
@@ -2908,6 +2991,74 @@
     return lhs;
 }
 
+// expression
+//   : unary_expression bitwise_expression.post.unary_expression
+//   | unary_expression relational_expression.post.unary_expression
+//   | unary_expression relational_expression.post.unary_expression and_and
+//        relational_expression ( and_and relational_expression )*
+//   | unary_expression relational_expression.post.unary_expression or_or
+//        relational_expression ( or_or relational_expression )*
+//
+// Note, a `relational_expression` element was added to simplify many of the right sides
+Maybe<const ast::Expression*> ParserImpl::maybe_expression() {
+    auto lhs = unary_expression();
+    if (lhs.errored) {
+        return Failure::kErrored;
+    }
+    if (!lhs.matched) {
+        return Failure::kNoMatch;
+    }
+
+    auto bitwise = bitwise_expression_post_unary_expression(lhs.value);
+    if (bitwise.errored) {
+        return Failure::kErrored;
+    }
+    if (bitwise.matched) {
+        return bitwise.value;
+    }
+
+    auto relational = expect_relational_expression_post_unary_expression(lhs.value);
+    if (relational.errored) {
+        return Failure::kErrored;
+    }
+    auto* ret = relational.value;
+
+    auto& t = peek();
+    if (t.Is(Token::Type::kAndAnd) || t.Is(Token::Type::kOrOr)) {
+        ast::BinaryOp op = ast::BinaryOp::kNone;
+        if (t.Is(Token::Type::kAndAnd)) {
+            op = ast::BinaryOp::kLogicalAnd;
+        } else if (t.Is(Token::Type::kOrOr)) {
+            op = ast::BinaryOp::kLogicalOr;
+        }
+
+        while (continue_parsing()) {
+            auto& n = peek();
+            if (!n.Is(t.type())) {
+                if (n.Is(Token::Type::kAndAnd) || n.Is(Token::Type::kOrOr)) {
+                    return add_error(
+                        n.source(), std::string("mixing '") + std::string(t.to_name()) + "' and '" +
+                                        std::string(n.to_name()) + "' requires parenthesis");
+                }
+                break;
+            }
+            next();
+
+            auto rhs = relational_expression();
+            if (rhs.errored) {
+                return Failure::kErrored;
+            }
+            if (!rhs.matched) {
+                return add_error(peek(), std::string("unable to parse right side of ") +
+                                             std::string(t.to_name()) + " expression");
+            }
+
+            ret = create<ast::BinaryExpression>(t.source(), op, ret, rhs.value);
+        }
+    }
+    return ret;
+}
+
 // singular_expression
 //   : primary_expression postfix_expr
 Maybe<const ast::Expression*> ParserImpl::singular_expression() {
diff --git a/src/tint/reader/wgsl/parser_impl.h b/src/tint/reader/wgsl/parser_impl.h
index d6d3ccc..44525c0 100644
--- a/src/tint/reader/wgsl/parser_impl.h
+++ b/src/tint/reader/wgsl/parser_impl.h
@@ -309,6 +309,14 @@
         ast::Access access = ast::Access::kUndefined;
     };
 
+    /// MatrixDimensions contains the column and row information for a matrix
+    struct MatrixDimensions {
+        /// The number of columns
+        uint32_t columns = 0;
+        /// The number of rows
+        uint32_t rows = 0;
+    };
+
     /// Creates a new parser using the given file
     /// @param file the input source file to parse
     explicit ParserImpl(Source::File const* file);
@@ -435,6 +443,18 @@
     /// Parses a `type_alias_decl` grammar element
     /// @returns the type alias or nullptr on error
     Maybe<const ast::Alias*> type_alias_decl();
+    /// Parses a `callable` grammar element
+    /// @returns the type or nullptr
+    Maybe<const ast::Type*> callable();
+    /// Parses a `vec_prefix` grammar element
+    /// @returns the vector size or nullptr
+    Maybe<uint32_t> vec_prefix();
+    /// Parses a `mat_prefix` grammar element
+    /// @returns the matrix dimensions or nullptr
+    Maybe<MatrixDimensions> mat_prefix();
+    /// Parses a `type_decl_without_ident` grammar element
+    /// @returns the parsed Type or nullptr if none matched.
+    Maybe<const ast::Type*> type_decl_without_ident();
     /// Parses a `type_decl` grammar element
     /// @returns the parsed Type or nullptr if none matched.
     Maybe<const ast::Type*> type_decl();
@@ -630,6 +650,9 @@
     /// @param lhs the left side of the expression
     /// @returns the parsed expression or nullptr
     Expect<const ast::Expression*> expect_relational_expr(const ast::Expression* lhs);
+    /// Parses the `expression` grammar rule
+    /// @returns the parsed expression or nullptr
+    Maybe<const ast::Expression*> maybe_expression();
     /// Parses the `relational_expression` grammar element
     /// @returns the parsed expression or nullptr
     Maybe<const ast::Expression*> relational_expression();
@@ -759,6 +782,11 @@
     /// @return the parsed attribute, or nullptr on error.
     Expect<const ast::Attribute*> expect_attribute();
 
+    /// Splits a peekable token into to parts filling in the peekable fields.
+    /// @param lhs the token to set in the current position
+    /// @param rhs the token to set in the placeholder
+    void split_token(Token::Type lhs, Token::Type rhs);
+
   private:
     /// ReturnType resolves to the return type for the function or lambda F.
     template <typename F>
@@ -916,11 +944,11 @@
     /// Used to ensure that all attributes are consumed.
     bool expect_attributes_consumed(utils::VectorRef<const ast::Attribute*> list);
 
-    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_decl_pointer(const Source& s);
+    Expect<const ast::Type*> expect_type_decl_atomic(const Source& s);
+    Expect<const ast::Type*> expect_type_decl_vector(const Source& s, uint32_t count);
+    Expect<const ast::Type*> expect_type_decl_array(const Source& s);
+    Expect<const ast::Type*> expect_type_decl_matrix(const Source& s, const MatrixDimensions& dims);
 
     Expect<const ast::Type*> expect_type(std::string_view use);
 
@@ -928,8 +956,6 @@
     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);
diff --git a/src/tint/reader/wgsl/parser_impl_callable_test.cc b/src/tint/reader/wgsl/parser_impl_callable_test.cc
new file mode 100644
index 0000000..f47dde0
--- /dev/null
+++ b/src/tint/reader/wgsl/parser_impl_callable_test.cc
@@ -0,0 +1,156 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/reader/wgsl/parser_impl_test_helper.h"
+
+namespace tint::reader::wgsl {
+namespace {
+
+TEST_F(ParserImplTest, Callable_Array) {
+    auto p = parser("array");
+    auto t = p->callable();
+    ASSERT_TRUE(p->peek_is(Token::Type::kEOF));
+    EXPECT_TRUE(t.matched);
+    EXPECT_FALSE(t.errored);
+    ASSERT_FALSE(p->has_error()) << p->error();
+    ASSERT_NE(t.value, nullptr);
+    ASSERT_TRUE(t.value->Is<ast::Array>());
+
+    auto* a = t.value->As<ast::Array>();
+    EXPECT_FALSE(a->IsRuntimeArray());
+    EXPECT_EQ(a->type, nullptr);
+    EXPECT_EQ(a->count, nullptr);
+}
+
+TEST_F(ParserImplTest, Callable_VecPrefix) {
+    auto p = parser("vec3");
+    auto t = p->callable();
+    ASSERT_TRUE(p->peek_is(Token::Type::kEOF));
+    EXPECT_TRUE(t.matched);
+    EXPECT_FALSE(t.errored);
+    ASSERT_FALSE(p->has_error()) << p->error();
+    ASSERT_NE(t.value, nullptr);
+    ASSERT_TRUE(t.value->Is<ast::Vector>());
+
+    auto* v = t.value->As<ast::Vector>();
+    EXPECT_EQ(v->type, nullptr);
+    EXPECT_EQ(v->width, 3u);
+}
+
+TEST_F(ParserImplTest, Callable_MatPrefix) {
+    auto p = parser("mat3x2");
+    auto t = p->callable();
+    ASSERT_TRUE(p->peek_is(Token::Type::kEOF));
+    EXPECT_TRUE(t.matched);
+    EXPECT_FALSE(t.errored);
+    ASSERT_FALSE(p->has_error()) << p->error();
+    ASSERT_NE(t.value, nullptr);
+    ASSERT_TRUE(t.value->Is<ast::Matrix>());
+
+    auto* m = t.value->As<ast::Matrix>();
+    EXPECT_EQ(m->type, nullptr);
+    EXPECT_EQ(m->columns, 3u);
+    EXPECT_EQ(m->rows, 2u);
+}
+
+TEST_F(ParserImplTest, Callable_TypeDecl_F32) {
+    auto p = parser("f32");
+    auto t = p->callable();
+    ASSERT_TRUE(p->peek_is(Token::Type::kEOF));
+    EXPECT_TRUE(t.matched);
+    EXPECT_FALSE(t.errored);
+    ASSERT_FALSE(p->has_error()) << p->error();
+    ASSERT_NE(t.value, nullptr);
+    ASSERT_TRUE(t.value->Is<ast::F32>());
+}
+
+TEST_F(ParserImplTest, Callable_TypeDecl_Array) {
+    auto p = parser("array<f32, 2>");
+    auto t = p->callable();
+    ASSERT_TRUE(p->peek_is(Token::Type::kEOF));
+    EXPECT_TRUE(t.matched);
+    EXPECT_FALSE(t.errored);
+    ASSERT_FALSE(p->has_error()) << p->error();
+    ASSERT_NE(t.value, nullptr);
+    ASSERT_TRUE(t.value->Is<ast::Array>());
+
+    auto* a = t.value->As<ast::Array>();
+    EXPECT_FALSE(a->IsRuntimeArray());
+    EXPECT_TRUE(a->type->Is<ast::F32>());
+
+    auto* size = a->count->As<ast::IntLiteralExpression>();
+    ASSERT_NE(size, nullptr);
+    EXPECT_EQ(size->value, 2);
+    EXPECT_EQ(size->suffix, ast::IntLiteralExpression::Suffix::kNone);
+}
+
+TEST_F(ParserImplTest, Callable_TypeDecl_Array_Runtime) {
+    auto p = parser("array<f32>");
+    auto t = p->callable();
+    ASSERT_TRUE(p->peek_is(Token::Type::kEOF));
+    EXPECT_TRUE(t.matched);
+    EXPECT_FALSE(t.errored);
+    ASSERT_FALSE(p->has_error()) << p->error();
+    ASSERT_NE(t.value, nullptr);
+    ASSERT_TRUE(t.value->Is<ast::Array>());
+
+    auto* a = t.value->As<ast::Array>();
+    EXPECT_TRUE(a->IsRuntimeArray());
+    EXPECT_TRUE(a->type->Is<ast::F32>());
+
+    ASSERT_EQ(a->count, nullptr);
+}
+
+TEST_F(ParserImplTest, Callable_TypeDecl_VecPrefix) {
+    auto p = parser("vec3<f32>");
+    auto t = p->callable();
+    ASSERT_TRUE(p->peek_is(Token::Type::kEOF));
+    EXPECT_TRUE(t.matched);
+    EXPECT_FALSE(t.errored);
+    ASSERT_FALSE(p->has_error()) << p->error();
+    ASSERT_NE(t.value, nullptr);
+    ASSERT_TRUE(t.value->Is<ast::Vector>());
+
+    auto* v = t.value->As<ast::Vector>();
+    EXPECT_TRUE(v->type->Is<ast::F32>());
+    EXPECT_EQ(v->width, 3u);
+}
+
+TEST_F(ParserImplTest, Callable_TypeDecl_MatPrefix) {
+    auto p = parser("mat3x2<f32>");
+    auto t = p->callable();
+    ASSERT_TRUE(p->peek_is(Token::Type::kEOF));
+    EXPECT_TRUE(t.matched);
+    EXPECT_FALSE(t.errored);
+    ASSERT_FALSE(p->has_error()) << p->error();
+    ASSERT_NE(t.value, nullptr);
+    ASSERT_TRUE(t.value->Is<ast::Matrix>());
+
+    auto* m = t.value->As<ast::Matrix>();
+    EXPECT_TRUE(m->type->Is<ast::F32>());
+    EXPECT_EQ(m->columns, 3u);
+    EXPECT_EQ(m->rows, 2u);
+}
+
+TEST_F(ParserImplTest, Callable_NoMatch) {
+    auto p = parser("ident");
+    auto t = p->callable();
+    EXPECT_FALSE(t.matched);
+    EXPECT_FALSE(t.errored);
+    ASSERT_FALSE(p->has_error()) << p->error();
+    EXPECT_EQ(nullptr, t.value);
+}
+
+}  // namespace
+}  // namespace tint::reader::wgsl
diff --git a/src/tint/reader/wgsl/parser_impl_expression_test.cc b/src/tint/reader/wgsl/parser_impl_expression_test.cc
new file mode 100644
index 0000000..2938e3c
--- /dev/null
+++ b/src/tint/reader/wgsl/parser_impl_expression_test.cc
@@ -0,0 +1,254 @@
+// Copyright 2022 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/reader/wgsl/parser_impl_test_helper.h"
+
+namespace tint::reader::wgsl {
+namespace {
+
+TEST_F(ParserImplTest, Expression_InvalidLHS) {
+    auto p = parser("if (a) {} || true");
+    auto e = p->maybe_expression();
+    EXPECT_FALSE(e.matched);
+    EXPECT_FALSE(e.errored);
+    EXPECT_FALSE(p->has_error()) << p->error();
+    EXPECT_EQ(e.value, nullptr);
+}
+
+TEST_F(ParserImplTest, Expression_Or_Parses) {
+    auto p = parser("a || true");
+    auto e = p->maybe_expression();
+    EXPECT_TRUE(e.matched);
+    EXPECT_FALSE(e.errored);
+    EXPECT_FALSE(p->has_error()) << p->error();
+    ASSERT_NE(e.value, nullptr);
+
+    EXPECT_EQ(e->source.range.begin.line, 1u);
+    EXPECT_EQ(e->source.range.begin.column, 3u);
+    EXPECT_EQ(e->source.range.end.line, 1u);
+    EXPECT_EQ(e->source.range.end.column, 5u);
+
+    ASSERT_TRUE(e->Is<ast::BinaryExpression>());
+    auto* rel = e->As<ast::BinaryExpression>();
+    EXPECT_EQ(ast::BinaryOp::kLogicalOr, rel->op);
+
+    ASSERT_TRUE(rel->lhs->Is<ast::IdentifierExpression>());
+    auto* ident = rel->lhs->As<ast::IdentifierExpression>();
+    EXPECT_EQ(ident->symbol, p->builder().Symbols().Get("a"));
+
+    ASSERT_TRUE(rel->rhs->Is<ast::BoolLiteralExpression>());
+    ASSERT_TRUE(rel->rhs->As<ast::BoolLiteralExpression>()->value);
+}
+
+TEST_F(ParserImplTest, Expression_Or_Parses_Multiple) {
+    auto p = parser("a || true || b");
+    auto e = p->maybe_expression();
+    EXPECT_TRUE(e.matched);
+    EXPECT_FALSE(e.errored);
+    EXPECT_FALSE(p->has_error()) << p->error();
+    ASSERT_NE(e.value, nullptr);
+
+    ASSERT_TRUE(e->Is<ast::BinaryExpression>());
+    // lhs: (a || true)
+    // rhs: b
+    auto* rel = e->As<ast::BinaryExpression>();
+    EXPECT_EQ(ast::BinaryOp::kLogicalOr, rel->op);
+
+    ASSERT_TRUE(rel->rhs->Is<ast::IdentifierExpression>());
+    auto* ident = rel->rhs->As<ast::IdentifierExpression>();
+    EXPECT_EQ(ident->symbol, p->builder().Symbols().Get("b"));
+
+    ASSERT_TRUE(rel->lhs->Is<ast::BinaryExpression>());
+    // lhs: a
+    // rhs: true
+    rel = rel->lhs->As<ast::BinaryExpression>();
+    EXPECT_EQ(ast::BinaryOp::kLogicalOr, rel->op);
+
+    ASSERT_TRUE(rel->lhs->Is<ast::IdentifierExpression>());
+    ident = rel->lhs->As<ast::IdentifierExpression>();
+    EXPECT_EQ(ident->symbol, p->builder().Symbols().Get("a"));
+
+    ASSERT_TRUE(rel->rhs->Is<ast::BoolLiteralExpression>());
+    ASSERT_TRUE(rel->rhs->As<ast::BoolLiteralExpression>()->value);
+}
+
+TEST_F(ParserImplTest, Expression_Or_InvalidRHS) {
+    auto p = parser("true || if (a) {}");
+    auto e = p->maybe_expression();
+    EXPECT_FALSE(e.matched);
+    EXPECT_TRUE(e.errored);
+    EXPECT_EQ(e.value, nullptr);
+    EXPECT_TRUE(p->has_error());
+    EXPECT_EQ(p->error(), "1:9: unable to parse right side of || expression");
+}
+
+TEST_F(ParserImplTest, Expression_And_Parses) {
+    auto p = parser("a && true");
+    auto e = p->maybe_expression();
+    EXPECT_TRUE(e.matched);
+    EXPECT_FALSE(e.errored);
+    EXPECT_FALSE(p->has_error()) << p->error();
+    ASSERT_NE(e.value, nullptr);
+
+    EXPECT_EQ(e->source.range.begin.line, 1u);
+    EXPECT_EQ(e->source.range.begin.column, 3u);
+    EXPECT_EQ(e->source.range.end.line, 1u);
+    EXPECT_EQ(e->source.range.end.column, 5u);
+
+    ASSERT_TRUE(e->Is<ast::BinaryExpression>());
+    auto* rel = e->As<ast::BinaryExpression>();
+    EXPECT_EQ(ast::BinaryOp::kLogicalAnd, rel->op);
+
+    ASSERT_TRUE(rel->lhs->Is<ast::IdentifierExpression>());
+    auto* ident = rel->lhs->As<ast::IdentifierExpression>();
+    EXPECT_EQ(ident->symbol, p->builder().Symbols().Get("a"));
+
+    ASSERT_TRUE(rel->rhs->Is<ast::BoolLiteralExpression>());
+    ASSERT_TRUE(rel->rhs->As<ast::BoolLiteralExpression>()->value);
+}
+
+TEST_F(ParserImplTest, Expression_And_Parses_Multple) {
+    auto p = parser("a && true && b");
+    auto e = p->maybe_expression();
+    EXPECT_TRUE(e.matched);
+    EXPECT_FALSE(e.errored);
+    EXPECT_FALSE(p->has_error()) << p->error();
+    ASSERT_NE(e.value, nullptr);
+
+    ASSERT_TRUE(e->Is<ast::BinaryExpression>());
+    // lhs: (a && true)
+    // rhs: b
+    auto* rel = e->As<ast::BinaryExpression>();
+    EXPECT_EQ(ast::BinaryOp::kLogicalAnd, rel->op);
+
+    ASSERT_TRUE(rel->rhs->Is<ast::IdentifierExpression>());
+    auto* ident = rel->rhs->As<ast::IdentifierExpression>();
+    EXPECT_EQ(ident->symbol, p->builder().Symbols().Get("b"));
+
+    ASSERT_TRUE(rel->lhs->Is<ast::BinaryExpression>());
+    // lhs: a
+    // rhs: true
+    rel = rel->lhs->As<ast::BinaryExpression>();
+    ASSERT_TRUE(rel->lhs->Is<ast::IdentifierExpression>());
+    ident = rel->lhs->As<ast::IdentifierExpression>();
+    EXPECT_EQ(ident->symbol, p->builder().Symbols().Get("a"));
+
+    ASSERT_TRUE(rel->rhs->Is<ast::BoolLiteralExpression>());
+    ASSERT_TRUE(rel->rhs->As<ast::BoolLiteralExpression>()->value);
+}
+
+TEST_F(ParserImplTest, Expression_And_InvalidRHS) {
+    auto p = parser("true && if (a) {}");
+    auto e = p->maybe_expression();
+    EXPECT_FALSE(e.matched);
+    EXPECT_TRUE(e.errored);
+    EXPECT_EQ(e.value, nullptr);
+    EXPECT_TRUE(p->has_error());
+    EXPECT_EQ(p->error(), "1:9: unable to parse right side of && expression");
+}
+
+TEST_F(ParserImplTest, Expression_Mixing_OrWithAnd) {
+    auto p = parser("a && true || b");
+    auto e = p->maybe_expression();
+    EXPECT_FALSE(e.matched);
+    EXPECT_TRUE(e.errored);
+    EXPECT_EQ(e.value, nullptr);
+    EXPECT_TRUE(p->has_error());
+    EXPECT_EQ(p->error(), "1:11: mixing '&&' and '||' requires parenthesis");
+}
+
+TEST_F(ParserImplTest, Expression_Mixing_AndWithOr) {
+    auto p = parser("a || true && b");
+    auto e = p->maybe_expression();
+    EXPECT_FALSE(e.matched);
+    EXPECT_TRUE(e.errored);
+    EXPECT_EQ(e.value, nullptr);
+    EXPECT_TRUE(p->has_error());
+    EXPECT_EQ(p->error(), "1:11: mixing '||' and '&&' requires parenthesis");
+}
+
+TEST_F(ParserImplTest, Expression_Bitwise) {
+    auto p = parser("a & b");
+    auto e = p->maybe_expression();
+    EXPECT_TRUE(e.matched);
+    EXPECT_FALSE(e.errored);
+    EXPECT_FALSE(p->has_error()) << p->error();
+    ASSERT_NE(e.value, nullptr);
+
+    ASSERT_TRUE(e->Is<ast::BinaryExpression>());
+    auto* rel = e->As<ast::BinaryExpression>();
+    EXPECT_EQ(ast::BinaryOp::kAnd, rel->op);
+
+    ASSERT_TRUE(rel->lhs->Is<ast::IdentifierExpression>());
+    auto* ident = rel->lhs->As<ast::IdentifierExpression>();
+    EXPECT_EQ(ident->symbol, p->builder().Symbols().Get("a"));
+
+    ASSERT_TRUE(rel->rhs->Is<ast::IdentifierExpression>());
+    ident = rel->rhs->As<ast::IdentifierExpression>();
+    EXPECT_EQ(ident->symbol, p->builder().Symbols().Get("b"));
+}
+
+TEST_F(ParserImplTest, Expression_Relational) {
+    auto p = parser("a <= b");
+    auto e = p->maybe_expression();
+    EXPECT_TRUE(e.matched);
+    EXPECT_FALSE(e.errored);
+    EXPECT_FALSE(p->has_error()) << p->error();
+    ASSERT_NE(e.value, nullptr);
+
+    ASSERT_TRUE(e->Is<ast::BinaryExpression>());
+    auto* rel = e->As<ast::BinaryExpression>();
+    EXPECT_EQ(ast::BinaryOp::kLessThanEqual, rel->op);
+
+    ASSERT_TRUE(rel->lhs->Is<ast::IdentifierExpression>());
+    auto* ident = rel->lhs->As<ast::IdentifierExpression>();
+    EXPECT_EQ(ident->symbol, p->builder().Symbols().Get("a"));
+
+    ASSERT_TRUE(rel->rhs->Is<ast::IdentifierExpression>());
+    ident = rel->rhs->As<ast::IdentifierExpression>();
+    EXPECT_EQ(ident->symbol, p->builder().Symbols().Get("b"));
+}
+
+TEST_F(ParserImplTest, Expression_InvalidUnary) {
+    auto p = parser("!if || true");
+    auto e = p->maybe_expression();
+    EXPECT_FALSE(e.matched);
+    EXPECT_TRUE(e.errored);
+    EXPECT_EQ(e.value, nullptr);
+    EXPECT_TRUE(p->has_error());
+    EXPECT_EQ(p->error(), "1:2: unable to parse right side of ! expression");
+}
+
+TEST_F(ParserImplTest, Expression_InvalidBitwise) {
+    auto p = parser("a & if");
+    auto e = p->maybe_expression();
+    EXPECT_FALSE(e.matched);
+    EXPECT_TRUE(e.errored);
+    EXPECT_EQ(e.value, nullptr);
+    EXPECT_TRUE(p->has_error());
+    EXPECT_EQ(p->error(), "1:5: unable to parse right side of & expression");
+}
+
+TEST_F(ParserImplTest, Expression_InvalidRelational) {
+    auto p = parser("a <= if");
+    auto e = p->maybe_expression();
+    EXPECT_FALSE(e.matched);
+    EXPECT_TRUE(e.errored);
+    EXPECT_EQ(e.value, nullptr);
+    EXPECT_TRUE(p->has_error());
+    EXPECT_EQ(p->error(), "1:6: unable to parse right side of <= expression");
+}
+
+}  // namespace
+}  // namespace tint::reader::wgsl
diff --git a/src/tint/reader/wgsl/parser_impl_relational_expression_test.cc b/src/tint/reader/wgsl/parser_impl_relational_expression_test.cc
index 1c47f87..59cddc6 100644
--- a/src/tint/reader/wgsl/parser_impl_relational_expression_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_relational_expression_test.cc
@@ -244,6 +244,56 @@
     ASSERT_TRUE(rel->rhs->As<ast::BoolLiteralExpression>()->value);
 }
 
+TEST_F(ParserImplTest, RelationalExpression_PostUnary_Parses_Equal) {
+    auto p = parser("a == true");
+    auto lhs = p->unary_expression();
+    auto e = p->expect_relational_expression_post_unary_expression(lhs.value);
+    EXPECT_FALSE(e.errored);
+    EXPECT_FALSE(p->has_error()) << p->error();
+    ASSERT_NE(e.value, nullptr);
+
+    EXPECT_EQ(e->source.range.begin.line, 1u);
+    EXPECT_EQ(e->source.range.begin.column, 3u);
+    EXPECT_EQ(e->source.range.end.line, 1u);
+    EXPECT_EQ(e->source.range.end.column, 5u);
+
+    ASSERT_TRUE(e->Is<ast::BinaryExpression>());
+    auto* rel = e->As<ast::BinaryExpression>();
+    EXPECT_EQ(ast::BinaryOp::kEqual, rel->op);
+
+    ASSERT_TRUE(rel->lhs->Is<ast::IdentifierExpression>());
+    auto* ident = rel->lhs->As<ast::IdentifierExpression>();
+    EXPECT_EQ(ident->symbol, p->builder().Symbols().Get("a"));
+
+    ASSERT_TRUE(rel->rhs->Is<ast::BoolLiteralExpression>());
+    ASSERT_TRUE(rel->rhs->As<ast::BoolLiteralExpression>()->value);
+}
+
+TEST_F(ParserImplTest, RelationalExpression_PostUnary_Parses_NotEqual) {
+    auto p = parser("a != true");
+    auto lhs = p->unary_expression();
+    auto e = p->expect_relational_expression_post_unary_expression(lhs.value);
+    EXPECT_FALSE(e.errored);
+    EXPECT_FALSE(p->has_error()) << p->error();
+    ASSERT_NE(e.value, nullptr);
+
+    EXPECT_EQ(e->source.range.begin.line, 1u);
+    EXPECT_EQ(e->source.range.begin.column, 3u);
+    EXPECT_EQ(e->source.range.end.line, 1u);
+    EXPECT_EQ(e->source.range.end.column, 5u);
+
+    ASSERT_TRUE(e->Is<ast::BinaryExpression>());
+    auto* rel = e->As<ast::BinaryExpression>();
+    EXPECT_EQ(ast::BinaryOp::kNotEqual, rel->op);
+
+    ASSERT_TRUE(rel->lhs->Is<ast::IdentifierExpression>());
+    auto* ident = rel->lhs->As<ast::IdentifierExpression>();
+    EXPECT_EQ(ident->symbol, p->builder().Symbols().Get("a"));
+
+    ASSERT_TRUE(rel->rhs->Is<ast::BoolLiteralExpression>());
+    ASSERT_TRUE(rel->rhs->As<ast::BoolLiteralExpression>()->value);
+}
+
 TEST_F(ParserImplTest, RelationalExpression_PostUnary_InvalidRHS) {
     auto p = parser("true < if (a) {}");
     auto lhs = p->unary_expression();
diff --git a/src/tint/reader/wgsl/parser_impl_shift_expression_test.cc b/src/tint/reader/wgsl/parser_impl_shift_expression_test.cc
index 76337cd..d518004 100644
--- a/src/tint/reader/wgsl/parser_impl_shift_expression_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_shift_expression_test.cc
@@ -164,6 +164,27 @@
     ASSERT_TRUE(rel->rhs->As<ast::BoolLiteralExpression>()->value);
 }
 
+TEST_F(ParserImplTest, ShiftExpression_PostUnary_Parses_Additive) {
+    auto p = parser("a + b");
+    auto lhs = p->unary_expression();
+    auto e = p->expect_shift_expression_post_unary_expression(lhs.value);
+    EXPECT_FALSE(e.errored);
+    EXPECT_FALSE(p->has_error()) << p->error();
+    ASSERT_NE(e.value, nullptr);
+
+    ASSERT_TRUE(e->Is<ast::BinaryExpression>());
+    auto* rel = e->As<ast::BinaryExpression>();
+    EXPECT_EQ(ast::BinaryOp::kAdd, rel->op);
+
+    ASSERT_TRUE(rel->lhs->Is<ast::IdentifierExpression>());
+    auto* ident = rel->lhs->As<ast::IdentifierExpression>();
+    EXPECT_EQ(ident->symbol, p->builder().Symbols().Get("a"));
+
+    ASSERT_TRUE(rel->rhs->Is<ast::IdentifierExpression>());
+    ident = rel->rhs->As<ast::IdentifierExpression>();
+    EXPECT_EQ(ident->symbol, p->builder().Symbols().Get("b"));
+}
+
 TEST_F(ParserImplTest, ShiftExpression_PostUnary_Parses_Multiplicative) {
     auto p = parser("a * b");
     auto lhs = p->unary_expression();
diff --git a/src/tint/reader/wgsl/parser_impl_test.cc b/src/tint/reader/wgsl/parser_impl_test.cc
index c234811..21efd2f 100644
--- a/src/tint/reader/wgsl/parser_impl_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_test.cc
@@ -136,5 +136,66 @@
     EXPECT_EQ(p->error(), "5:3: unterminated block comment") << p->error();
 }
 
+TEST_F(ParserImplTest, Peek) {
+    auto p = parser("a == if");
+    EXPECT_TRUE(p->peek_is(Token::Type::kIdentifier));
+    EXPECT_TRUE(p->peek_is(Token::Type::kEqualEqual, 1));
+    EXPECT_TRUE(p->peek_is(Token::Type::kIf, 2));
+}
+
+TEST_F(ParserImplTest, Peek_Placeholder) {
+    auto p = parser(">> if");
+    EXPECT_TRUE(p->peek_is(Token::Type::kShiftRight));
+    EXPECT_TRUE(p->peek_is(Token::Type::kIf, 1));
+}
+
+TEST_F(ParserImplTest, Peek_PastPlaceholder) {
+    auto p = parser(">= vec2<u32>");
+    auto& n = p->next();
+    ASSERT_TRUE(n.Is(Token::Type::kGreaterThanEqual));
+    EXPECT_TRUE(p->peek_is(Token::Type::kVec2)) << "expected: vec2 got: " << p->peek().to_name();
+    EXPECT_TRUE(p->peek_is(Token::Type::kLessThan, 1))
+        << "expected: < got: " << p->peek(1).to_name();
+}
+
+TEST_F(ParserImplTest, Peek_MultiplePlaceholder) {
+    auto p = parser(">= >= vec2<u32>");
+    auto& n = p->next();
+    ASSERT_TRUE(n.Is(Token::Type::kGreaterThanEqual));
+    EXPECT_TRUE(p->peek_is(Token::Type::kGreaterThanEqual))
+        << "expected: <= got: " << p->peek().to_name();
+    EXPECT_TRUE(p->peek_is(Token::Type::kVec2, 1))
+        << "expected: vec2 got: " << p->peek(1).to_name();
+    EXPECT_TRUE(p->peek_is(Token::Type::kLessThan, 2))
+        << "expected: < got: " << p->peek(2).to_name();
+}
+
+TEST_F(ParserImplTest, Peek_PastEnd) {
+    auto p = parser(">");
+    EXPECT_TRUE(p->peek_is(Token::Type::kGreaterThan));
+    EXPECT_TRUE(p->peek_is(Token::Type::kEOF, 1));
+    EXPECT_TRUE(p->peek_is(Token::Type::kEOF, 2));
+}
+
+TEST_F(ParserImplTest, Peek_PastEnd_WalkingPlaceholders) {
+    auto p = parser(">= >=");
+    auto& n = p->next();
+    ASSERT_TRUE(n.Is(Token::Type::kGreaterThanEqual));
+    EXPECT_TRUE(p->peek_is(Token::Type::kGreaterThanEqual))
+        << "expected: <= got: " << p->peek().to_name();
+    EXPECT_TRUE(p->peek_is(Token::Type::kEOF, 1)) << "expected: EOF got: " << p->peek(1).to_name();
+}
+
+TEST_F(ParserImplTest, Peek_AfterSplit) {
+    auto p = parser(">= vec2<u32>");
+    auto& n = p->next();
+    ASSERT_TRUE(n.Is(Token::Type::kGreaterThanEqual));
+    EXPECT_TRUE(p->peek_is(Token::Type::kVec2)) << "expected: vec2 got: " << p->peek().to_name();
+
+    p->split_token(Token::Type::kGreaterThan, Token::Type::kEqual);
+    ASSERT_TRUE(n.Is(Token::Type::kGreaterThan));
+    EXPECT_TRUE(p->peek_is(Token::Type::kEqual)) << "expected: = got: " << p->peek().to_name();
+}
+
 }  // namespace
 }  // namespace tint::reader::wgsl
diff --git a/src/tint/reader/wgsl/parser_impl_type_decl_test.cc b/src/tint/reader/wgsl/parser_impl_type_decl_test.cc
index 57c1b72..926f7c7 100644
--- a/src/tint/reader/wgsl/parser_impl_type_decl_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_type_decl_test.cc
@@ -528,22 +528,6 @@
     EXPECT_EQ(t.value->source.range, (Source::Range{{1u, 1u}, {1u, 17u}}));
 }
 
-TEST_F(ParserImplTest, TypeDecl_Array_InferTypeAndSize) {
-    auto p = parser("array");
-    auto t = p->type_decl();
-    EXPECT_TRUE(t.matched);
-    EXPECT_FALSE(t.errored);
-    ASSERT_NE(t.value, nullptr) << p->error();
-    ASSERT_FALSE(p->has_error());
-    ASSERT_TRUE(t.value->Is<ast::Array>());
-
-    auto* a = t.value->As<ast::Array>();
-    EXPECT_FALSE(a->IsRuntimeArray());
-    EXPECT_EQ(a->type, nullptr);
-    EXPECT_EQ(a->count, nullptr);
-    EXPECT_EQ(t.value->source.range, (Source::Range{{1u, 1u}, {1u, 6u}}));
-}
-
 TEST_F(ParserImplTest, TypeDecl_Array_BadSize) {
     auto p = parser("array<f32, !>");
     auto t = p->type_decl();
diff --git a/src/tint/reader/wgsl/parser_impl_type_decl_without_ident_test.cc b/src/tint/reader/wgsl/parser_impl_type_decl_without_ident_test.cc
new file mode 100644
index 0000000..ec23f2f
--- /dev/null
+++ b/src/tint/reader/wgsl/parser_impl_type_decl_without_ident_test.cc
@@ -0,0 +1,676 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/alias.h"
+#include "src/tint/ast/array.h"
+#include "src/tint/ast/matrix.h"
+#include "src/tint/ast/sampler.h"
+#include "src/tint/reader/wgsl/parser_impl_test_helper.h"
+#include "src/tint/sem/sampled_texture.h"
+
+namespace tint::reader::wgsl {
+namespace {
+
+TEST_F(ParserImplTest, TypeDeclWithoutIdent_Invalid) {
+    auto p = parser("1234");
+    auto t = p->type_decl_without_ident();
+    EXPECT_EQ(t.errored, false);
+    EXPECT_EQ(t.matched, false);
+    EXPECT_EQ(t.value, nullptr);
+    EXPECT_FALSE(p->has_error()) << p->error();
+}
+
+TEST_F(ParserImplTest, TypeDeclWithoutIdent_Identifier) {
+    auto p = parser("A");
+    auto t = p->type_decl_without_ident();
+    EXPECT_FALSE(t.matched);
+    EXPECT_FALSE(t.errored);
+    ASSERT_EQ(t.value, nullptr);
+    EXPECT_FALSE(p->has_error()) << p->error();
+}
+
+TEST_F(ParserImplTest, TypeDeclWithoutIdent_Bool) {
+    auto p = parser("bool");
+    auto t = p->type_decl_without_ident();
+    EXPECT_TRUE(t.matched);
+    EXPECT_FALSE(t.errored);
+    ASSERT_NE(t.value, nullptr) << p->error();
+    ASSERT_TRUE(t.value->Is<ast::Bool>());
+    EXPECT_EQ(t.value->source.range, (Source::Range{{1u, 1u}, {1u, 5u}}));
+}
+
+TEST_F(ParserImplTest, TypeDeclWithoutIdent_F16) {
+    auto p = parser("f16");
+    auto t = p->type_decl_without_ident();
+    EXPECT_TRUE(t.matched);
+    EXPECT_FALSE(t.errored);
+    ASSERT_NE(t.value, nullptr) << p->error();
+    ASSERT_TRUE(t.value->Is<ast::F16>());
+    EXPECT_EQ(t.value->source.range, (Source::Range{{1u, 1u}, {1u, 4u}}));
+}
+
+TEST_F(ParserImplTest, TypeDeclWithoutIdent_F32) {
+    auto p = parser("f32");
+    auto t = p->type_decl_without_ident();
+    EXPECT_TRUE(t.matched);
+    EXPECT_FALSE(t.errored);
+    ASSERT_NE(t.value, nullptr) << p->error();
+    ASSERT_TRUE(t.value->Is<ast::F32>());
+    EXPECT_EQ(t.value->source.range, (Source::Range{{1u, 1u}, {1u, 4u}}));
+}
+
+TEST_F(ParserImplTest, TypeDeclWithoutIdent_I32) {
+    auto p = parser("i32");
+    auto t = p->type_decl_without_ident();
+    EXPECT_TRUE(t.matched);
+    EXPECT_FALSE(t.errored);
+    ASSERT_NE(t.value, nullptr) << p->error();
+    ASSERT_TRUE(t.value->Is<ast::I32>());
+    EXPECT_EQ(t.value->source.range, (Source::Range{{1u, 1u}, {1u, 4u}}));
+}
+
+TEST_F(ParserImplTest, TypeDeclWithoutIdent_U32) {
+    auto p = parser("u32");
+    auto t = p->type_decl_without_ident();
+    EXPECT_TRUE(t.matched);
+    EXPECT_FALSE(t.errored);
+    ASSERT_NE(t.value, nullptr) << p->error();
+    ASSERT_TRUE(t.value->Is<ast::U32>());
+    EXPECT_EQ(t.value->source.range, (Source::Range{{1u, 1u}, {1u, 4u}}));
+}
+
+struct VecData {
+    const char* input;
+    size_t count;
+    Source::Range range;
+};
+inline std::ostream& operator<<(std::ostream& out, VecData data) {
+    out << std::string(data.input);
+    return out;
+}
+
+class TypeDeclWithoutIdent_VecTest : public ParserImplTestWithParam<VecData> {};
+
+TEST_P(TypeDeclWithoutIdent_VecTest, Parse) {
+    auto params = GetParam();
+    auto p = parser(params.input);
+    auto t = p->type_decl_without_ident();
+    EXPECT_TRUE(t.matched);
+    EXPECT_FALSE(t.errored);
+    ASSERT_NE(t.value, nullptr) << p->error();
+    ASSERT_FALSE(p->has_error());
+    EXPECT_TRUE(t.value->Is<ast::Vector>());
+    EXPECT_EQ(t.value->As<ast::Vector>()->width, params.count);
+    EXPECT_EQ(t.value->source.range, params.range);
+}
+INSTANTIATE_TEST_SUITE_P(ParserImplTest,
+                         TypeDeclWithoutIdent_VecTest,
+                         testing::Values(VecData{"vec2<f32>", 2, {{1u, 1u}, {1u, 10u}}},
+                                         VecData{"vec3<f32>", 3, {{1u, 1u}, {1u, 10u}}},
+                                         VecData{"vec4<f32>", 4, {{1u, 1u}, {1u, 10u}}}));
+
+class TypeDeclWithoutIdent_VecMissingGreaterThanTest : public ParserImplTestWithParam<VecData> {};
+
+TEST_P(TypeDeclWithoutIdent_VecMissingGreaterThanTest, Handles_Missing_GreaterThan) {
+    auto params = GetParam();
+    auto p = parser(params.input);
+    auto t = p->type_decl_without_ident();
+    EXPECT_TRUE(t.errored);
+    EXPECT_FALSE(t.matched);
+    ASSERT_EQ(t.value, nullptr);
+    ASSERT_TRUE(p->has_error());
+    ASSERT_EQ(p->error(), "1:9: expected '>' for vector");
+}
+INSTANTIATE_TEST_SUITE_P(ParserImplTest,
+                         TypeDeclWithoutIdent_VecMissingGreaterThanTest,
+                         testing::Values(VecData{"vec2<f32", 2, {}},
+                                         VecData{"vec3<f32", 3, {}},
+                                         VecData{"vec4<f32", 4, {}}));
+
+class TypeDeclWithoutIdent_VecMissingType : public ParserImplTestWithParam<VecData> {};
+
+TEST_P(TypeDeclWithoutIdent_VecMissingType, Handles_Missing_Type) {
+    auto params = GetParam();
+    auto p = parser(params.input);
+    auto t = p->type_decl_without_ident();
+    EXPECT_TRUE(t.errored);
+    EXPECT_FALSE(t.matched);
+    ASSERT_EQ(t.value, nullptr);
+    ASSERT_TRUE(p->has_error());
+    ASSERT_EQ(p->error(), "1:6: invalid type for vector");
+}
+INSTANTIATE_TEST_SUITE_P(ParserImplTest,
+                         TypeDeclWithoutIdent_VecMissingType,
+                         testing::Values(VecData{"vec2<>", 2, {}},
+                                         VecData{"vec3<>", 3, {}},
+                                         VecData{"vec4<>", 4, {}}));
+
+TEST_F(ParserImplTest, TypeDeclWithoutIdent_Ptr) {
+    auto p = parser("ptr<function, f32>");
+    auto t = p->type_decl_without_ident();
+    EXPECT_TRUE(t.matched);
+    EXPECT_FALSE(t.errored);
+    ASSERT_NE(t.value, nullptr) << p->error();
+    ASSERT_FALSE(p->has_error());
+    ASSERT_TRUE(t.value->Is<ast::Pointer>());
+
+    auto* ptr = t.value->As<ast::Pointer>();
+    ASSERT_TRUE(ptr->type->Is<ast::F32>());
+    ASSERT_EQ(ptr->storage_class, ast::StorageClass::kFunction);
+    EXPECT_EQ(t.value->source.range, (Source::Range{{1u, 1u}, {1u, 19u}}));
+}
+
+TEST_F(ParserImplTest, TypeDeclWithoutIdent_Ptr_WithAccess) {
+    auto p = parser("ptr<function, f32, read>");
+    auto t = p->type_decl_without_ident();
+    EXPECT_TRUE(t.matched);
+    EXPECT_FALSE(t.errored);
+    ASSERT_NE(t.value, nullptr) << p->error();
+    ASSERT_FALSE(p->has_error());
+    ASSERT_TRUE(t.value->Is<ast::Pointer>());
+
+    auto* ptr = t.value->As<ast::Pointer>();
+    ASSERT_TRUE(ptr->type->Is<ast::F32>());
+    ASSERT_EQ(ptr->storage_class, ast::StorageClass::kFunction);
+    ASSERT_EQ(ptr->access, ast::Access::kRead);
+    EXPECT_EQ(t.value->source.range, (Source::Range{{1u, 1u}, {1u, 25u}}));
+}
+
+TEST_F(ParserImplTest, TypeDeclWithoutIdent_Ptr_ToVec) {
+    auto p = parser("ptr<function, vec2<f32>>");
+    auto t = p->type_decl_without_ident();
+    EXPECT_TRUE(t.matched);
+    EXPECT_FALSE(t.errored);
+    ASSERT_NE(t.value, nullptr) << p->error();
+    ASSERT_FALSE(p->has_error());
+    ASSERT_TRUE(t.value->Is<ast::Pointer>());
+
+    auto* ptr = t.value->As<ast::Pointer>();
+    ASSERT_TRUE(ptr->type->Is<ast::Vector>());
+    ASSERT_EQ(ptr->storage_class, ast::StorageClass::kFunction);
+
+    auto* vec = ptr->type->As<ast::Vector>();
+    ASSERT_EQ(vec->width, 2u);
+    ASSERT_TRUE(vec->type->Is<ast::F32>());
+    EXPECT_EQ(t.value->source.range, (Source::Range{{1u, 1u}, {1u, 25}}));
+}
+
+TEST_F(ParserImplTest, TypeDeclWithoutIdent_Ptr_MissingLessThan) {
+    auto p = parser("ptr private, f32>");
+    auto t = p->type_decl_without_ident();
+    EXPECT_TRUE(t.errored);
+    EXPECT_FALSE(t.matched);
+    ASSERT_EQ(t.value, nullptr);
+    ASSERT_TRUE(p->has_error());
+    ASSERT_EQ(p->error(), "1:5: expected '<' for ptr declaration");
+}
+
+TEST_F(ParserImplTest, TypeDeclWithoutIdent_Ptr_MissingGreaterThanAfterType) {
+    auto p = parser("ptr<function, f32");
+    auto t = p->type_decl_without_ident();
+    EXPECT_TRUE(t.errored);
+    EXPECT_FALSE(t.matched);
+    ASSERT_EQ(t.value, nullptr);
+    ASSERT_TRUE(p->has_error());
+    ASSERT_EQ(p->error(), "1:18: expected '>' for ptr declaration");
+}
+
+TEST_F(ParserImplTest, TypeDeclWithoutIdent_Ptr_MissingGreaterThanAfterAccess) {
+    auto p = parser("ptr<function, f32, read");
+    auto t = p->type_decl_without_ident();
+    EXPECT_TRUE(t.errored);
+    EXPECT_FALSE(t.matched);
+    ASSERT_EQ(t.value, nullptr);
+    ASSERT_TRUE(p->has_error());
+    ASSERT_EQ(p->error(), "1:24: expected '>' for ptr declaration");
+}
+
+TEST_F(ParserImplTest, TypeDeclWithoutIdent_Ptr_MissingCommaAfterStorageClass) {
+    auto p = parser("ptr<function f32>");
+    auto t = p->type_decl_without_ident();
+    EXPECT_TRUE(t.errored);
+    EXPECT_FALSE(t.matched);
+    ASSERT_EQ(t.value, nullptr);
+    ASSERT_TRUE(p->has_error());
+    ASSERT_EQ(p->error(), "1:14: expected ',' for ptr declaration");
+}
+
+TEST_F(ParserImplTest, TypeDeclWithoutIdent_Ptr_MissingCommaAfterAccess) {
+    auto p = parser("ptr<function, f32 read>");
+    auto t = p->type_decl_without_ident();
+    EXPECT_TRUE(t.errored);
+    EXPECT_FALSE(t.matched);
+    ASSERT_EQ(t.value, nullptr);
+    ASSERT_TRUE(p->has_error());
+    ASSERT_EQ(p->error(), "1:19: expected '>' for ptr declaration");
+}
+
+TEST_F(ParserImplTest, TypeDeclWithoutIdent_Ptr_MissingStorageClass) {
+    auto p = parser("ptr<, f32>");
+    auto t = p->type_decl_without_ident();
+    EXPECT_TRUE(t.errored);
+    EXPECT_FALSE(t.matched);
+    ASSERT_EQ(t.value, nullptr);
+    ASSERT_TRUE(p->has_error());
+    ASSERT_EQ(p->error(), "1:5: expected identifier for storage class");
+}
+
+TEST_F(ParserImplTest, TypeDeclWithoutIdent_Ptr_MissingType) {
+    auto p = parser("ptr<function,>");
+    auto t = p->type_decl_without_ident();
+    EXPECT_TRUE(t.errored);
+    EXPECT_FALSE(t.matched);
+    ASSERT_EQ(t.value, nullptr);
+    ASSERT_TRUE(p->has_error());
+    ASSERT_EQ(p->error(), "1:14: invalid type for ptr declaration");
+}
+
+TEST_F(ParserImplTest, TypeDeclWithoutIdent_Ptr_MissingAccess) {
+    auto p = parser("ptr<function, i32, >");
+    auto t = p->type_decl_without_ident();
+    EXPECT_TRUE(t.errored);
+    EXPECT_FALSE(t.matched);
+    ASSERT_EQ(t.value, nullptr);
+    ASSERT_TRUE(p->has_error());
+    ASSERT_EQ(p->error(), "1:20: expected identifier for access control");
+}
+
+TEST_F(ParserImplTest, TypeDeclWithoutIdent_Ptr_MissingParams) {
+    auto p = parser("ptr<>");
+    auto t = p->type_decl_without_ident();
+    EXPECT_TRUE(t.errored);
+    EXPECT_FALSE(t.matched);
+    ASSERT_EQ(t.value, nullptr);
+    ASSERT_TRUE(p->has_error());
+    ASSERT_EQ(p->error(), "1:5: expected identifier for storage class");
+}
+
+TEST_F(ParserImplTest, TypeDeclWithoutIdent_Ptr_BadStorageClass) {
+    auto p = parser("ptr<unknown, f32>");
+    auto t = p->type_decl_without_ident();
+    EXPECT_TRUE(t.errored);
+    EXPECT_FALSE(t.matched);
+    ASSERT_EQ(t.value, nullptr);
+    ASSERT_TRUE(p->has_error());
+    ASSERT_EQ(p->error(), "1:5: invalid storage class for ptr declaration");
+}
+
+TEST_F(ParserImplTest, TypeDeclWithoutIdent_Ptr_BadAccess) {
+    auto p = parser("ptr<function, i32, unknown>");
+    auto t = p->type_decl_without_ident();
+    EXPECT_TRUE(t.errored);
+    EXPECT_FALSE(t.matched);
+    ASSERT_EQ(t.value, nullptr);
+    ASSERT_TRUE(p->has_error());
+    ASSERT_EQ(p->error(), "1:20: invalid value for access control");
+}
+
+TEST_F(ParserImplTest, TypeDeclWithoutIdent_Atomic) {
+    auto p = parser("atomic<f32>");
+    auto t = p->type_decl_without_ident();
+    EXPECT_TRUE(t.matched);
+    EXPECT_FALSE(t.errored);
+    ASSERT_NE(t.value, nullptr) << p->error();
+    ASSERT_FALSE(p->has_error());
+    ASSERT_TRUE(t.value->Is<ast::Atomic>());
+
+    auto* atomic = t.value->As<ast::Atomic>();
+    ASSERT_TRUE(atomic->type->Is<ast::F32>());
+    EXPECT_EQ(t.value->source.range, (Source::Range{{1u, 1u}, {1u, 12u}}));
+}
+
+TEST_F(ParserImplTest, TypeDeclWithoutIdent_Atomic_ToVec) {
+    auto p = parser("atomic<vec2<f32>>");
+    auto t = p->type_decl_without_ident();
+    EXPECT_TRUE(t.matched);
+    EXPECT_FALSE(t.errored);
+    ASSERT_NE(t.value, nullptr) << p->error();
+    ASSERT_FALSE(p->has_error());
+    ASSERT_TRUE(t.value->Is<ast::Atomic>());
+
+    auto* atomic = t.value->As<ast::Atomic>();
+    ASSERT_TRUE(atomic->type->Is<ast::Vector>());
+
+    auto* vec = atomic->type->As<ast::Vector>();
+    ASSERT_EQ(vec->width, 2u);
+    ASSERT_TRUE(vec->type->Is<ast::F32>());
+    EXPECT_EQ(t.value->source.range, (Source::Range{{1u, 1u}, {1u, 18u}}));
+}
+
+TEST_F(ParserImplTest, TypeDeclWithoutIdent_Atomic_MissingLessThan) {
+    auto p = parser("atomic f32>");
+    auto t = p->type_decl_without_ident();
+    EXPECT_TRUE(t.errored);
+    EXPECT_FALSE(t.matched);
+    ASSERT_EQ(t.value, nullptr);
+    ASSERT_TRUE(p->has_error());
+    ASSERT_EQ(p->error(), "1:8: expected '<' for atomic declaration");
+}
+
+TEST_F(ParserImplTest, TypeDeclWithoutIdent_Atomic_MissingGreaterThan) {
+    auto p = parser("atomic<f32");
+    auto t = p->type_decl_without_ident();
+    EXPECT_TRUE(t.errored);
+    EXPECT_FALSE(t.matched);
+    ASSERT_EQ(t.value, nullptr);
+    ASSERT_TRUE(p->has_error());
+    ASSERT_EQ(p->error(), "1:11: expected '>' for atomic declaration");
+}
+
+TEST_F(ParserImplTest, TypeDeclWithoutIdent_Atomic_MissingType) {
+    auto p = parser("atomic<>");
+    auto t = p->type_decl_without_ident();
+    EXPECT_TRUE(t.errored);
+    EXPECT_FALSE(t.matched);
+    ASSERT_EQ(t.value, nullptr);
+    ASSERT_TRUE(p->has_error());
+    ASSERT_EQ(p->error(), "1:8: invalid type for atomic declaration");
+}
+
+TEST_F(ParserImplTest, TypeDeclWithoutIdent_Array_AbstractIntLiteralSize) {
+    auto p = parser("array<f32, 5>");
+    auto t = p->type_decl_without_ident();
+    EXPECT_TRUE(t.matched);
+    EXPECT_FALSE(t.errored);
+    ASSERT_NE(t.value, nullptr) << p->error();
+    ASSERT_FALSE(p->has_error());
+    ASSERT_TRUE(t.value->Is<ast::Array>());
+
+    auto* a = t.value->As<ast::Array>();
+    ASSERT_FALSE(a->IsRuntimeArray());
+    ASSERT_TRUE(a->type->Is<ast::F32>());
+    EXPECT_EQ(a->attributes.Length(), 0u);
+    EXPECT_EQ(t.value->source.range, (Source::Range{{1u, 1u}, {1u, 14u}}));
+
+    auto* size = a->count->As<ast::IntLiteralExpression>();
+    ASSERT_NE(size, nullptr);
+    EXPECT_EQ(size->value, 5);
+    EXPECT_EQ(size->suffix, ast::IntLiteralExpression::Suffix::kNone);
+}
+
+TEST_F(ParserImplTest, TypeDeclWithoutIdent_Array_SintLiteralSize) {
+    auto p = parser("array<f32, 5i>");
+    auto t = p->type_decl_without_ident();
+    EXPECT_TRUE(t.matched);
+    EXPECT_FALSE(t.errored);
+    ASSERT_NE(t.value, nullptr) << p->error();
+    ASSERT_FALSE(p->has_error());
+    ASSERT_TRUE(t.value->Is<ast::Array>());
+
+    auto* a = t.value->As<ast::Array>();
+    ASSERT_FALSE(a->IsRuntimeArray());
+    ASSERT_TRUE(a->type->Is<ast::F32>());
+    EXPECT_EQ(a->attributes.Length(), 0u);
+    EXPECT_EQ(t.value->source.range, (Source::Range{{1u, 1u}, {1u, 15u}}));
+
+    auto* size = a->count->As<ast::IntLiteralExpression>();
+    ASSERT_NE(size, nullptr);
+    EXPECT_EQ(size->value, 5);
+    EXPECT_EQ(size->suffix, ast::IntLiteralExpression::Suffix::kI);
+}
+
+TEST_F(ParserImplTest, TypeDeclWithoutIdent_Array_UintLiteralSize) {
+    auto p = parser("array<f32, 5u>");
+    auto t = p->type_decl_without_ident();
+    EXPECT_TRUE(t.matched);
+    EXPECT_FALSE(t.errored);
+    ASSERT_NE(t.value, nullptr) << p->error();
+    ASSERT_FALSE(p->has_error());
+    ASSERT_TRUE(t.value->Is<ast::Array>());
+
+    auto* a = t.value->As<ast::Array>();
+    ASSERT_FALSE(a->IsRuntimeArray());
+    ASSERT_TRUE(a->type->Is<ast::F32>());
+    EXPECT_EQ(a->attributes.Length(), 0u);
+    EXPECT_EQ(t.value->source.range, (Source::Range{{1u, 1u}, {1u, 15u}}));
+
+    auto* size = a->count->As<ast::IntLiteralExpression>();
+    ASSERT_NE(size, nullptr);
+    EXPECT_EQ(size->suffix, ast::IntLiteralExpression::Suffix::kU);
+}
+
+TEST_F(ParserImplTest, TypeDeclWithoutIdent_Array_ConstantSize) {
+    auto p = parser("array<f32, size>");
+    auto t = p->type_decl_without_ident();
+    EXPECT_TRUE(t.matched);
+    EXPECT_FALSE(t.errored);
+    ASSERT_NE(t.value, nullptr) << p->error();
+    ASSERT_FALSE(p->has_error());
+    ASSERT_TRUE(t.value->Is<ast::Array>());
+
+    auto* a = t.value->As<ast::Array>();
+    ASSERT_FALSE(a->IsRuntimeArray());
+    ASSERT_TRUE(a->type->Is<ast::F32>());
+    EXPECT_EQ(a->attributes.Length(), 0u);
+    EXPECT_EQ(t.value->source.range, (Source::Range{{1u, 1u}, {1u, 17u}}));
+
+    auto* count_expr = a->count->As<ast::IdentifierExpression>();
+    ASSERT_NE(count_expr, nullptr);
+    EXPECT_EQ(p->builder().Symbols().NameFor(count_expr->symbol), "size");
+}
+
+TEST_F(ParserImplTest, TypeDeclWithoutIdent_Array_ExpressionSize) {
+    auto p = parser("array<f32, size + 2>");
+    auto t = p->type_decl_without_ident();
+    EXPECT_TRUE(t.matched);
+    EXPECT_FALSE(t.errored);
+    ASSERT_NE(t.value, nullptr) << p->error();
+    ASSERT_FALSE(p->has_error());
+    ASSERT_TRUE(t.value->Is<ast::Array>());
+
+    auto* a = t.value->As<ast::Array>();
+    ASSERT_FALSE(a->IsRuntimeArray());
+    ASSERT_TRUE(a->type->Is<ast::F32>());
+    EXPECT_EQ(a->attributes.Length(), 0u);
+
+    ASSERT_TRUE(a->count->Is<ast::BinaryExpression>());
+    auto* count_expr = a->count->As<ast::BinaryExpression>();
+    EXPECT_EQ(ast::BinaryOp::kAdd, count_expr->op);
+
+    ASSERT_TRUE(count_expr->lhs->Is<ast::IdentifierExpression>());
+    auto* ident = count_expr->lhs->As<ast::IdentifierExpression>();
+    EXPECT_EQ(p->builder().Symbols().NameFor(ident->symbol), "size");
+
+    ASSERT_TRUE(count_expr->rhs->Is<ast::IntLiteralExpression>());
+    auto* val = count_expr->rhs->As<ast::IntLiteralExpression>();
+    EXPECT_EQ(2, static_cast<int32_t>(val->value));
+}
+
+TEST_F(ParserImplTest, TypeDeclWithoutIdent_Array_Runtime) {
+    auto p = parser("array<u32>");
+    auto t = p->type_decl_without_ident();
+    EXPECT_TRUE(t.matched);
+    EXPECT_FALSE(t.errored);
+    ASSERT_NE(t.value, nullptr) << p->error();
+    ASSERT_FALSE(p->has_error());
+    ASSERT_TRUE(t.value->Is<ast::Array>());
+
+    auto* a = t.value->As<ast::Array>();
+    ASSERT_TRUE(a->IsRuntimeArray());
+    ASSERT_TRUE(a->type->Is<ast::U32>());
+    EXPECT_EQ(t.value->source.range, (Source::Range{{1u, 1u}, {1u, 11u}}));
+}
+
+TEST_F(ParserImplTest, TypeDeclWithoutIdent_Array_Runtime_Vec) {
+    auto p = parser("array<vec4<u32>>");
+    auto t = p->type_decl_without_ident();
+    EXPECT_TRUE(t.matched);
+    EXPECT_FALSE(t.errored);
+    ASSERT_NE(t.value, nullptr) << p->error();
+    ASSERT_FALSE(p->has_error());
+    ASSERT_TRUE(t.value->Is<ast::Array>());
+
+    auto* a = t.value->As<ast::Array>();
+    ASSERT_TRUE(a->IsRuntimeArray());
+    ASSERT_TRUE(a->type->Is<ast::Vector>());
+    EXPECT_EQ(a->type->As<ast::Vector>()->width, 4u);
+    EXPECT_TRUE(a->type->As<ast::Vector>()->type->Is<ast::U32>());
+    EXPECT_EQ(t.value->source.range, (Source::Range{{1u, 1u}, {1u, 17u}}));
+}
+
+TEST_F(ParserImplTest, TypeDeclWithoutIdent_Array_BadSize) {
+    auto p = parser("array<f32, !>");
+    auto t = p->type_decl_without_ident();
+    EXPECT_TRUE(t.errored);
+    EXPECT_FALSE(t.matched);
+    ASSERT_EQ(t.value, nullptr);
+    ASSERT_TRUE(p->has_error());
+    ASSERT_EQ(p->error(), "1:13: unable to parse right side of ! expression");
+}
+
+TEST_F(ParserImplTest, TypeDeclWithoutIdent_Array_MissingSize) {
+    auto p = parser("array<f32,>");
+    auto t = p->type_decl_without_ident();
+    EXPECT_TRUE(t.errored);
+    EXPECT_FALSE(t.matched);
+    ASSERT_EQ(t.value, nullptr);
+    ASSERT_TRUE(p->has_error());
+    ASSERT_EQ(p->error(), "1:11: expected array size expression");
+}
+
+TEST_F(ParserImplTest, TypeDeclWithoutIdent_Array_MissingGreaterThan) {
+    auto p = parser("array<f32");
+    auto t = p->type_decl_without_ident();
+    EXPECT_TRUE(t.errored);
+    EXPECT_FALSE(t.matched);
+    ASSERT_EQ(t.value, nullptr);
+    ASSERT_TRUE(p->has_error());
+    ASSERT_EQ(p->error(), "1:10: expected '>' for array declaration");
+}
+
+TEST_F(ParserImplTest, TypeDeclWithoutIdent_Array_MissingComma) {
+    auto p = parser("array<f32 3>");
+    auto t = p->type_decl_without_ident();
+    EXPECT_TRUE(t.errored);
+    EXPECT_FALSE(t.matched);
+    ASSERT_EQ(t.value, nullptr);
+    ASSERT_TRUE(p->has_error());
+    ASSERT_EQ(p->error(), "1:11: expected '>' for array declaration");
+}
+
+struct MatrixData {
+    const char* input;
+    size_t columns;
+    size_t rows;
+    Source::Range range;
+};
+inline std::ostream& operator<<(std::ostream& out, MatrixData data) {
+    out << std::string(data.input);
+    return out;
+}
+
+class TypeDeclWithoutIdent_MatrixTest : public ParserImplTestWithParam<MatrixData> {};
+
+TEST_P(TypeDeclWithoutIdent_MatrixTest, Parse) {
+    auto params = GetParam();
+    auto p = parser(params.input);
+    auto t = p->type_decl_without_ident();
+    EXPECT_TRUE(t.matched);
+    EXPECT_FALSE(t.errored);
+    ASSERT_NE(t.value, nullptr) << p->error();
+    ASSERT_FALSE(p->has_error());
+    EXPECT_TRUE(t.value->Is<ast::Matrix>());
+    auto* mat = t.value->As<ast::Matrix>();
+    EXPECT_EQ(mat->rows, params.rows);
+    EXPECT_EQ(mat->columns, params.columns);
+    EXPECT_EQ(t.value->source.range, params.range);
+}
+INSTANTIATE_TEST_SUITE_P(ParserImplTest,
+                         TypeDeclWithoutIdent_MatrixTest,
+                         testing::Values(MatrixData{"mat2x2<f32>", 2, 2, {{1u, 1u}, {1u, 12u}}},
+                                         MatrixData{"mat2x3<f32>", 2, 3, {{1u, 1u}, {1u, 12u}}},
+                                         MatrixData{"mat2x4<f32>", 2, 4, {{1u, 1u}, {1u, 12u}}},
+                                         MatrixData{"mat3x2<f32>", 3, 2, {{1u, 1u}, {1u, 12u}}},
+                                         MatrixData{"mat3x3<f32>", 3, 3, {{1u, 1u}, {1u, 12u}}},
+                                         MatrixData{"mat3x4<f32>", 3, 4, {{1u, 1u}, {1u, 12u}}},
+                                         MatrixData{"mat4x2<f32>", 4, 2, {{1u, 1u}, {1u, 12u}}},
+                                         MatrixData{"mat4x3<f32>", 4, 3, {{1u, 1u}, {1u, 12u}}},
+                                         MatrixData{"mat4x4<f32>", 4, 4, {{1u, 1u}, {1u, 12u}}}));
+
+class TypeDeclWithoutIdent_MatrixMissingGreaterThanTest
+    : public ParserImplTestWithParam<MatrixData> {};
+
+TEST_P(TypeDeclWithoutIdent_MatrixMissingGreaterThanTest, Handles_Missing_GreaterThan) {
+    auto params = GetParam();
+    auto p = parser(params.input);
+    auto t = p->type_decl_without_ident();
+    EXPECT_TRUE(t.errored);
+    EXPECT_FALSE(t.matched);
+    ASSERT_EQ(t.value, nullptr);
+    ASSERT_TRUE(p->has_error());
+    ASSERT_EQ(p->error(), "1:11: expected '>' for matrix");
+}
+INSTANTIATE_TEST_SUITE_P(ParserImplTest,
+                         TypeDeclWithoutIdent_MatrixMissingGreaterThanTest,
+                         testing::Values(MatrixData{"mat2x2<f32", 2, 2, {}},
+                                         MatrixData{"mat2x3<f32", 2, 3, {}},
+                                         MatrixData{"mat2x4<f32", 2, 4, {}},
+                                         MatrixData{"mat3x2<f32", 3, 2, {}},
+                                         MatrixData{"mat3x3<f32", 3, 3, {}},
+                                         MatrixData{"mat3x4<f32", 3, 4, {}},
+                                         MatrixData{"mat4x2<f32", 4, 2, {}},
+                                         MatrixData{"mat4x3<f32", 4, 3, {}},
+                                         MatrixData{"mat4x4<f32", 4, 4, {}}));
+
+class TypeDeclWithoutIdent_MatrixMissingType : public ParserImplTestWithParam<MatrixData> {};
+
+TEST_P(TypeDeclWithoutIdent_MatrixMissingType, Handles_Missing_Type) {
+    auto params = GetParam();
+    auto p = parser(params.input);
+    auto t = p->type_decl_without_ident();
+    EXPECT_TRUE(t.errored);
+    EXPECT_FALSE(t.matched);
+    ASSERT_EQ(t.value, nullptr);
+    ASSERT_TRUE(p->has_error());
+    ASSERT_EQ(p->error(), "1:8: invalid type for matrix");
+}
+INSTANTIATE_TEST_SUITE_P(ParserImplTest,
+                         TypeDeclWithoutIdent_MatrixMissingType,
+                         testing::Values(MatrixData{"mat2x2<>", 2, 2, {}},
+                                         MatrixData{"mat2x3<>", 2, 3, {}},
+                                         MatrixData{"mat2x4<>", 2, 4, {}},
+                                         MatrixData{"mat3x2<>", 3, 2, {}},
+                                         MatrixData{"mat3x3<>", 3, 3, {}},
+                                         MatrixData{"mat3x4<>", 3, 4, {}},
+                                         MatrixData{"mat4x2<>", 4, 2, {}},
+                                         MatrixData{"mat4x3<>", 4, 3, {}},
+                                         MatrixData{"mat4x4<>", 4, 4, {}}));
+
+TEST_F(ParserImplTest, TypeDeclWithoutIdent_Sampler) {
+    auto p = parser("sampler");
+
+    auto t = p->type_decl_without_ident();
+    EXPECT_TRUE(t.matched);
+    EXPECT_FALSE(t.errored);
+    ASSERT_NE(t.value, nullptr) << p->error();
+    ASSERT_TRUE(t.value->Is<ast::Sampler>());
+    ASSERT_FALSE(t.value->As<ast::Sampler>()->IsComparison());
+    EXPECT_EQ(t.value->source.range, (Source::Range{{1u, 1u}, {1u, 8u}}));
+}
+
+TEST_F(ParserImplTest, TypeDeclWithoutIdent_Texture) {
+    auto p = parser("texture_cube<f32>");
+
+    auto t = p->type_decl_without_ident();
+    EXPECT_TRUE(t.matched);
+    EXPECT_FALSE(t.errored);
+    ASSERT_NE(t.value, nullptr);
+    ASSERT_TRUE(t.value->Is<ast::Texture>());
+    ASSERT_TRUE(t.value->Is<ast::SampledTexture>());
+    ASSERT_TRUE(t.value->As<ast::SampledTexture>()->type->Is<ast::F32>());
+    EXPECT_EQ(t.value->source.range, (Source::Range{{1u, 1u}, {1u, 18u}}));
+}
+
+}  // namespace
+}  // namespace tint::reader::wgsl
diff --git a/src/tint/reader/wgsl/parser_impl_variable_stmt_test.cc b/src/tint/reader/wgsl/parser_impl_variable_stmt_test.cc
index c370262..3e60acd 100644
--- a/src/tint/reader/wgsl/parser_impl_variable_stmt_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_variable_stmt_test.cc
@@ -153,6 +153,32 @@
     ASSERT_EQ(e->source.range.end.column, 6u);
 }
 
+TEST_F(ParserImplTest, VariableStmt_Let_ComplexExpression) {
+    auto p = parser("let x = collide + collide_1;");
+    // Parse as `statement` to validate the `;` at the end so we know we parsed the whole expression
+    auto e = p->statement();
+    EXPECT_TRUE(e.matched);
+    EXPECT_FALSE(e.errored);
+    EXPECT_FALSE(p->has_error()) << p->error();
+    ASSERT_NE(e.value, nullptr);
+    ASSERT_TRUE(e->Is<ast::VariableDeclStatement>());
+
+    auto* decl = e->As<ast::VariableDeclStatement>();
+    ASSERT_NE(decl->variable->constructor, nullptr);
+
+    ASSERT_TRUE(decl->variable->constructor->Is<ast::BinaryExpression>());
+    auto* expr = decl->variable->constructor->As<ast::BinaryExpression>();
+    EXPECT_EQ(expr->op, ast::BinaryOp::kAdd);
+
+    ASSERT_TRUE(expr->lhs->Is<ast::IdentifierExpression>());
+    auto* ident = expr->lhs->As<ast::IdentifierExpression>();
+    EXPECT_EQ(ident->symbol, p->builder().Symbols().Get("collide"));
+
+    ASSERT_TRUE(expr->rhs->Is<ast::IdentifierExpression>());
+    ident = expr->rhs->As<ast::IdentifierExpression>();
+    EXPECT_EQ(ident->symbol, p->builder().Symbols().Get("collide_1"));
+}
+
 TEST_F(ParserImplTest, VariableStmt_Let_MissingEqual) {
     auto p = parser("let a : i32 1");
     auto e = p->variable_statement();
diff --git a/src/tint/reader/wgsl/parser_impl_vec_mat_prefix_test.cc b/src/tint/reader/wgsl/parser_impl_vec_mat_prefix_test.cc
new file mode 100644
index 0000000..d6204d7
--- /dev/null
+++ b/src/tint/reader/wgsl/parser_impl_vec_mat_prefix_test.cc
@@ -0,0 +1,103 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/reader/wgsl/parser_impl_test_helper.h"
+
+namespace tint::reader::wgsl {
+namespace {
+
+TEST_F(ParserImplTest, VecPrefix_Vec2) {
+    auto p = parser("vec2");
+    auto t = p->vec_prefix();
+    EXPECT_TRUE(t.matched);
+    EXPECT_FALSE(t.errored);
+    ASSERT_FALSE(p->has_error()) << p->error();
+
+    EXPECT_EQ(2u, t.value);
+}
+
+TEST_F(ParserImplTest, VecPrefix_Vec3) {
+    auto p = parser("vec3");
+    auto t = p->vec_prefix();
+    EXPECT_TRUE(t.matched);
+    EXPECT_FALSE(t.errored);
+    ASSERT_FALSE(p->has_error()) << p->error();
+
+    EXPECT_EQ(3u, t.value);
+}
+
+TEST_F(ParserImplTest, VecPrefix_Vec4) {
+    auto p = parser("vec4");
+    auto t = p->vec_prefix();
+    EXPECT_TRUE(t.matched);
+    EXPECT_FALSE(t.errored);
+    ASSERT_FALSE(p->has_error()) << p->error();
+
+    EXPECT_EQ(4u, t.value);
+}
+
+TEST_F(ParserImplTest, VecPrefix_NoMatch) {
+    auto p = parser("mat2x2");
+    auto t = p->vec_prefix();
+    EXPECT_FALSE(t.matched);
+    EXPECT_FALSE(t.errored);
+    ASSERT_FALSE(p->has_error()) << p->error();
+
+    EXPECT_EQ(0u, t.value);
+}
+
+struct MatData {
+    std::string name;
+    uint32_t columns;
+    uint32_t rows;
+};
+class MatPrefixTest : public ParserImplTestWithParam<MatData> {};
+TEST_P(MatPrefixTest, Parse) {
+    auto params = GetParam();
+
+    auto p = parser(params.name);
+    auto t = p->mat_prefix();
+    EXPECT_TRUE(t.matched);
+    EXPECT_FALSE(t.errored);
+    ASSERT_FALSE(p->has_error()) << p->error();
+
+    auto dims = t.value;
+    EXPECT_EQ(params.columns, dims.columns);
+    EXPECT_EQ(params.rows, dims.rows);
+}
+INSTANTIATE_TEST_SUITE_P(ParserImplTest,
+                         MatPrefixTest,
+                         testing::Values(MatData{"mat2x2", 2, 2},
+                                         MatData{"mat2x3", 2, 3},
+                                         MatData{"mat2x4", 2, 4},
+                                         MatData{"mat3x2", 3, 2},
+                                         MatData{"mat3x3", 3, 3},
+                                         MatData{"mat3x4", 3, 4},
+                                         MatData{"mat4x2", 4, 2},
+                                         MatData{"mat4x3", 4, 3},
+                                         MatData{"mat4x4", 4, 4}));
+
+TEST_F(ParserImplTest, MatPrefix_NoMatch) {
+    auto p = parser("vec2");
+    auto t = p->mat_prefix();
+    EXPECT_FALSE(t.matched);
+    EXPECT_FALSE(t.errored);
+    ASSERT_FALSE(p->has_error()) << p->error();
+
+    EXPECT_EQ(0u, t.value.columns);
+    EXPECT_EQ(0u, t.value.rows);
+}
+
+}  // namespace
+}  // namespace tint::reader::wgsl
diff --git a/src/tint/transform/promote_side_effects_to_decl_test.cc b/src/tint/transform/promote_side_effects_to_decl_test.cc
index 937b59e..2cd7401 100644
--- a/src/tint/transform/promote_side_effects_to_decl_test.cc
+++ b/src/tint/transform/promote_side_effects_to_decl_test.cc
@@ -1639,7 +1639,7 @@
   var b = true;
   var c = true;
   var d = true;
-  let r = b && a(0) || c && a(1) && c && d || a(2);
+  let r = (b && a(0)) || (c && a(1) && c && d) || a(2);
 }
 )";
 
@@ -1869,7 +1869,7 @@
 
 fn f() {
   var b = true;
-  let r = bool(a(0)) && bool(a(1) && b) || bool(a(2) && a(3));
+  let r = (bool(a(0)) && bool(a(1) && b)) || bool(a(2) && a(3));
 }
 )";
 
@@ -1916,7 +1916,7 @@
 
 fn f() {
   var b = true;
-  let r = bool(a(0)) && bool(a(1)) || b;
+  let r = (bool(a(0)) && bool(a(1))) || b;
 }
 )";
 
@@ -1994,7 +1994,7 @@
 
 fn f() {
   var b = true;
-  let r = all(a(0)) && all(a(1) && b) || all(a(2) && a(3));
+  let r = (all(a(0)) && all(a(1) && b)) || all(a(2) && a(3));
 }
 )";