Initial commit
diff --git a/src/reader/reader.cc b/src/reader/reader.cc
new file mode 100644
index 0000000..6d90273
--- /dev/null
+++ b/src/reader/reader.cc
@@ -0,0 +1,25 @@
+// 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/reader/reader.h"
+
+namespace tint {
+namespace reader {
+
+Reader::Reader() = default;
+
+Reader::~Reader() = default;
+
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/reader.h b/src/reader/reader.h
new file mode 100644
index 0000000..8aa48d5
--- /dev/null
+++ b/src/reader/reader.h
@@ -0,0 +1,57 @@
+// 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.
+
+#ifndef SRC_READER_READER_H_
+#define SRC_READER_READER_H_
+
+#include <string>
+
+#include "src/ast/module.h"
+
+namespace tint {
+namespace reader {
+
+/// Base class for input readers
+class Reader {
+ public:
+  virtual ~Reader();
+
+  /// Parses the input data
+  /// @returns true if the parse was successful
+  virtual bool Parse() = 0;
+
+  /// @returns true if an error was encountered
+  bool has_error() const { return error_.size() > 0; }
+  /// @returns the parser error string
+  const std::string& error() const { return error_; }
+
+  /// @returns the module. The module in the parser will be reset after this.
+  virtual ast::Module module() = 0;
+
+ protected:
+  /// Constructor
+  Reader();
+
+  /// Sets the error string
+  /// @param msg the error message
+  void set_error(const std::string& msg) { error_ = msg; }
+
+  /// An error message, if an error was encountered
+  std::string error_;
+};
+
+}  // namespace reader
+}  // namespace tint
+
+#endif  // SRC_READER_READER_H_
diff --git a/src/reader/spv/parser.cc b/src/reader/spv/parser.cc
new file mode 100644
index 0000000..3c56d39
--- /dev/null
+++ b/src/reader/spv/parser.cc
@@ -0,0 +1,37 @@
+// 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/reader/spv/parser.h"
+
+#include <utility>
+
+namespace tint {
+namespace reader {
+namespace spv {
+
+Parser::Parser(const std::vector<uint32_t>&) : Reader() {}
+
+Parser::~Parser() = default;
+
+bool Parser::Parse() {
+  return false;
+}
+
+ast::Module Parser::module() {
+  return std::move(module_);
+}
+
+}  // namespace spv
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/spv/parser.h b/src/reader/spv/parser.h
new file mode 100644
index 0000000..c47d14f
--- /dev/null
+++ b/src/reader/spv/parser.h
@@ -0,0 +1,51 @@
+// 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.
+
+#ifndef SRC_READER_SPV_PARSER_H_
+#define SRC_READER_SPV_PARSER_H_
+
+#include <vector>
+
+#include "src/reader/reader.h"
+
+namespace tint {
+namespace reader {
+namespace spv {
+
+class ParserImpl;
+
+/// Parser for SPIR-V source data
+class Parser : public Reader {
+ public:
+  /// Creates a new parser
+  /// @param input the input data to parse
+  explicit Parser(const std::vector<uint32_t>& input);
+  ~Parser() override;
+
+  /// Run the parser
+  /// @returns true if the parse was successful, false otherwise.
+  bool Parse() override;
+
+  /// @returns the module. The module in the parser will be reset after this.
+  ast::Module module() override;
+
+ private:
+  ast::Module module_;
+};
+
+}  // namespace spv
+}  // namespace reader
+}  // namespace tint
+
+#endif  // SRC_READER_SPV_PARSER_H_
diff --git a/src/reader/wgsl/lexer.cc b/src/reader/wgsl/lexer.cc
new file mode 100644
index 0000000..903f9b1
--- /dev/null
+++ b/src/reader/wgsl/lexer.cc
@@ -0,0 +1,691 @@
+// 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/reader/wgsl/lexer.h"
+
+#include <ctype.h>
+#include <errno.h>
+#include <stdlib.h>
+
+#include <limits>
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+namespace {
+
+bool is_whitespace(char c) {
+  return std::isspace(c);
+}
+
+}  // namespace
+
+Lexer::Lexer(const std::string& input)
+    : input_(input), len_(static_cast<uint32_t>(input.size())) {}
+
+Lexer::~Lexer() = default;
+
+Token Lexer::next() {
+  skip_whitespace();
+  skip_comments();
+
+  if (is_eof()) {
+    return {Token::Type::kEOF, make_source()};
+  }
+
+  auto t = try_hex_integer();
+  if (!t.IsUninitialized()) {
+    return t;
+  }
+
+  t = try_float();
+  if (!t.IsUninitialized()) {
+    return t;
+  }
+
+  t = try_integer();
+  if (!t.IsUninitialized()) {
+    return t;
+  }
+
+  t = try_string();
+  if (!t.IsUninitialized()) {
+    return t;
+  }
+
+  t = try_punctuation();
+  if (!t.IsUninitialized()) {
+    return t;
+  }
+
+  t = try_ident();
+  if (!t.IsUninitialized()) {
+    return t;
+  }
+
+  return {Token::Type::kError, make_source(), "invalid character found"};
+}
+
+Source Lexer::make_source() const {
+  return Source{line_, column_};
+}
+
+bool Lexer::is_eof() const {
+  return pos_ >= len_;
+}
+
+bool Lexer::is_alpha(char ch) const {
+  return std::isalpha(ch) || ch == '_';
+}
+
+bool Lexer::is_digit(char ch) const {
+  return std::isdigit(ch);
+}
+
+bool Lexer::is_alphanum(char ch) const {
+  return is_alpha(ch) || is_digit(ch);
+}
+
+bool Lexer::is_hex(char ch) const {
+  return std::isxdigit(ch);
+}
+
+bool Lexer::matches(size_t pos, const std::string& substr) {
+  if (pos >= input_.size())
+    return false;
+  return input_.substr(pos, substr.size()) == substr;
+}
+
+void Lexer::skip_whitespace() {
+  for (;;) {
+    auto pos = pos_;
+    while (!is_eof() && is_whitespace(input_[pos_])) {
+      if (matches(pos_, "\n")) {
+        pos_++;
+        line_++;
+        column_ = 1;
+        continue;
+      }
+
+      pos_++;
+      column_++;
+    }
+
+    skip_comments();
+
+    // If the cursor didn't advance we didn't remove any whitespace
+    // so we're done.
+    if (pos == pos_)
+      break;
+  }
+}
+
+void Lexer::skip_comments() {
+  if (!matches(pos_, "#")) {
+    return;
+  }
+
+  while (!is_eof() && !matches(pos_, "\n")) {
+    pos_++;
+    column_++;
+  }
+}
+
+Token Lexer::try_float() {
+  auto start = pos_;
+  auto end = pos_;
+
+  auto source = make_source();
+
+  if (matches(end, "-")) {
+    end++;
+  }
+  while (end < len_ && is_digit(input_[end])) {
+    end++;
+  }
+
+  if (end >= len_ || !matches(end, ".")) {
+    return {};
+  }
+  end++;
+
+  while (end < len_ && is_digit(input_[end])) {
+    end++;
+  }
+
+  // Parse the exponent if one exists
+  if (end < len_ && matches(end, "e")) {
+    end++;
+    if (end < len_ && (matches(end, "+") || matches(end, "-"))) {
+      end++;
+    }
+
+    auto exp_start = end;
+    while (end < len_ && isdigit(input_[end])) {
+      end++;
+    }
+
+    // Must have an exponent
+    if (exp_start == end)
+      return {};
+  }
+
+  auto str = input_.substr(start, end - start);
+  if (str == "." || str == "-.")
+    return {};
+
+  pos_ = end;
+  column_ += (end - start);
+
+  auto res = strtod(input_.c_str() + start, nullptr);
+  // This handles if the number is a really small in the exponent
+  if (res > 0 && res < static_cast<double>(std::numeric_limits<float>::min())) {
+    return {Token::Type::kError, source, "f32 (" + str + " too small"};
+  }
+  // This handles if the number is really large negative number
+  if (res < static_cast<double>(std::numeric_limits<float>::lowest())) {
+    return {Token::Type::kError, source, "f32 (" + str + ") too small"};
+  }
+  if (res > static_cast<double>(std::numeric_limits<float>::max())) {
+    return {Token::Type::kError, source, "f32 (" + str + ") too large"};
+  }
+
+  return {source, static_cast<float>(res)};
+}
+
+Token Lexer::build_token_from_int_if_possible(const Source& source,
+                                              size_t start,
+                                              size_t end,
+                                              int32_t base) {
+  auto res = strtoll(input_.c_str() + start, nullptr, base);
+  if (matches(pos_, "u")) {
+    if (static_cast<uint64_t>(res) >
+        static_cast<uint64_t>(std::numeric_limits<uint32_t>::max())) {
+      return {Token::Type::kError, source,
+              "u32 (" + input_.substr(start, end - start) + ") too large"};
+    }
+    return {source, static_cast<uint32_t>(res)};
+  }
+
+  if (res < static_cast<int64_t>(std::numeric_limits<int32_t>::min())) {
+    return {Token::Type::kError, source,
+            "i32 (" + input_.substr(start, end - start) + ") too small"};
+  }
+  if (res > static_cast<int64_t>(std::numeric_limits<int32_t>::max())) {
+    return {Token::Type::kError, source,
+            "i32 (" + input_.substr(start, end - start) + ") too large"};
+  }
+  return {source, static_cast<int32_t>(res)};
+}
+
+Token Lexer::try_hex_integer() {
+  auto start = pos_;
+  auto end = pos_;
+
+  auto source = make_source();
+
+  if (matches(end, "-")) {
+    end++;
+  }
+  if (!matches(end, "0x")) {
+    return Token();
+  }
+  end += 2;
+
+  while (!is_eof() && is_hex(input_[end])) {
+    end += 1;
+  }
+
+  pos_ = end;
+  column_ += (end - start);
+
+  return build_token_from_int_if_possible(source, start, end, 16);
+}
+
+Token Lexer::try_integer() {
+  auto start = pos_;
+  auto end = start;
+
+  auto source = make_source();
+
+  if (matches(end, "-")) {
+    end++;
+  }
+  if (end >= len_ || !is_digit(input_[end])) {
+    return {};
+  }
+
+  auto first = end;
+  while (end < len_ && is_digit(input_[end])) {
+    end++;
+  }
+
+  // If the first digit is a zero this must only be zero as leading zeros
+  // are not allowed.
+  if (input_[first] == '0' && (end - first != 1))
+    return {};
+
+  pos_ = end;
+  column_ += (end - start);
+
+  return build_token_from_int_if_possible(source, start, end, 10);
+}
+
+Token Lexer::try_ident() {
+  // Must begin with an a-zA-Z_
+  if (!is_alpha(input_[pos_])) {
+    return {};
+  }
+
+  auto source = make_source();
+
+  auto s = pos_;
+  while (!is_eof() && is_alphanum(input_[pos_])) {
+    pos_++;
+    column_++;
+  }
+
+  auto str = input_.substr(s, pos_ - s);
+  auto t = check_reserved(source, str);
+  if (!t.IsUninitialized()) {
+    return t;
+  }
+
+  t = check_keyword(source, str);
+  if (!t.IsUninitialized()) {
+    return t;
+  }
+
+  return {Token::Type::kIdentifier, source, str};
+}
+
+Token Lexer::try_string() {
+  if (!matches(pos_, R"(")"))
+    return {};
+
+  auto source = make_source();
+
+  pos_++;
+  auto start = pos_;
+  while (pos_ < len_ && !matches(pos_, R"(")")) {
+    pos_++;
+  }
+  auto end = pos_;
+  pos_++;
+  column_ += (pos_ - start) + 1;
+
+  return {Token::Type::kStringLiteral, source,
+          input_.substr(start, end - start)};
+}
+
+Token Lexer::try_punctuation() {
+  auto source = make_source();
+  auto type = Token::Type::kUninitialized;
+
+  if (matches(pos_, "[[")) {
+    type = Token::Type::kAttrLeft;
+    pos_ += 2;
+    column_ += 2;
+  } else if (matches(pos_, "]]")) {
+    type = Token::Type::kAttrRight;
+    pos_ += 2;
+    column_ += 2;
+  } else if (matches(pos_, "(")) {
+    type = Token::Type::kParenLeft;
+    pos_ += 1;
+    column_ += 1;
+  } else if (matches(pos_, ")")) {
+    type = Token::Type::kParenRight;
+    pos_ += 1;
+    column_ += 1;
+  } else if (matches(pos_, "[")) {
+    type = Token::Type::kBraceLeft;
+    pos_ += 1;
+    column_ += 1;
+  } else if (matches(pos_, "]")) {
+    type = Token::Type::kBraceRight;
+    pos_ += 1;
+    column_ += 1;
+  } else if (matches(pos_, "{")) {
+    type = Token::Type::kBracketLeft;
+    pos_ += 1;
+    column_ += 1;
+  } else if (matches(pos_, "}")) {
+    type = Token::Type::kBracketRight;
+    pos_ += 1;
+    column_ += 1;
+  } else if (matches(pos_, "&&")) {
+    type = Token::Type::kAndAnd;
+    pos_ += 2;
+    column_ += 2;
+  } else if (matches(pos_, "&")) {
+    type = Token::Type::kAnd;
+    pos_ += 1;
+    column_ += 1;
+  } else if (matches(pos_, "/")) {
+    type = Token::Type::kForwardSlash;
+    pos_ += 1;
+    column_ += 1;
+  } else if (matches(pos_, "!=")) {
+    type = Token::Type::kNotEqual;
+    pos_ += 2;
+    column_ += 2;
+  } else if (matches(pos_, "!")) {
+    type = Token::Type::kBang;
+    pos_ += 1;
+    column_ += 1;
+  } else if (matches(pos_, "::")) {
+    type = Token::Type::kNamespace;
+    pos_ += 2;
+    column_ += 2;
+  } else if (matches(pos_, ":")) {
+    type = Token::Type::kColon;
+    pos_ += 1;
+    column_ += 1;
+  } else if (matches(pos_, ",")) {
+    type = Token::Type::kComma;
+    pos_ += 1;
+    column_ += 1;
+  } else if (matches(pos_, "==")) {
+    type = Token::Type::kEqualEqual;
+    pos_ += 2;
+    column_ += 2;
+  } else if (matches(pos_, "=")) {
+    type = Token::Type::kEqual;
+    pos_ += 1;
+    column_ += 1;
+  } else if (matches(pos_, ">=")) {
+    type = Token::Type::kGreaterThanEqual;
+    pos_ += 2;
+    column_ += 2;
+  } else if (matches(pos_, ">")) {
+    type = Token::Type::kGreaterThan;
+    pos_ += 1;
+    column_ += 1;
+  } else if (matches(pos_, "<=")) {
+    type = Token::Type::kLessThanEqual;
+    pos_ += 2;
+    column_ += 2;
+  } else if (matches(pos_, "<")) {
+    type = Token::Type::kLessThan;
+    pos_ += 1;
+    column_ += 1;
+  } else if (matches(pos_, "%")) {
+    type = Token::Type::kMod;
+    pos_ += 1;
+    column_ += 1;
+  } else if (matches(pos_, "->")) {
+    type = Token::Type::kArrow;
+    pos_ += 2;
+    column_ += 2;
+  } else if (matches(pos_, "-")) {
+    type = Token::Type::kMinus;
+    pos_ += 1;
+    column_ += 1;
+  } else if (matches(pos_, ".")) {
+    type = Token::Type::kPeriod;
+    pos_ += 1;
+    column_ += 1;
+  } else if (matches(pos_, "+")) {
+    type = Token::Type::kPlus;
+    pos_ += 1;
+    column_ += 1;
+  } else if (matches(pos_, "||")) {
+    type = Token::Type::kOrOr;
+    pos_ += 2;
+    column_ += 2;
+  } else if (matches(pos_, "|")) {
+    type = Token::Type::kOr;
+    pos_ += 1;
+    column_ += 1;
+  } else if (matches(pos_, ";")) {
+    type = Token::Type::kSemicolon;
+    pos_ += 1;
+    column_ += 1;
+  } else if (matches(pos_, "*")) {
+    type = Token::Type::kStar;
+    pos_ += 1;
+    column_ += 1;
+  } else if (matches(pos_, "^")) {
+    type = Token::Type::kXor;
+    pos_ += 1;
+    column_ += 1;
+  }
+
+  return {type, source};
+}
+
+Token Lexer::check_keyword(const Source& source, const std::string& str) {
+  if (str == "all")
+    return {Token::Type::kAll, source, "all"};
+  if (str == "any")
+    return {Token::Type::kAny, source, "any"};
+  if (str == "array")
+    return {Token::Type::kArray, source, "array"};
+  if (str == "as")
+    return {Token::Type::kAs, source, "as"};
+  if (str == "binding")
+    return {Token::Type::kBinding, source, "binding"};
+  if (str == "block")
+    return {Token::Type::kBlock, source, "block"};
+  if (str == "bool")
+    return {Token::Type::kBool, source, "bool"};
+  if (str == "break")
+    return {Token::Type::kBreak, source, "break"};
+  if (str == "builtin")
+    return {Token::Type::kBuiltin, source, "builtin"};
+  if (str == "case")
+    return {Token::Type::kCase, source, "case"};
+  if (str == "cast")
+    return {Token::Type::kCast, source, "cast"};
+  if (str == "compute")
+    return {Token::Type::kCompute, source, "compute"};
+  if (str == "const")
+    return {Token::Type::kConst, source, "const"};
+  if (str == "continue")
+    return {Token::Type::kContinue, source, "continue"};
+  if (str == "continuing")
+    return {Token::Type::kContinuing, source, "continuing"};
+  if (str == "coarse")
+    return {Token::Type::kCoarse, source, "coarse"};
+  if (str == "default")
+    return {Token::Type::kDefault, source, "default"};
+  if (str == "dot")
+    return {Token::Type::kDot, source, "dot"};
+  if (str == "dpdx")
+    return {Token::Type::kDpdx, source, "dpdx"};
+  if (str == "dpdy")
+    return {Token::Type::kDpdy, source, "dpdy"};
+  if (str == "else")
+    return {Token::Type::kElse, source, "else"};
+  if (str == "elseif")
+    return {Token::Type::kElseIf, source, "elseif"};
+  if (str == "entry_point")
+    return {Token::Type::kEntryPoint, source, "entry_point"};
+  if (str == "f32")
+    return {Token::Type::kF32, source, "f32"};
+  if (str == "fallthrough")
+    return {Token::Type::kFallthrough, source, "fallthrough"};
+  if (str == "false")
+    return {Token::Type::kFalse, source, "false"};
+  if (str == "fine")
+    return {Token::Type::kFine, source, "fine"};
+  if (str == "fn")
+    return {Token::Type::kFn, source, "fn"};
+  if (str == "frag_coord")
+    return {Token::Type::kFragCoord, source, "frag_coord"};
+  if (str == "frag_depth")
+    return {Token::Type::kFragDepth, source, "frag_depth"};
+  if (str == "fragment")
+    return {Token::Type::kFragment, source, "fragment"};
+  if (str == "front_facing")
+    return {Token::Type::kFrontFacing, source, "front_facing"};
+  if (str == "function")
+    return {Token::Type::kFunction, source, "function"};
+  if (str == "fwidth")
+    return {Token::Type::kFwidth, source, "fwidth"};
+  if (str == "global_invocation_id")
+    return {Token::Type::kGlobalInvocationId, source, "global_invocation_id"};
+  if (str == "i32")
+    return {Token::Type::kI32, source, "i32"};
+  if (str == "if")
+    return {Token::Type::kIf, source, "if"};
+  if (str == "image")
+    return {Token::Type::kImage, source, "image"};
+  if (str == "import")
+    return {Token::Type::kImport, source, "import"};
+  if (str == "in")
+    return {Token::Type::kIn, source, "in"};
+  if (str == "instance_idx")
+    return {Token::Type::kInstanceIdx, source, "instance_idx"};
+  if (str == "is_nan")
+    return {Token::Type::kIsNan, source, "is_nan"};
+  if (str == "is_inf")
+    return {Token::Type::kIsInf, source, "is_inf"};
+  if (str == "is_finite")
+    return {Token::Type::kIsFinite, source, "is_finite"};
+  if (str == "is_normal")
+    return {Token::Type::kIsNormal, source, "is_normal"};
+  if (str == "kill")
+    return {Token::Type::kKill, source, "kill"};
+  if (str == "local_invocation_id")
+    return {Token::Type::kLocalInvocationId, source, "local_invocation_id"};
+  if (str == "local_invocation_idx")
+    return {Token::Type::kLocalInvocationIdx, source, "local_invocation_idx"};
+  if (str == "location")
+    return {Token::Type::kLocation, source, "location"};
+  if (str == "loop")
+    return {Token::Type::kLoop, source, "loop"};
+  if (str == "mat2x2")
+    return {Token::Type::kMat2x2, source, "mat2x2"};
+  if (str == "mat2x3")
+    return {Token::Type::kMat2x3, source, "mat2x3"};
+  if (str == "mat2x4")
+    return {Token::Type::kMat2x4, source, "mat2x4"};
+  if (str == "mat3x2")
+    return {Token::Type::kMat3x2, source, "mat3x2"};
+  if (str == "mat3x3")
+    return {Token::Type::kMat3x3, source, "mat3x3"};
+  if (str == "mat3x4")
+    return {Token::Type::kMat3x4, source, "mat3x4"};
+  if (str == "mat4x2")
+    return {Token::Type::kMat4x2, source, "mat4x2"};
+  if (str == "mat4x3")
+    return {Token::Type::kMat4x3, source, "mat4x3"};
+  if (str == "mat4x4")
+    return {Token::Type::kMat4x4, source, "mat4x4"};
+  if (str == "nop")
+    return {Token::Type::kNop, source, "nop"};
+  if (str == "num_workgroups")
+    return {Token::Type::kNumWorkgroups, source, "num_workgroups"};
+  if (str == "offset")
+    return {Token::Type::kOffset, source, "offset"};
+  if (str == "out")
+    return {Token::Type::kOut, source, "out"};
+  if (str == "outer_product")
+    return {Token::Type::kOuterProduct, source, "outer_product"};
+  if (str == "position")
+    return {Token::Type::kPosition, source, "position"};
+  if (str == "premerge")
+    return {Token::Type::kPremerge, source, "premerge"};
+  if (str == "private")
+    return {Token::Type::kPrivate, source, "private"};
+  if (str == "ptr")
+    return {Token::Type::kPtr, source, "ptr"};
+  if (str == "push_constant")
+    return {Token::Type::kPushConstant, source, "push_constant"};
+  if (str == "regardless")
+    return {Token::Type::kRegardless, source, "regardless"};
+  if (str == "return")
+    return {Token::Type::kReturn, source, "return"};
+  if (str == "set")
+    return {Token::Type::kSet, source, "set"};
+  if (str == "storage_buffer")
+    return {Token::Type::kStorageBuffer, source, "storage_buffer"};
+  if (str == "struct")
+    return {Token::Type::kStruct, source, "struct"};
+  if (str == "switch")
+    return {Token::Type::kSwitch, source, "switch"};
+  if (str == "true")
+    return {Token::Type::kTrue, source, "true"};
+  if (str == "type")
+    return {Token::Type::kType, source, "type"};
+  if (str == "u32")
+    return {Token::Type::kU32, source, "u32"};
+  if (str == "uniform")
+    return {Token::Type::kUniform, source, "uniform"};
+  if (str == "uniform_constant")
+    return {Token::Type::kUniformConstant, source, "uniform_constant"};
+  if (str == "unless")
+    return {Token::Type::kUnless, source, "unless"};
+  if (str == "var")
+    return {Token::Type::kVar, source, "var"};
+  if (str == "vec2")
+    return {Token::Type::kVec2, source, "vec2"};
+  if (str == "vec3")
+    return {Token::Type::kVec3, source, "vec3"};
+  if (str == "vec4")
+    return {Token::Type::kVec4, source, "vec4"};
+  if (str == "vertex")
+    return {Token::Type::kVertex, source, "vertex"};
+  if (str == "vertex_idx")
+    return {Token::Type::kVertexIdx, source, "vertex_idx"};
+  if (str == "void")
+    return {Token::Type::kVoid, source, "void"};
+  if (str == "workgroup")
+    return {Token::Type::kWorkgroup, source, "workgroup"};
+  if (str == "workgroup_size")
+    return {Token::Type::kWorkgroupSize, source, "workgroup_size"};
+
+  return {};
+}
+
+Token Lexer::check_reserved(const Source& source, const std::string& str) {
+  if (str == "asm")
+    return {Token::Type::kReservedKeyword, source, "asm"};
+  if (str == "bf16")
+    return {Token::Type::kReservedKeyword, source, "bf16"};
+  if (str == "do")
+    return {Token::Type::kReservedKeyword, source, "do"};
+  if (str == "enum")
+    return {Token::Type::kReservedKeyword, source, "enum"};
+  if (str == "f16")
+    return {Token::Type::kReservedKeyword, source, "f16"};
+  if (str == "f64")
+    return {Token::Type::kReservedKeyword, source, "f64"};
+  if (str == "for")
+    return {Token::Type::kReservedKeyword, source, "for"};
+  if (str == "i8")
+    return {Token::Type::kReservedKeyword, source, "i8"};
+  if (str == "i16")
+    return {Token::Type::kReservedKeyword, source, "i16"};
+  if (str == "i64")
+    return {Token::Type::kReservedKeyword, source, "i64"};
+  if (str == "let")
+    return {Token::Type::kReservedKeyword, source, "let"};
+  if (str == "typedef")
+    return {Token::Type::kReservedKeyword, source, "typedef"};
+  if (str == "u8")
+    return {Token::Type::kReservedKeyword, source, "u8"};
+  if (str == "u16")
+    return {Token::Type::kReservedKeyword, source, "u16"};
+  if (str == "u64")
+    return {Token::Type::kReservedKeyword, source, "u64"};
+
+  return {};
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/lexer.h b/src/reader/wgsl/lexer.h
new file mode 100644
index 0000000..0a38b50
--- /dev/null
+++ b/src/reader/wgsl/lexer.h
@@ -0,0 +1,81 @@
+// 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.
+
+#ifndef SRC_READER_WGSL_LEXER_H_
+#define SRC_READER_WGSL_LEXER_H_
+
+#include <string>
+
+#include "src/reader/wgsl/token.h"
+#include "src/source.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+/// Converts the input stream into a series of Tokens
+class Lexer {
+ public:
+  /// Creates a new Lexer
+  /// @param input the input to parse
+  explicit Lexer(const std::string& input);
+  ~Lexer();
+
+  /// Returns the next token in the input stream
+  /// @return Token
+  Token next();
+
+ private:
+  void skip_whitespace();
+  void skip_comments();
+
+  Token build_token_from_int_if_possible(const Source& source,
+                                         size_t start,
+                                         size_t end,
+                                         int32_t base);
+  Token check_keyword(const Source&, const std::string&);
+  Token check_reserved(const Source&, const std::string&);
+  Token try_float();
+  Token try_hex_integer();
+  Token try_ident();
+  Token try_integer();
+  Token try_punctuation();
+  Token try_string();
+
+  Source make_source() const;
+
+  bool is_eof() const;
+  bool is_alpha(char ch) const;
+  bool is_digit(char ch) const;
+  bool is_hex(char ch) const;
+  bool is_alphanum(char ch) const;
+  bool matches(size_t pos, const std::string& substr);
+
+  /// The source to parse
+  std::string input_;
+  /// The length of the input
+  uint32_t len_ = 0;
+  /// The current position within the input
+  uint32_t pos_ = 0;
+  /// The current line within the input
+  uint32_t line_ = 1;
+  /// The current column within the input
+  uint32_t column_ = 1;
+};
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
+
+#endif  // SRC_READER_WGSL_LEXER_H_
diff --git a/src/reader/wgsl/lexer_test.cc b/src/reader/wgsl/lexer_test.cc
new file mode 100644
index 0000000..17210ff
--- /dev/null
+++ b/src/reader/wgsl/lexer_test.cc
@@ -0,0 +1,531 @@
+// 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/reader/wgsl/lexer.h"
+
+#include <limits>
+
+#include "gtest/gtest.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using LexerTest = testing::Test;
+
+TEST_F(LexerTest, Empty) {
+  Lexer l("");
+  auto t = l.next();
+  EXPECT_TRUE(t.IsEof());
+}
+
+TEST_F(LexerTest, Skips_Whitespace) {
+  Lexer l("\t\r\n\t    ident\t\n\t  \r ");
+
+  auto t = l.next();
+  EXPECT_TRUE(t.IsIdentifier());
+  EXPECT_EQ(t.line(), 2);
+  EXPECT_EQ(t.column(), 6);
+  EXPECT_EQ(t.to_str(), "ident");
+
+  t = l.next();
+  EXPECT_TRUE(t.IsEof());
+}
+
+TEST_F(LexerTest, Skips_Comments) {
+  Lexer l(R"(#starts with comment
+ident1 #ends with comment
+# blank line
+ ident2)");
+
+  auto t = l.next();
+  EXPECT_TRUE(t.IsIdentifier());
+  EXPECT_EQ(t.line(), 2);
+  EXPECT_EQ(t.column(), 1);
+  EXPECT_EQ(t.to_str(), "ident1");
+
+  t = l.next();
+  EXPECT_TRUE(t.IsIdentifier());
+  EXPECT_EQ(t.line(), 4);
+  EXPECT_EQ(t.column(), 2);
+  EXPECT_EQ(t.to_str(), "ident2");
+
+  t = l.next();
+  EXPECT_TRUE(t.IsEof());
+}
+
+TEST_F(LexerTest, StringTest_Parse) {
+  Lexer l(R"(id "this is string content" id2)");
+
+  auto t = l.next();
+  EXPECT_TRUE(t.IsIdentifier());
+  EXPECT_EQ(t.to_str(), "id");
+  EXPECT_EQ(1, t.line());
+  EXPECT_EQ(1, t.column());
+
+  t = l.next();
+  EXPECT_TRUE(t.IsStringLiteral());
+  EXPECT_EQ(t.to_str(), "this is string content");
+  EXPECT_EQ(1, t.line());
+  EXPECT_EQ(4, t.column());
+
+  t = l.next();
+  EXPECT_TRUE(t.IsIdentifier());
+  EXPECT_EQ(t.to_str(), "id2");
+  EXPECT_EQ(1, t.line());
+  EXPECT_EQ(29, t.column());
+}
+
+TEST_F(LexerTest, StringTest_Unterminated) {
+  Lexer l(R"(id "this is string content)");
+
+  auto t = l.next();
+  EXPECT_TRUE(t.IsIdentifier());
+  EXPECT_EQ(t.to_str(), "id");
+  EXPECT_EQ(1, t.line());
+  EXPECT_EQ(1, t.column());
+
+  t = l.next();
+  EXPECT_TRUE(t.IsStringLiteral());
+  EXPECT_EQ(t.to_str(), "this is string content");
+
+  t = l.next();
+  EXPECT_TRUE(t.IsEof());
+}
+
+struct FloatData {
+  const char* input;
+  float result;
+};
+inline std::ostream& operator<<(std::ostream& out, FloatData data) {
+  out << std::string(data.input);
+  return out;
+}
+using FloatTest = testing::TestWithParam<FloatData>;
+TEST_P(FloatTest, Parse) {
+  auto params = GetParam();
+  Lexer l(std::string(params.input));
+
+  auto t = l.next();
+  EXPECT_TRUE(t.IsFloatLiteral());
+  EXPECT_EQ(t.to_f32(), params.result);
+  EXPECT_EQ(1, t.line());
+  EXPECT_EQ(1, t.column());
+
+  t = l.next();
+  EXPECT_TRUE(t.IsEof());
+}
+INSTANTIATE_TEST_SUITE_P(LexerTest,
+                         FloatTest,
+                         testing::Values(FloatData{"0.0", 0.0f},
+                                         FloatData{"0.", 0.0f},
+                                         FloatData{".0", 0.0f},
+                                         FloatData{"5.7", 5.7f},
+                                         FloatData{"5.", 5.f},
+                                         FloatData{".7", .7f},
+                                         FloatData{"-0.0", 0.0f},
+                                         FloatData{"-.0", 0.0f},
+                                         FloatData{"-0.", 0.0f},
+                                         FloatData{"-5.7", -5.7f},
+                                         FloatData{"-5.", -5.f},
+                                         FloatData{"-.7", -.7f},
+                                         FloatData{"0.2e+12", 0.2e12f},
+                                         FloatData{"1.2e-5", 1.2e-5f},
+                                         FloatData{"2.57e23", 2.57e23f},
+                                         FloatData{"2.5e+0", 2.5f},
+                                         FloatData{"2.5e-0", 2.5f}));
+
+using FloatTest_Invalid = testing::TestWithParam<const char*>;
+TEST_P(FloatTest_Invalid, Handles) {
+  Lexer l(GetParam());
+
+  auto t = l.next();
+  EXPECT_FALSE(t.IsFloatLiteral());
+}
+INSTANTIATE_TEST_SUITE_P(LexerTest,
+                         FloatTest_Invalid,
+                         testing::Values(".",
+                                         "-.",
+                                         "2.5e+256",
+                                         "-2.5e+127",
+                                         "2.5e-300",
+                                         "2.5e 12",
+                                         "2.5e+ 123"));
+
+using IdentifierTest = testing::TestWithParam<const char*>;
+TEST_P(IdentifierTest, Parse) {
+  Lexer l(GetParam());
+
+  auto t = l.next();
+  EXPECT_TRUE(t.IsIdentifier());
+  EXPECT_EQ(t.line(), 1);
+  EXPECT_EQ(t.column(), 1);
+  EXPECT_EQ(t.to_str(), GetParam());
+}
+INSTANTIATE_TEST_SUITE_P(
+    LexerTest,
+    IdentifierTest,
+    testing::Values("test01", "_test_", "test_", "_test", "_01", "_test01"));
+
+TEST_F(LexerTest, IdentifierTest_DoesNotStartWithNumber) {
+  Lexer l("01test");
+
+  auto t = l.next();
+  EXPECT_FALSE(t.IsIdentifier());
+}
+
+struct HexSignedIntData {
+  const char* input;
+  int32_t result;
+};
+inline std::ostream& operator<<(std::ostream& out, HexSignedIntData data) {
+  out << std::string(data.input);
+  return out;
+}
+
+using IntegerTest_HexSigned = testing::TestWithParam<HexSignedIntData>;
+TEST_P(IntegerTest_HexSigned, Matches) {
+  auto params = GetParam();
+  Lexer l(std::string(params.input));
+
+  auto t = l.next();
+  EXPECT_TRUE(t.IsIntLiteral());
+  EXPECT_EQ(t.line(), 1);
+  EXPECT_EQ(t.column(), 1);
+  EXPECT_EQ(t.to_i32(), params.result);
+}
+INSTANTIATE_TEST_SUITE_P(
+    LexerTest,
+    IntegerTest_HexSigned,
+    testing::Values(
+        HexSignedIntData{"0x0", 0},
+        HexSignedIntData{"0x42", 66},
+        HexSignedIntData{"-0x42", -66},
+        HexSignedIntData{"0xeF1Abc9", 250719177},
+        HexSignedIntData{"-0x80000000", std::numeric_limits<int32_t>::min()},
+        HexSignedIntData{"0x7FFFFFFF", std::numeric_limits<int32_t>::max()}));
+
+TEST_F(LexerTest, IntegerTest_HexSignedTooLarge) {
+  Lexer l("0x80000000");
+  auto t = l.next();
+  ASSERT_TRUE(t.IsError());
+  EXPECT_EQ(t.to_str(), "i32 (0x80000000) too large");
+}
+
+TEST_F(LexerTest, IntegerTest_HexSignedTooSmall) {
+  Lexer l("-0x8000000F");
+  auto t = l.next();
+  ASSERT_TRUE(t.IsError());
+  EXPECT_EQ(t.to_str(), "i32 (-0x8000000F) too small");
+}
+
+struct HexUnsignedIntData {
+  const char* input;
+  uint32_t result;
+};
+inline std::ostream& operator<<(std::ostream& out, HexUnsignedIntData data) {
+  out << std::string(data.input);
+  return out;
+}
+using IntegerTest_HexUnsigned = testing::TestWithParam<HexUnsignedIntData>;
+TEST_P(IntegerTest_HexUnsigned, Matches) {
+  auto params = GetParam();
+  Lexer l(std::string(params.input));
+
+  auto t = l.next();
+  EXPECT_TRUE(t.IsUintLiteral());
+  EXPECT_EQ(t.line(), 1);
+  EXPECT_EQ(t.column(), 1);
+  EXPECT_EQ(t.to_u32(), params.result);
+}
+INSTANTIATE_TEST_SUITE_P(
+    LexerTest,
+    IntegerTest_HexUnsigned,
+    testing::Values(HexUnsignedIntData{"0x0u", 0},
+                    HexUnsignedIntData{"0x42u", 66},
+                    HexUnsignedIntData{"0xeF1Abc9u", 250719177},
+                    HexUnsignedIntData{"0x0u",
+                                       std::numeric_limits<uint32_t>::min()},
+                    HexUnsignedIntData{"0xFFFFFFFFu",
+                                       std::numeric_limits<uint32_t>::max()}));
+
+TEST_F(LexerTest, IntegerTest_HexUnsignedTooLarge) {
+  Lexer l("0xffffffffffu");
+  auto t = l.next();
+  ASSERT_TRUE(t.IsError());
+  EXPECT_EQ(t.to_str(), "u32 (0xffffffffff) too large");
+}
+
+struct UnsignedIntData {
+  const char* input;
+  uint32_t result;
+};
+inline std::ostream& operator<<(std::ostream& out, UnsignedIntData data) {
+  out << std::string(data.input);
+  return out;
+}
+using IntegerTest_Unsigned = testing::TestWithParam<UnsignedIntData>;
+TEST_P(IntegerTest_Unsigned, Matches) {
+  auto params = GetParam();
+  Lexer l(params.input);
+
+  auto t = l.next();
+  EXPECT_TRUE(t.IsUintLiteral());
+  EXPECT_EQ(t.to_u32(), params.result);
+  EXPECT_EQ(1, t.line());
+  EXPECT_EQ(1, t.column());
+}
+INSTANTIATE_TEST_SUITE_P(LexerTest,
+                         IntegerTest_Unsigned,
+                         testing::Values(UnsignedIntData{"0u", 0u},
+                                         UnsignedIntData{"123u", 123u},
+                                         UnsignedIntData{"4294967295u",
+                                                         4294967295u}));
+
+struct SignedIntData {
+  const char* input;
+  int32_t result;
+};
+inline std::ostream& operator<<(std::ostream& out, SignedIntData data) {
+  out << std::string(data.input);
+  return out;
+}
+using IntegerTest_Signed = testing::TestWithParam<SignedIntData>;
+TEST_P(IntegerTest_Signed, Matches) {
+  auto params = GetParam();
+  Lexer l(params.input);
+
+  auto t = l.next();
+  EXPECT_TRUE(t.IsIntLiteral());
+  EXPECT_EQ(t.to_i32(), params.result);
+  EXPECT_EQ(1, t.line());
+  EXPECT_EQ(1, t.column());
+}
+INSTANTIATE_TEST_SUITE_P(
+    LexerTest,
+    IntegerTest_Signed,
+    testing::Values(SignedIntData{"0", 0},
+                    SignedIntData{"-2", -2},
+                    SignedIntData{"2", 2},
+                    SignedIntData{"123", 123},
+                    SignedIntData{"2147483647", 2147483647},
+                    SignedIntData{"-2147483648", -2147483648}));
+
+using IntegerTest_Invalid = testing::TestWithParam<const char*>;
+TEST_P(IntegerTest_Invalid, Parses) {
+  Lexer l(GetParam());
+
+  auto t = l.next();
+  EXPECT_FALSE(t.IsIntLiteral());
+  EXPECT_FALSE(t.IsUintLiteral());
+}
+INSTANTIATE_TEST_SUITE_P(LexerTest,
+                         IntegerTest_Invalid,
+                         testing::Values("2147483648", "4294967296u"));
+
+struct TokenData {
+  const char* input;
+  Token::Type type;
+};
+inline std::ostream& operator<<(std::ostream& out, TokenData data) {
+  out << std::string(data.input);
+  return out;
+}
+using PunctuationTest = testing::TestWithParam<TokenData>;
+TEST_P(PunctuationTest, Parses) {
+  auto params = GetParam();
+  Lexer l(params.input);
+
+  auto t = l.next();
+  EXPECT_TRUE(t.Is(params.type));
+  EXPECT_EQ(1, t.line());
+  EXPECT_EQ(1, t.column());
+
+  t = l.next();
+  EXPECT_EQ(1 + std::string(params.input).size(), t.column());
+}
+INSTANTIATE_TEST_SUITE_P(
+    LexerTest,
+    PunctuationTest,
+    testing::Values(TokenData{"&", Token::Type::kAnd},
+                    TokenData{"&&", Token::Type::kAndAnd},
+                    TokenData{"->", Token::Type::kArrow},
+                    TokenData{"[[", Token::Type::kAttrLeft},
+                    TokenData{"]]", Token::Type::kAttrRight},
+                    TokenData{"/", Token::Type::kForwardSlash},
+                    TokenData{"!", Token::Type::kBang},
+                    TokenData{"[", Token::Type::kBraceLeft},
+                    TokenData{"]", Token::Type::kBraceRight},
+                    TokenData{"{", Token::Type::kBracketLeft},
+                    TokenData{"}", Token::Type::kBracketRight},
+                    TokenData{":", Token::Type::kColon},
+                    TokenData{",", Token::Type::kComma},
+                    TokenData{"=", Token::Type::kEqual},
+                    TokenData{"==", Token::Type::kEqualEqual},
+                    TokenData{">", Token::Type::kGreaterThan},
+                    TokenData{">=", Token::Type::kGreaterThanEqual},
+                    TokenData{"<", Token::Type::kLessThan},
+                    TokenData{"<=", Token::Type::kLessThanEqual},
+                    TokenData{"%", Token::Type::kMod},
+                    TokenData{"!=", Token::Type::kNotEqual},
+                    TokenData{"-", Token::Type::kMinus},
+                    TokenData{"::", Token::Type::kNamespace},
+                    TokenData{".", Token::Type::kPeriod},
+                    TokenData{"+", Token::Type::kPlus},
+                    TokenData{"|", Token::Type::kOr},
+                    TokenData{"||", Token::Type::kOrOr},
+                    TokenData{"(", Token::Type::kParenLeft},
+                    TokenData{")", Token::Type::kParenRight},
+                    TokenData{";", Token::Type::kSemicolon},
+                    TokenData{"*", Token::Type::kStar},
+                    TokenData{"^", Token::Type::kXor}));
+
+using KeywordTest = testing::TestWithParam<TokenData>;
+TEST_P(KeywordTest, Parses) {
+  auto params = GetParam();
+  Lexer l(params.input);
+
+  auto t = l.next();
+  EXPECT_TRUE(t.Is(params.type));
+  EXPECT_EQ(1, t.line());
+  EXPECT_EQ(1, t.column());
+
+  t = l.next();
+  EXPECT_EQ(1 + std::string(params.input).size(), t.column());
+}
+INSTANTIATE_TEST_SUITE_P(
+    LexerTest,
+    KeywordTest,
+    testing::Values(
+        TokenData{"all", Token::Type::kAll},
+        TokenData{"any", Token::Type::kAny},
+        TokenData{"array", Token::Type::kArray},
+        TokenData{"as", Token::Type::kAs},
+        TokenData{"binding", Token::Type::kBinding},
+        TokenData{"block", Token::Type::kBlock},
+        TokenData{"bool", Token::Type::kBool},
+        TokenData{"break", Token::Type::kBreak},
+        TokenData{"builtin", Token::Type::kBuiltin},
+        TokenData{"case", Token::Type::kCase},
+        TokenData{"cast", Token::Type::kCast},
+        TokenData{"compute", Token::Type::kCompute},
+        TokenData{"const", Token::Type::kConst},
+        TokenData{"continue", Token::Type::kContinue},
+        TokenData{"continuing", Token::Type::kContinuing},
+        TokenData{"coarse", Token::Type::kCoarse},
+        TokenData{"default", Token::Type::kDefault},
+        TokenData{"dot", Token::Type::kDot},
+        TokenData{"dpdx", Token::Type::kDpdx},
+        TokenData{"dpdy", Token::Type::kDpdy},
+        TokenData{"else", Token::Type::kElse},
+        TokenData{"elseif", Token::Type::kElseIf},
+        TokenData{"entry_point", Token::Type::kEntryPoint},
+        TokenData{"f32", Token::Type::kF32},
+        TokenData{"fallthrough", Token::Type::kFallthrough},
+        TokenData{"false", Token::Type::kFalse},
+        TokenData{"fine", Token::Type::kFine},
+        TokenData{"fn", Token::Type::kFn},
+        TokenData{"frag_coord", Token::Type::kFragCoord},
+        TokenData{"frag_depth", Token::Type::kFragDepth},
+        TokenData{"fragment", Token::Type::kFragment},
+        TokenData{"front_facing", Token::Type::kFrontFacing},
+        TokenData{"function", Token::Type::kFunction},
+        TokenData{"fwidth", Token::Type::kFwidth},
+        TokenData{"global_invocation_id", Token::Type::kGlobalInvocationId},
+        TokenData{"i32", Token::Type::kI32},
+        TokenData{"if", Token::Type::kIf},
+        TokenData{"image", Token::Type::kImage},
+        TokenData{"import", Token::Type::kImport},
+        TokenData{"in", Token::Type::kIn},
+        TokenData{"instance_idx", Token::Type::kInstanceIdx},
+        TokenData{"is_nan", Token::Type::kIsNan},
+        TokenData{"is_inf", Token::Type::kIsInf},
+        TokenData{"is_finite", Token::Type::kIsFinite},
+        TokenData{"is_normal", Token::Type::kIsNormal},
+        TokenData{"kill", Token::Type::kKill},
+        TokenData{"local_invocation_id", Token::Type::kLocalInvocationId},
+        TokenData{"local_invocation_idx", Token::Type::kLocalInvocationIdx},
+        TokenData{"location", Token::Type::kLocation},
+        TokenData{"loop", Token::Type::kLoop},
+        TokenData{"mat2x2", Token::Type::kMat2x2},
+        TokenData{"mat2x3", Token::Type::kMat2x3},
+        TokenData{"mat2x4", Token::Type::kMat2x4},
+        TokenData{"mat3x2", Token::Type::kMat3x2},
+        TokenData{"mat3x3", Token::Type::kMat3x3},
+        TokenData{"mat3x4", Token::Type::kMat3x4},
+        TokenData{"mat4x2", Token::Type::kMat4x2},
+        TokenData{"mat4x3", Token::Type::kMat4x3},
+        TokenData{"mat4x4", Token::Type::kMat4x4},
+        TokenData{"nop", Token::Type::kNop},
+        TokenData{"num_workgroups", Token::Type::kNumWorkgroups},
+        TokenData{"offset", Token::Type::kOffset},
+        TokenData{"out", Token::Type::kOut},
+        TokenData{"outer_product", Token::Type::kOuterProduct},
+        TokenData{"position", Token::Type::kPosition},
+        TokenData{"premerge", Token::Type::kPremerge},
+        TokenData{"private", Token::Type::kPrivate},
+        TokenData{"ptr", Token::Type::kPtr},
+        TokenData{"push_constant", Token::Type::kPushConstant},
+        TokenData{"regardless", Token::Type::kRegardless},
+        TokenData{"return", Token::Type::kReturn},
+        TokenData{"set", Token::Type::kSet},
+        TokenData{"storage_buffer", Token::Type::kStorageBuffer},
+        TokenData{"struct", Token::Type::kStruct},
+        TokenData{"switch", Token::Type::kSwitch},
+        TokenData{"true", Token::Type::kTrue},
+        TokenData{"type", Token::Type::kType},
+        TokenData{"u32", Token::Type::kU32},
+        TokenData{"uniform", Token::Type::kUniform},
+        TokenData{"uniform_constant", Token::Type::kUniformConstant},
+        TokenData{"unless", Token::Type::kUnless},
+        TokenData{"var", Token::Type::kVar},
+        TokenData{"vec2", Token::Type::kVec2},
+        TokenData{"vec3", Token::Type::kVec3},
+        TokenData{"vec4", Token::Type::kVec4},
+        TokenData{"vertex", Token::Type::kVertex},
+        TokenData{"vertex_idx", Token::Type::kVertexIdx},
+        TokenData{"void", Token::Type::kVoid},
+        TokenData{"workgroup", Token::Type::kWorkgroup},
+        TokenData{"workgroup_size", Token::Type::kWorkgroupSize}));
+
+using KeywordTest_Reserved = testing::TestWithParam<const char*>;
+TEST_P(KeywordTest_Reserved, Parses) {
+  auto keyword = GetParam();
+  Lexer l(keyword);
+
+  auto t = l.next();
+  EXPECT_TRUE(t.IsReservedKeyword());
+  EXPECT_EQ(t.to_str(), keyword);
+}
+INSTANTIATE_TEST_SUITE_P(LexerTest,
+                         KeywordTest_Reserved,
+                         testing::Values("asm",
+                                         "bf16",
+                                         "do",
+                                         "enum",
+                                         "f16",
+                                         "f64",
+                                         "for",
+                                         "i8",
+                                         "i16",
+                                         "i64",
+                                         "let",
+                                         "typedef",
+                                         "u8",
+                                         "u16",
+                                         "u64"));
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser.cc b/src/reader/wgsl/parser.cc
new file mode 100644
index 0000000..f84dc62
--- /dev/null
+++ b/src/reader/wgsl/parser.cc
@@ -0,0 +1,43 @@
+// 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/reader/wgsl/parser.h"
+
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+Parser::Parser(const std::string& input)
+    : Reader(), impl_(std::make_unique<ParserImpl>(input)) {}
+
+Parser::~Parser() = default;
+
+bool Parser::Parse() {
+  bool ret = impl_->Parse();
+
+  if (impl_->has_error())
+    set_error(impl_->error());
+
+  return ret;
+}
+
+ast::Module Parser::module() {
+  return impl_->module();
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser.h b/src/reader/wgsl/parser.h
new file mode 100644
index 0000000..b3b27d9
--- /dev/null
+++ b/src/reader/wgsl/parser.h
@@ -0,0 +1,52 @@
+// 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.
+
+#ifndef SRC_READER_WGSL_PARSER_H_
+#define SRC_READER_WGSL_PARSER_H_
+
+#include <memory>
+#include <string>
+
+#include "src/reader/reader.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+class ParserImpl;
+
+/// Parser for WGSL source data
+class Parser : public Reader {
+ public:
+  /// Creates a new parser
+  /// @param input the input string to parse
+  explicit Parser(const std::string& input);
+  ~Parser() override;
+
+  /// Run the parser
+  /// @returns true if the parse was successful, false otherwise.
+  bool Parse() override;
+
+  /// @returns the module. The module in the parser will be reset after this.
+  ast::Module module() override;
+
+ private:
+  std::unique_ptr<ParserImpl> impl_;
+};
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
+
+#endif  // SRC_READER_WGSL_PARSER_H_
diff --git a/src/reader/wgsl/parser_impl.cc b/src/reader/wgsl/parser_impl.cc
new file mode 100644
index 0000000..58d4bf2
--- /dev/null
+++ b/src/reader/wgsl/parser_impl.cc
@@ -0,0 +1,3061 @@
+// 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/reader/wgsl/parser_impl.h"
+
+#include <memory>
+
+#include "src/ast/array_accessor_expression.h"
+#include "src/ast/as_expression.h"
+#include "src/ast/binding_decoration.h"
+#include "src/ast/bool_literal.h"
+#include "src/ast/break_statement.h"
+#include "src/ast/builtin_decoration.h"
+#include "src/ast/call_expression.h"
+#include "src/ast/case_statement.h"
+#include "src/ast/cast_expression.h"
+#include "src/ast/const_initializer_expression.h"
+#include "src/ast/continue_statement.h"
+#include "src/ast/decorated_variable.h"
+#include "src/ast/else_statement.h"
+#include "src/ast/fallthrough_statement.h"
+#include "src/ast/float_literal.h"
+#include "src/ast/identifier_expression.h"
+#include "src/ast/if_statement.h"
+#include "src/ast/int_literal.h"
+#include "src/ast/kill_statement.h"
+#include "src/ast/location_decoration.h"
+#include "src/ast/member_accessor_expression.h"
+#include "src/ast/nop_statement.h"
+#include "src/ast/relational_expression.h"
+#include "src/ast/return_statement.h"
+#include "src/ast/set_decoration.h"
+#include "src/ast/statement_condition.h"
+#include "src/ast/struct_member_offset_decoration.h"
+#include "src/ast/switch_statement.h"
+#include "src/ast/type/alias_type.h"
+#include "src/ast/type/array_type.h"
+#include "src/ast/type/bool_type.h"
+#include "src/ast/type/f32_type.h"
+#include "src/ast/type/i32_type.h"
+#include "src/ast/type/matrix_type.h"
+#include "src/ast/type/pointer_type.h"
+#include "src/ast/type/struct_type.h"
+#include "src/ast/type/u32_type.h"
+#include "src/ast/type/vector_type.h"
+#include "src/ast/type/void_type.h"
+#include "src/ast/type_initializer_expression.h"
+#include "src/ast/uint_literal.h"
+#include "src/ast/unary_derivative.h"
+#include "src/ast/unary_derivative_expression.h"
+#include "src/ast/unary_method.h"
+#include "src/ast/unary_method_expression.h"
+#include "src/ast/unary_op.h"
+#include "src/ast/unary_op_expression.h"
+#include "src/ast/variable_statement.h"
+#include "src/reader/wgsl/lexer.h"
+#include "src/type_manager.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+ParserImpl::ParserImpl(const std::string& input)
+    : lexer_(std::make_unique<Lexer>(input)) {}
+
+ParserImpl::~ParserImpl() = default;
+
+void ParserImpl::set_error(const Token& t, const std::string& err) {
+  auto prefix =
+      std::to_string(t.line()) + ":" + std::to_string(t.column()) + ": ";
+
+  if (t.IsReservedKeyword()) {
+    error_ = prefix + "reserved token (" + t.to_str() + ") found";
+    return;
+  }
+  if (t.IsError()) {
+    error_ = prefix + t.to_str();
+    return;
+  }
+
+  if (err.size() != 0) {
+    error_ = prefix + err;
+  } else {
+    error_ = prefix + "invalid token (" + t.to_name() + ") encountered";
+  }
+}
+
+void ParserImpl::set_error(const Token& t) {
+  set_error(t, "");
+}
+
+Token ParserImpl::next() {
+  if (!token_queue_.empty()) {
+    auto t = token_queue_.front();
+    token_queue_.pop_front();
+    return t;
+  }
+  return lexer_->next();
+}
+
+Token ParserImpl::peek(size_t idx) {
+  while (token_queue_.size() < (idx + 1))
+    token_queue_.push_back(lexer_->next());
+
+  return token_queue_[idx];
+}
+
+Token ParserImpl::peek() {
+  return peek(0);
+}
+
+void ParserImpl::register_alias(const std::string& name,
+                                ast::type::Type* type) {
+  assert(type);
+  registered_aliases_[name] = type;
+}
+
+ast::type::Type* ParserImpl::get_alias(const std::string& name) {
+  if (registered_aliases_.find(name) == registered_aliases_.end()) {
+    return nullptr;
+  }
+  return registered_aliases_[name];
+}
+
+bool ParserImpl::Parse() {
+  translation_unit();
+  return !has_error();
+}
+
+// translation_unit
+//  : global_decl* EOF
+void ParserImpl::translation_unit() {
+  for (;;) {
+    global_decl();
+    if (has_error())
+      return;
+
+    if (peek().IsEof())
+      break;
+  }
+
+  assert(module_.IsValid());
+}
+
+// global_decl
+//  : SEMICOLON
+//  | import_decl SEMICOLON
+//  | global_variable_decl SEMICLON
+//  | global_constant_decl SEMICOLON
+//  | entry_point_decl SEMICOLON
+//  | type_alias SEMICOLON
+//  | function_decl
+void ParserImpl::global_decl() {
+  auto t = peek();
+  if (t.IsEof())
+    return;
+
+  if (t.IsSemicolon()) {
+    next();  // consume the peek
+    return;
+  }
+
+  auto import = import_decl();
+  if (has_error())
+    return;
+  if (import != nullptr) {
+    t = next();
+    if (!t.IsSemicolon()) {
+      set_error(t, "missing ';' for import");
+      return;
+    }
+    module_.AddImport(std::move(import));
+    return;
+  }
+
+  auto gv = global_variable_decl();
+  if (has_error())
+    return;
+  if (gv != nullptr) {
+    t = next();
+    if (!t.IsSemicolon()) {
+      set_error(t, "missing ';' for variable declaration");
+      return;
+    }
+    module_.AddGlobalVariable(std::move(gv));
+    return;
+  }
+
+  auto gc = global_constant_decl();
+  if (has_error())
+    return;
+  if (gc != nullptr) {
+    t = next();
+    if (!t.IsSemicolon()) {
+      set_error(t, "missing ';' for constant declaration");
+      return;
+    }
+    module_.AddGlobalVariable(std::move(gc));
+    return;
+  }
+
+  auto ep = entry_point_decl();
+  if (has_error())
+    return;
+  if (ep != nullptr) {
+    t = next();
+    if (!t.IsSemicolon()) {
+      set_error(t, "missing ';' for entry point");
+      return;
+    }
+    module_.AddEntryPoint(std::move(ep));
+    return;
+  }
+
+  auto ta = type_alias();
+  if (has_error())
+    return;
+  if (ta != nullptr) {
+    t = next();
+    if (!t.IsSemicolon()) {
+      set_error(t, "missing ';' for type alias");
+      return;
+    }
+    module_.AddAliasType(ta);
+    return;
+  }
+
+  auto func = function_decl();
+  if (has_error())
+    return;
+  if (func != nullptr) {
+    module_.AddFunction(std::move(func));
+    return;
+  }
+
+  set_error(t);
+}
+
+// import_decl
+//  : IMPORT STRING_LITERAL AS (IDENT NAMESPACE)* IDENT
+std::unique_ptr<ast::Import> ParserImpl::import_decl() {
+  auto t = peek();
+  if (!t.IsImport())
+    return {};
+
+  auto source = t.source();
+  next();  // consume the import token
+
+  t = next();
+  if (!t.IsStringLiteral()) {
+    set_error(t, "missing path for import");
+    return {};
+  }
+  auto path = t.to_str();
+  if (path.length() == 0) {
+    set_error(t, "import path must not be empty");
+    return {};
+  }
+
+  t = next();
+  if (!t.IsAs()) {
+    set_error(t, "missing 'as' for import");
+    return {};
+  }
+
+  std::string name = "";
+  for (;;) {
+    t = peek();
+    if (!t.IsIdentifier()) {
+      break;
+    }
+    next();  // consume the peek
+
+    name += t.to_str();
+
+    t = peek();
+    if (!t.IsNamespace()) {
+      break;
+    }
+    next();  // consume the peek
+
+    name += "::";
+  }
+  if (name.length() == 0) {
+    if (t.IsEof() || t.IsSemicolon()) {
+      set_error(t, "missing name for import");
+    } else {
+      set_error(t, "invalid name for import");
+    }
+    return {};
+  }
+  if (name.length() > 2) {
+    auto end = name.length() - 1;
+    if (name[end] == ':' && name[end - 1] == ':') {
+      set_error(t, "invalid name for import");
+      return {};
+    }
+  }
+  return std::make_unique<ast::Import>(source, path, name);
+}
+
+// global_variable_decl
+//  : variable_decoration_list variable_decl
+//  | variable_decoration_list variable_decl EQUAL const_expr
+std::unique_ptr<ast::Variable> ParserImpl::global_variable_decl() {
+  auto decos = variable_decoration_list();
+  if (has_error())
+    return nullptr;
+
+  auto var = variable_decl();
+  if (has_error())
+    return nullptr;
+  if (var == nullptr) {
+    if (decos.size() > 0)
+      set_error(peek(), "error parsing variable declaration");
+
+    return nullptr;
+  }
+
+  if (decos.size() > 0) {
+    auto dv = std::make_unique<ast::DecoratedVariable>();
+    dv->set_source(var->source());
+    dv->set_name(var->name());
+    dv->set_type(var->type());
+    dv->set_storage_class(var->storage_class());
+    dv->set_decorations(std::move(decos));
+
+    var = std::move(dv);
+  }
+
+  auto t = peek();
+  if (t.IsEqual()) {
+    next();  // Consume the peek
+
+    auto expr = const_expr();
+    if (has_error())
+      return nullptr;
+    if (expr == nullptr) {
+      set_error(peek(), "invalid expression");
+      return nullptr;
+    }
+
+    var->set_initializer(std::move(expr));
+  }
+  return var;
+}
+
+// global_constant_decl
+//  : CONST variable_ident_decl EQUAL const_expr
+std::unique_ptr<ast::Variable> ParserImpl::global_constant_decl() {
+  auto t = peek();
+  if (!t.IsConst())
+    return nullptr;
+
+  auto source = t.source();
+  next();  // Consume the peek
+
+  std::string name;
+  ast::type::Type* type;
+  std::tie(name, type) = variable_ident_decl();
+  if (has_error())
+    return nullptr;
+  if (name == "" || type == nullptr) {
+    set_error(peek(), "error parsing constant variable identifier");
+    return nullptr;
+  }
+
+  auto var = std::make_unique<ast::Variable>(source, name,
+                                             ast::StorageClass::kNone, type);
+  var->set_is_const(true);
+
+  t = next();
+  if (!t.IsEqual()) {
+    set_error(t, "missing = for const declaration");
+    return nullptr;
+  }
+
+  auto init = const_expr();
+  if (has_error())
+    return nullptr;
+  if (init == nullptr) {
+    set_error(peek(), "error parsing constant initializer");
+    return nullptr;
+  }
+  var->set_initializer(std::move(init));
+
+  return var;
+}
+
+// variable_decoration_list
+//  : ATTR_LEFT variable_decoration (COMMA variable_decoration)* ATTR_RIGHT
+std::vector<std::unique_ptr<ast::VariableDecoration>>
+ParserImpl::variable_decoration_list() {
+  std::vector<std::unique_ptr<ast::VariableDecoration>> decos;
+
+  auto t = peek();
+  if (!t.IsAttrLeft())
+    return decos;
+
+  next();  // consume the peek
+
+  auto deco = variable_decoration();
+  if (has_error())
+    return {};
+  if (deco == nullptr) {
+    t = peek();
+    if (t.IsAttrRight()) {
+      set_error(t, "empty variable decoration list");
+      return {};
+    }
+    set_error(t, "missing variable decoration for decoration list");
+    return {};
+  }
+  for (;;) {
+    decos.push_back(std::move(deco));
+
+    t = peek();
+    if (!t.IsComma()) {
+      break;
+    }
+    next();  // consume the peek
+
+    deco = variable_decoration();
+    if (has_error())
+      return {};
+    if (deco == nullptr) {
+      set_error(peek(), "missing variable decoration after comma");
+      return {};
+    }
+  }
+
+  t = peek();
+  if (!t.IsAttrRight()) {
+    deco = variable_decoration();
+    if (deco != nullptr) {
+      set_error(t, "missing comma in variable decoration list");
+      return {};
+    }
+    set_error(t, "missing ]] for variable decoration");
+    return {};
+  }
+  next();  // consume the peek
+
+  return decos;
+}
+
+// variable_decoration
+//  : LOCATION INT_LITERAL
+//  | BUILTIN builtin_decoration
+//  | BINDING INT_LITERAL
+//  | SET INT_LITERAL
+std::unique_ptr<ast::VariableDecoration> ParserImpl::variable_decoration() {
+  auto t = peek();
+  if (t.IsLocation()) {
+    next();  // consume the peek
+
+    t = next();
+    if (!t.IsIntLiteral()) {
+      set_error(t, "invalid value for location decoration");
+      return {};
+    }
+
+    return std::make_unique<ast::LocationDecoration>(t.to_i32());
+  }
+  if (t.IsBuiltin()) {
+    next();  // consume the peek
+
+    ast::Builtin builtin = builtin_decoration();
+    if (has_error())
+      return {};
+    if (builtin == ast::Builtin::kNone) {
+      set_error(peek(), "invalid value for builtin decoration");
+      return {};
+    }
+
+    return std::make_unique<ast::BuiltinDecoration>(builtin);
+  }
+  if (t.IsBinding()) {
+    next();  // consume the peek
+
+    t = next();
+    if (!t.IsIntLiteral()) {
+      set_error(t, "invalid value for binding decoration");
+      return {};
+    }
+
+    return std::make_unique<ast::BindingDecoration>(t.to_i32());
+  }
+  if (t.IsSet()) {
+    next();  // consume the peek
+
+    t = next();
+    if (!t.IsIntLiteral()) {
+      set_error(t, "invalid value for set decoration");
+      return {};
+    }
+
+    return std::make_unique<ast::SetDecoration>(t.to_i32());
+  }
+
+  return nullptr;
+}
+
+// builtin_decoration
+//  : POSITION
+//  | VERTEX_IDX
+//  | INSTANCE_IDX
+//  | FRONT_FACING
+//  | FRAG_COORD
+//  | FRAG_DEPTH
+//  | NUM_WORKGROUPS
+//  | WORKGROUP_SIZE
+//  | LOCAL_INVOC_ID
+//  | LOCAL_INVOC_IDX
+//  | GLOBAL_INVOC_ID
+ast::Builtin ParserImpl::builtin_decoration() {
+  auto t = peek();
+  if (t.IsPosition()) {
+    next();  // consume the peek
+    return ast::Builtin::kPosition;
+  }
+  if (t.IsVertexIdx()) {
+    next();  // consume the peek
+    return ast::Builtin::kVertexIdx;
+  }
+  if (t.IsInstanceIdx()) {
+    next();  // consume the peek
+    return ast::Builtin::kInstanceIdx;
+  }
+  if (t.IsFrontFacing()) {
+    next();  // consume the peek
+    return ast::Builtin::kFrontFacing;
+  }
+  if (t.IsFragCoord()) {
+    next();  // consume the peek
+    return ast::Builtin::kFragCoord;
+  }
+  if (t.IsFragDepth()) {
+    next();  // consume the peek
+    return ast::Builtin::kFragDepth;
+  }
+  if (t.IsNumWorkgroups()) {
+    next();  // consume the peek
+    return ast::Builtin::kNumWorkgroups;
+  }
+  if (t.IsWorkgroupSize()) {
+    next();  // consume the peek
+    return ast::Builtin::kWorkgroupSize;
+  }
+  if (t.IsLocalInvocationId()) {
+    next();  // consume the peek
+    return ast::Builtin::kLocalInvocationId;
+  }
+  if (t.IsLocalInvocationIdx()) {
+    next();  // consume the peek
+    return ast::Builtin::kLocalInvocationIdx;
+  }
+  if (t.IsGlobalInvocationId()) {
+    next();  // consume the peek
+    return ast::Builtin::kGlobalInvocationId;
+  }
+  return ast::Builtin::kNone;
+}
+
+// variable_decl
+//   : VAR variable_storage_decoration? variable_ident_decl
+std::unique_ptr<ast::Variable> ParserImpl::variable_decl() {
+  auto t = peek();
+  auto source = t.source();
+  if (!t.IsVar())
+    return nullptr;
+
+  next();  // Consume the peek
+
+  auto sc = variable_storage_decoration();
+  if (has_error())
+    return {};
+
+  std::string name;
+  ast::type::Type* type;
+  std::tie(name, type) = variable_ident_decl();
+  if (has_error())
+    return nullptr;
+  if (name == "" || type == nullptr) {
+    set_error(peek(), "invalid identifier declaration");
+    return nullptr;
+  }
+
+  return std::make_unique<ast::Variable>(source, name, sc, type);
+}
+
+// variable_ident_decl
+//   : IDENT COLON type_decl
+std::pair<std::string, ast::type::Type*> ParserImpl::variable_ident_decl() {
+  auto t = peek();
+  if (!t.IsIdentifier())
+    return {};
+
+  auto name = t.to_str();
+  next();  // Consume the peek
+
+  t = next();
+  if (!t.IsColon()) {
+    set_error(t, "missing : for identifier declaration");
+    return {};
+  }
+
+  auto type = type_decl();
+  if (has_error())
+    return {};
+  if (type == nullptr) {
+    set_error(peek(), "invalid type for identifier declaration");
+    return {};
+  }
+
+  return {name, type};
+}
+
+// variable_storage_decoration
+//   : LESS_THAN storage_class GREATER_THAN
+ast::StorageClass ParserImpl::variable_storage_decoration() {
+  auto t = peek();
+  if (!t.IsLessThan())
+    return ast::StorageClass::kNone;
+
+  next();  // Consume the peek
+
+  auto sc = storage_class();
+  if (has_error())
+    return sc;
+  if (sc == ast::StorageClass::kNone) {
+    set_error(peek(), "invalid storage class for variable decoration");
+    return sc;
+  }
+
+  t = next();
+  if (!t.IsGreaterThan()) {
+    set_error(t, "missing > for variable decoration");
+    return ast::StorageClass::kNone;
+  }
+
+  return sc;
+}
+
+// type_alias
+//   : TYPE IDENT EQUAL type_decl
+//   | TYPE IDENT EQUAL struct_decl
+ast::type::AliasType* ParserImpl::type_alias() {
+  auto t = peek();
+  if (!t.IsType())
+    return nullptr;
+
+  next();  // Consume the peek
+
+  t = next();
+  if (!t.IsIdentifier()) {
+    set_error(t, "missing identifier for type alias");
+    return nullptr;
+  }
+  auto name = t.to_str();
+
+  t = next();
+  if (!t.IsEqual()) {
+    set_error(t, "missing = for type alias");
+    return nullptr;
+  }
+
+  auto tm = TypeManager::Instance();
+
+  auto type = type_decl();
+  if (has_error())
+    return nullptr;
+  if (type == nullptr) {
+    auto str = struct_decl();
+    if (has_error())
+      return nullptr;
+    if (str == nullptr) {
+      set_error(peek(), "invalid type alias");
+      return nullptr;
+    }
+
+    str->set_name(name);
+    type = tm->Get(std::move(str));
+  }
+  if (type == nullptr) {
+    set_error(peek(), "invalid type for alias");
+    return nullptr;
+  }
+
+  auto alias = tm->Get(std::make_unique<ast::type::AliasType>(name, type));
+  register_alias(name, alias);
+
+  return alias->AsAlias();
+}
+
+// type_decl
+//   : IDENTIFIER
+//   | BOOL
+//   | FLOAT32
+//   | INT32
+//   | UINT32
+//   | VEC2 LESS_THAN type_decl GREATER_THAN
+//   | VEC3 LESS_THAN type_decl GREATER_THAN
+//   | VEC3 LESS_THAN type_decl GREATER_THAN
+//   | PTR LESS_THAN storage_class, type_decl GREATER_THAN
+//   | ARRAY LESS_THAN type_decl COMMA INT_LITERAL GREATER_THAN
+//   | 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
+ast::type::Type* ParserImpl::type_decl() {
+  auto tm = TypeManager::Instance();
+
+  auto t = peek();
+  if (t.IsIdentifier()) {
+    next();  // Consume the peek
+    auto alias = get_alias(t.to_str());
+    if (alias == nullptr) {
+      set_error(t, "unknown type alias '" + t.to_str() + "'");
+      return nullptr;
+    }
+    return alias;
+  }
+  if (t.IsBool()) {
+    next();  // Consume the peek
+    return tm->Get(std::make_unique<ast::type::BoolType>());
+  }
+  if (t.IsF32()) {
+    next();  // Consume the peek
+    return tm->Get(std::make_unique<ast::type::F32Type>());
+  }
+  if (t.IsI32()) {
+    next();  // Consume the peek
+    return tm->Get(std::make_unique<ast::type::I32Type>());
+  }
+  if (t.IsU32()) {
+    next();  // Consume the peek
+    return tm->Get(std::make_unique<ast::type::U32Type>());
+  }
+  if (t.IsVec2() || t.IsVec3() || t.IsVec4()) {
+    return type_decl_vector(t);
+  }
+  if (t.IsPtr()) {
+    return type_decl_pointer(t);
+  }
+  if (t.IsArray()) {
+    return type_decl_array(t);
+  }
+  if (t.IsMat2x2() || t.IsMat2x3() || t.IsMat2x4() || t.IsMat3x2() ||
+      t.IsMat3x3() || t.IsMat3x4() || t.IsMat4x2() || t.IsMat4x3() ||
+      t.IsMat4x4()) {
+    return type_decl_matrix(t);
+  }
+  return nullptr;
+}
+
+ast::type::Type* ParserImpl::type_decl_pointer(Token t) {
+  next();  // Consume the peek
+
+  t = next();
+  if (!t.IsLessThan()) {
+    set_error(t, "missing < for ptr declaration");
+    return nullptr;
+  }
+
+  auto sc = storage_class();
+  if (has_error())
+    return nullptr;
+  if (sc == ast::StorageClass::kNone) {
+    set_error(peek(), "missing storage class for ptr declaration");
+    return nullptr;
+  }
+
+  t = next();
+  if (!t.IsComma()) {
+    set_error(t, "missing , for ptr declaration");
+    return nullptr;
+  }
+
+  auto subtype = type_decl();
+  if (has_error())
+    return nullptr;
+  if (subtype == nullptr) {
+    set_error(peek(), "missing type for ptr declaration");
+    return nullptr;
+  }
+
+  t = next();
+  if (!t.IsGreaterThan()) {
+    set_error(t, "missing > for ptr declaration");
+    return nullptr;
+  }
+
+  return TypeManager::Instance()->Get(
+      std::make_unique<ast::type::PointerType>(subtype, sc));
+}
+
+ast::type::Type* ParserImpl::type_decl_vector(Token t) {
+  next();  // Consume the peek
+
+  size_t count = 2;
+  if (t.IsVec3())
+    count = 3;
+  else if (t.IsVec4())
+    count = 4;
+
+  t = next();
+  if (!t.IsLessThan()) {
+    set_error(t, "missing < for vector");
+    return nullptr;
+  }
+
+  auto subtype = type_decl();
+  if (has_error())
+    return nullptr;
+  if (subtype == nullptr) {
+    set_error(peek(), "unable to determine subtype for vector");
+    return nullptr;
+  }
+
+  t = next();
+  if (!t.IsGreaterThan()) {
+    set_error(t, "missing > for vector");
+    return nullptr;
+  }
+
+  return TypeManager::Instance()->Get(
+      std::make_unique<ast::type::VectorType>(subtype, count));
+}
+
+ast::type::Type* ParserImpl::type_decl_array(Token t) {
+  next();  // Consume the peek
+
+  t = next();
+  if (!t.IsLessThan()) {
+    set_error(t, "missing < for array declaration");
+    return nullptr;
+  }
+
+  auto subtype = type_decl();
+  if (has_error())
+    return nullptr;
+  if (subtype == nullptr) {
+    set_error(peek(), "invalid type for array declaration");
+    return nullptr;
+  }
+
+  t = next();
+  size_t size = 0;
+  if (t.IsComma()) {
+    t = next();
+    if (!t.IsIntLiteral()) {
+      set_error(t, "missing size of array declaration");
+      return nullptr;
+    }
+    if (t.to_i32() <= 0) {
+      set_error(t, "invalid size for array declaration");
+      return nullptr;
+    }
+    size = static_cast<size_t>(t.to_i32());
+    t = next();
+  }
+  if (!t.IsGreaterThan()) {
+    set_error(t, "missing > for array declaration");
+    return nullptr;
+  }
+
+  return TypeManager::Instance()->Get(
+      std::make_unique<ast::type::ArrayType>(subtype, size));
+}
+
+ast::type::Type* ParserImpl::type_decl_matrix(Token t) {
+  next();  // Consume the peek
+
+  size_t rows = 2;
+  size_t columns = 2;
+  if (t.IsMat3x2() || t.IsMat3x3() || t.IsMat3x4()) {
+    rows = 3;
+  } else if (t.IsMat4x2() || t.IsMat4x3() || t.IsMat4x4()) {
+    rows = 4;
+  }
+  if (t.IsMat2x3() || t.IsMat3x3() || t.IsMat4x3()) {
+    columns = 3;
+  } else if (t.IsMat2x4() || t.IsMat3x4() || t.IsMat4x4()) {
+    columns = 4;
+  }
+
+  t = next();
+  if (!t.IsLessThan()) {
+    set_error(t, "missing < for matrix");
+    return nullptr;
+  }
+
+  auto subtype = type_decl();
+  if (has_error())
+    return nullptr;
+  if (subtype == nullptr) {
+    set_error(peek(), "unable to determine subtype for matrix");
+    return nullptr;
+  }
+
+  t = next();
+  if (!t.IsGreaterThan()) {
+    set_error(t, "missing > for matrix");
+    return nullptr;
+  }
+
+  return TypeManager::Instance()->Get(
+      std::make_unique<ast::type::MatrixType>(subtype, rows, columns));
+}
+
+// storage_class
+//  : INPUT
+//  | OUTPUT
+//  | UNIFORM
+//  | WORKGROUP
+//  | UNIFORM_CONSTANT
+//  | STORAGE_BUFFER
+//  | IMAGE
+//  | PUSH_CONSTANT
+//  | PRIVATE
+//  | FUNCTION
+ast::StorageClass ParserImpl::storage_class() {
+  auto t = peek();
+  if (t.IsIn()) {
+    next();  // consume the peek
+    return ast::StorageClass::kInput;
+  }
+  if (t.IsOut()) {
+    next();  // consume the peek
+    return ast::StorageClass::kOutput;
+  }
+  if (t.IsUniform()) {
+    next();  // consume the peek
+    return ast::StorageClass::kUniform;
+  }
+  if (t.IsWorkgroup()) {
+    next();  // consume the peek
+    return ast::StorageClass::kWorkgroup;
+  }
+  if (t.IsUniformConstant()) {
+    next();  // consume the peek
+    return ast::StorageClass::kUniformConstant;
+  }
+  if (t.IsStorageBuffer()) {
+    next();  // consume the peek
+    return ast::StorageClass::kStorageBuffer;
+  }
+  if (t.IsImage()) {
+    next();  // consume the peek
+    return ast::StorageClass::kImage;
+  }
+  if (t.IsPushConstant()) {
+    next();  // consume the peek
+    return ast::StorageClass::kPushConstant;
+  }
+  if (t.IsPrivate()) {
+    next();  // consume the peek
+    return ast::StorageClass::kPrivate;
+  }
+  if (t.IsFunction()) {
+    next();  // consume the peek
+    return ast::StorageClass::kFunction;
+  }
+  return ast::StorageClass::kNone;
+}
+
+// struct_decl
+//   : struct_decoration_decl? STRUCT struct_body_decl
+std::unique_ptr<ast::type::StructType> ParserImpl::struct_decl() {
+  auto t = peek();
+  auto source = t.source();
+
+  auto deco = struct_decoration_decl();
+  if (has_error())
+    return nullptr;
+
+  t = next();
+  if (!t.IsStruct()) {
+    set_error(t, "missing struct declaration");
+    return nullptr;
+  }
+
+  t = peek();
+  if (!t.IsBracketLeft()) {
+    set_error(t, "missing { for struct declaration");
+    return nullptr;
+  }
+
+  auto body = struct_body_decl();
+  if (has_error()) {
+    return nullptr;
+  }
+
+  return std::make_unique<ast::type::StructType>(
+      std::make_unique<ast::Struct>(source, deco, std::move(body)));
+}
+
+// struct_decoration_decl
+//  : ATTR_LEFT struct_decoration ATTR_RIGHT
+ast::StructDecoration ParserImpl::struct_decoration_decl() {
+  auto t = peek();
+  if (!t.IsAttrLeft())
+    return ast::StructDecoration::kNone;
+
+  next();  // Consume the peek
+
+  auto deco = struct_decoration();
+  if (has_error())
+    return ast::StructDecoration::kNone;
+  if (deco == ast::StructDecoration::kNone) {
+    set_error(peek(), "unknown struct decoration");
+    return ast::StructDecoration::kNone;
+  }
+
+  t = next();
+  if (!t.IsAttrRight()) {
+    set_error(t, "missing ]] for struct decoration");
+    return ast::StructDecoration::kNone;
+  }
+
+  return deco;
+}
+
+// struct_decoration
+//  : BLOCK
+ast::StructDecoration ParserImpl::struct_decoration() {
+  auto t = peek();
+  if (t.IsBlock()) {
+    next();  // Consume the peek
+    return ast::StructDecoration::kBlock;
+  }
+  return ast::StructDecoration::kNone;
+}
+
+// struct_body_decl
+//   : BRACKET_LEFT struct_member* BRACKET_RIGHT
+std::vector<std::unique_ptr<ast::StructMember>> ParserImpl::struct_body_decl() {
+  auto t = peek();
+  if (!t.IsBracketLeft())
+    return {};
+
+  next();  // Consume the peek
+
+  t = peek();
+  if (t.IsBracketRight())
+    return {};
+
+  std::vector<std::unique_ptr<ast::StructMember>> members;
+  for (;;) {
+    auto mem = struct_member();
+    if (has_error())
+      return {};
+    if (mem == nullptr) {
+      set_error(peek(), "invalid struct member");
+      return {};
+    }
+
+    members.push_back(std::move(mem));
+
+    t = peek();
+    if (t.IsBracketRight() || t.IsEof())
+      break;
+  }
+
+  t = next();
+  if (!t.IsBracketRight()) {
+    set_error(t, "missing } for struct declaration");
+    return {};
+  }
+
+  return members;
+}
+
+// struct_member
+//   : struct_member_decoration_decl variable_ident_decl SEMICOLON
+std::unique_ptr<ast::StructMember> ParserImpl::struct_member() {
+  auto t = peek();
+  auto source = t.source();
+
+  auto decos = struct_member_decoration_decl();
+  if (has_error())
+    return nullptr;
+
+  std::string name;
+  ast::type::Type* type;
+  std::tie(name, type) = variable_ident_decl();
+  if (has_error())
+    return nullptr;
+  if (name == "" || type == nullptr) {
+    set_error(peek(), "invalid identifier declaration");
+    return nullptr;
+  }
+
+  t = next();
+  if (!t.IsSemicolon()) {
+    set_error(t, "missing ; for struct member");
+    return nullptr;
+  }
+
+  return std::make_unique<ast::StructMember>(source, name, type,
+                                             std::move(decos));
+}
+
+// struct_member_decoration_decl
+//   :
+//   | ATTR_LEFT (struct_member_decoration COMMA)*
+//                struct_member_decoration ATTR_RIGHT
+std::vector<std::unique_ptr<ast::StructMemberDecoration>>
+ParserImpl::struct_member_decoration_decl() {
+  auto t = peek();
+  if (!t.IsAttrLeft())
+    return {};
+
+  next();  // Consume the peek
+
+  t = peek();
+  if (t.IsAttrRight()) {
+    set_error(t, "empty struct member decoration found");
+    return {};
+  }
+
+  std::vector<std::unique_ptr<ast::StructMemberDecoration>> decos;
+  bool found_offset = false;
+  for (;;) {
+    auto deco = struct_member_decoration();
+    if (has_error())
+      return {};
+    if (deco == nullptr)
+      break;
+
+    if (deco->IsOffset()) {
+      if (found_offset) {
+        set_error(peek(), "duplicate offset decoration found");
+        return {};
+      }
+      found_offset = true;
+    }
+    decos.push_back(std::move(deco));
+
+    t = next();
+    if (!t.IsComma())
+      break;
+  }
+
+  if (!t.IsAttrRight()) {
+    set_error(t, "missing ]] for struct member decoration");
+    return {};
+  }
+  return decos;
+}
+
+// struct_member_decoration
+//   : OFFSET INT_LITERAL
+std::unique_ptr<ast::StructMemberDecoration>
+ParserImpl::struct_member_decoration() {
+  auto t = peek();
+  if (!t.IsOffset())
+    return nullptr;
+
+  next();  // Consume the peek
+
+  t = next();
+  if (!t.IsIntLiteral()) {
+    set_error(t, "invalid value for offset decoration");
+    return nullptr;
+  }
+  int32_t val = t.to_i32();
+  if (val < 0) {
+    set_error(t, "offset value must be >= 0");
+    return nullptr;
+  }
+
+  return std::make_unique<ast::StructMemberOffsetDecoration>(
+      static_cast<size_t>(val));
+}
+
+// function_decl
+//   : function_header body_stmt
+std::unique_ptr<ast::Function> ParserImpl::function_decl() {
+  auto f = function_header();
+  if (has_error())
+    return nullptr;
+  if (f == nullptr)
+    return nullptr;
+
+  auto body = body_stmt();
+  if (has_error())
+    return nullptr;
+
+  f->set_body(std::move(body));
+  return f;
+}
+
+// function_type_decl
+//   : type_decl
+//   | VOID
+ast::type::Type* ParserImpl::function_type_decl() {
+  auto tm = TypeManager::Instance();
+
+  auto t = peek();
+  if (t.IsVoid()) {
+    next();  // Consume the peek
+    return tm->Get(std::make_unique<ast::type::VoidType>());
+  }
+  return type_decl();
+}
+
+// function_header
+//   : FN IDENT PAREN_LEFT param_list PAREN_RIGHT ARROW function_type_decl
+std::unique_ptr<ast::Function> ParserImpl::function_header() {
+  auto t = peek();
+  if (!t.IsFn())
+    return nullptr;
+
+  auto source = t.source();
+  next();  // Consume the peek
+
+  t = next();
+  if (!t.IsIdentifier()) {
+    set_error(t, "missing identifier for function");
+    return nullptr;
+  }
+  auto name = t.to_str();
+
+  t = next();
+  if (!t.IsParenLeft()) {
+    set_error(t, "missing ( for function declaration");
+    return nullptr;
+  }
+
+  auto params = param_list();
+  if (has_error())
+    return nullptr;
+
+  t = next();
+  if (!t.IsParenRight()) {
+    set_error(t, "missing ) for function declaration");
+    return nullptr;
+  }
+
+  t = next();
+  if (!t.IsArrow()) {
+    set_error(t, "missing -> for function declaration");
+    return nullptr;
+  }
+
+  auto type = function_type_decl();
+  if (has_error())
+    return nullptr;
+  if (type == nullptr) {
+    set_error(peek(), "unable to determine function return type");
+    return nullptr;
+  }
+
+  return std::make_unique<ast::Function>(source, name, std::move(params), type);
+}
+
+// param_list
+//   :
+//   | (variable_ident_decl COMMA)* variable_ident_decl
+std::vector<std::unique_ptr<ast::Variable>> ParserImpl::param_list() {
+  auto t = peek();
+  auto source = t.source();
+
+  std::vector<std::unique_ptr<ast::Variable>> ret;
+
+  std::string name;
+  ast::type::Type* type;
+  std::tie(name, type) = variable_ident_decl();
+  if (has_error())
+    return {};
+  if (name == "" || type == nullptr)
+    return {};
+
+  for (;;) {
+    ret.push_back(std::make_unique<ast::Variable>(
+        source, name, ast::StorageClass::kNone, type));
+
+    t = peek();
+    if (!t.IsComma())
+      break;
+
+    source = t.source();
+    next();  // Consume the peek
+
+    std::tie(name, type) = variable_ident_decl();
+    if (has_error())
+      return {};
+    if (name == "" || type == nullptr) {
+      set_error(t, "found , but no variable declaration");
+      return {};
+    }
+  }
+
+  return ret;
+}
+
+// entry_point_decl
+//   : ENTRY_POINT pipeline_stage EQUAL IDENT
+//   | ENTRY_POINT pipeline_stage AS STRING_LITERAL EQUAL IDENT
+//   | ENTRY_POINT pipeline_stage AS IDENT EQUAL IDENT
+std::unique_ptr<ast::EntryPoint> ParserImpl::entry_point_decl() {
+  auto t = peek();
+  auto source = t.source();
+  if (!t.IsEntryPoint())
+    return nullptr;
+
+  next();  // Consume the peek
+
+  auto stage = pipeline_stage();
+  if (has_error())
+    return nullptr;
+  if (stage == ast::PipelineStage::kNone) {
+    set_error(peek(), "missing pipeline stage for entry point");
+    return nullptr;
+  }
+
+  t = next();
+  std::string name;
+  if (t.IsAs()) {
+    t = next();
+    if (t.IsStringLiteral()) {
+      name = t.to_str();
+    } else if (t.IsIdentifier()) {
+      name = t.to_str();
+    } else {
+      set_error(t, "invalid name for entry point");
+      return nullptr;
+    }
+    t = next();
+  }
+
+  if (!t.IsEqual()) {
+    set_error(t, "missing = for entry point");
+    return nullptr;
+  }
+
+  t = next();
+  if (!t.IsIdentifier()) {
+    set_error(t, "invalid function name for entry point");
+    return nullptr;
+  }
+  auto fn_name = t.to_str();
+
+  // Set the name to the function name if it isn't provided
+  if (name.length() == 0)
+    name = fn_name;
+
+  return std::make_unique<ast::EntryPoint>(source, stage, name, fn_name);
+}
+
+// pipeline_stage
+//   : VERTEX
+//   | FRAGMENT
+//   | COMPUTE
+ast::PipelineStage ParserImpl::pipeline_stage() {
+  auto t = peek();
+  if (t.IsVertex()) {
+    next();  // consume the peek
+    return ast::PipelineStage::kVertex;
+  }
+  if (t.IsFragment()) {
+    next();  // consume the peek
+    return ast::PipelineStage::kFragment;
+  }
+  if (t.IsCompute()) {
+    next();  // consume the peek
+    return ast::PipelineStage::kCompute;
+  }
+  return ast::PipelineStage::kNone;
+}
+
+// body_stmt
+//   : BRACKET_LEFT statements BRACKET_RIGHT
+std::vector<std::unique_ptr<ast::Statement>> ParserImpl::body_stmt() {
+  auto t = peek();
+  if (!t.IsBracketLeft())
+    return {};
+
+  next();  // Consume the peek
+
+  auto stmts = statements();
+  if (has_error())
+    return {};
+
+  t = next();
+  if (!t.IsBracketRight()) {
+    set_error(t, "missing }");
+    return {};
+  }
+
+  return stmts;
+}
+
+// paren_rhs_stmt
+//   : PAREN_LEFT logical_or_expression PAREN_RIGHT
+std::unique_ptr<ast::Expression> ParserImpl::paren_rhs_stmt() {
+  auto t = peek();
+  if (!t.IsParenLeft()) {
+    set_error(t, "expected (");
+    return nullptr;
+  }
+  next();  // Consume the peek
+
+  auto expr = logical_or_expression();
+  if (has_error())
+    return nullptr;
+  if (expr == nullptr) {
+    set_error(peek(), "unable to parse expression");
+    return nullptr;
+  }
+
+  t = next();
+  if (!t.IsParenRight()) {
+    set_error(t, "expected )");
+    return nullptr;
+  }
+
+  return expr;
+}
+
+// statements
+//   : statement*
+std::vector<std::unique_ptr<ast::Statement>> ParserImpl::statements() {
+  std::vector<std::unique_ptr<ast::Statement>> ret;
+
+  for (;;) {
+    auto stmt = statement();
+    if (has_error())
+      return {};
+    if (stmt == nullptr)
+      break;
+
+    ret.push_back(std::move(stmt));
+  }
+
+  return ret;
+}
+
+// statement
+//   : SEMICOLON
+//   | RETURN logical_or_expression SEMICOLON
+//   | if_stmt
+//   | unless_stmt
+//   | regardless_stmt
+//   | switch_stmt
+//   | loop_stmt
+//   | variable_stmt SEMICOLON
+//   | break_stmt SEMICOLON
+//   | continue_stmt SEMICOLON
+//   | KILL SEMICOLON
+//   | NOP SEMICOLON
+//   | assignment_expression SEMICOLON
+std::unique_ptr<ast::Statement> ParserImpl::statement() {
+  auto t = peek();
+  if (t.IsSemicolon()) {
+    next();  // Consume the peek
+    return statement();
+  }
+
+  if (t.IsReturn()) {
+    auto source = t.source();
+    next();  // Consume the peek
+
+    t = peek();
+
+    std::unique_ptr<ast::Expression> expr = nullptr;
+    if (!t.IsSemicolon()) {
+      expr = logical_or_expression();
+      if (has_error())
+        return nullptr;
+    }
+
+    t = next();
+    if (!t.IsSemicolon()) {
+      set_error(t, "missing ;");
+      return nullptr;
+    }
+    return std::make_unique<ast::ReturnStatement>(source, std::move(expr));
+  }
+
+  auto stmt_if = if_stmt();
+  if (has_error())
+    return nullptr;
+  if (stmt_if != nullptr)
+    return stmt_if;
+
+  auto unless = unless_stmt();
+  if (has_error())
+    return nullptr;
+  if (unless != nullptr)
+    return unless;
+
+  auto regardless = regardless_stmt();
+  if (has_error())
+    return nullptr;
+  if (regardless != nullptr)
+    return regardless;
+
+  auto sw = switch_stmt();
+  if (has_error())
+    return nullptr;
+  if (sw != nullptr)
+    return sw;
+
+  auto loop = loop_stmt();
+  if (has_error())
+    return nullptr;
+  if (loop != nullptr)
+    return loop;
+
+  auto var = variable_stmt();
+  if (has_error())
+    return nullptr;
+  if (var != nullptr) {
+    t = next();
+    if (!t.IsSemicolon()) {
+      set_error(t, "missing ;");
+      return nullptr;
+    }
+    return var;
+  }
+
+  auto b = break_stmt();
+  if (has_error())
+    return nullptr;
+  if (b != nullptr) {
+    t = next();
+    if (!t.IsSemicolon()) {
+      set_error(t, "missing ;");
+      return nullptr;
+    }
+    return b;
+  }
+
+  auto cont = continue_stmt();
+  if (has_error())
+    return nullptr;
+  if (cont != nullptr) {
+    t = next();
+    if (!t.IsSemicolon()) {
+      set_error(t, "missing ;");
+      return nullptr;
+    }
+    return cont;
+  }
+
+  if (t.IsKill()) {
+    auto source = t.source();
+    next();  // Consume the peek
+
+    t = next();
+    if (!t.IsSemicolon()) {
+      set_error(t, "missing ;");
+      return nullptr;
+    }
+    return std::make_unique<ast::KillStatement>(source);
+  }
+
+  if (t.IsNop()) {
+    auto source = t.source();
+    next();  // Consume the peek
+
+    t = next();
+    if (!t.IsSemicolon()) {
+      set_error(t, "missing ;");
+      return nullptr;
+    }
+    return std::make_unique<ast::NopStatement>(source);
+  }
+
+  auto assign = assignment_stmt();
+  if (has_error())
+    return nullptr;
+  if (assign != nullptr) {
+    t = next();
+    if (!t.IsSemicolon()) {
+      set_error(t, "missing ;");
+      return nullptr;
+    }
+    return assign;
+  }
+
+  return nullptr;
+}
+
+// break_stmt
+//   : BREAK ({IF | UNLESS} paren_rhs_stmt)?
+std::unique_ptr<ast::BreakStatement> ParserImpl::break_stmt() {
+  auto t = peek();
+  if (!t.IsBreak())
+    return nullptr;
+
+  auto source = t.source();
+  next();  // Consume the peek
+
+  ast::StatementCondition condition = ast::StatementCondition::kNone;
+  std::unique_ptr<ast::Expression> conditional = nullptr;
+
+  t = peek();
+  if (t.IsIf() || t.IsUnless()) {
+    next();  // Consume the peek
+
+    if (t.IsIf())
+      condition = ast::StatementCondition::kIf;
+    else
+      condition = ast::StatementCondition::kUnless;
+
+    conditional = paren_rhs_stmt();
+    if (has_error())
+      return nullptr;
+    if (conditional == nullptr) {
+      set_error(peek(), "unable to parse conditional statement");
+      return nullptr;
+    }
+  }
+
+  return std::make_unique<ast::BreakStatement>(source, condition,
+                                               std::move(conditional));
+}
+
+// continue_stmt
+//   : CONTINUE ({IF | UNLESS} paren_rhs_stmt)?
+std::unique_ptr<ast::ContinueStatement> ParserImpl::continue_stmt() {
+  auto t = peek();
+  if (!t.IsContinue())
+    return nullptr;
+
+  auto source = t.source();
+  next();  // Consume the peek
+
+  ast::StatementCondition condition = ast::StatementCondition::kNone;
+  std::unique_ptr<ast::Expression> conditional = nullptr;
+
+  t = peek();
+  if (t.IsIf() || t.IsUnless()) {
+    next();  // Consume the peek
+
+    if (t.IsIf())
+      condition = ast::StatementCondition::kIf;
+    else
+      condition = ast::StatementCondition::kUnless;
+
+    conditional = paren_rhs_stmt();
+    if (has_error())
+      return nullptr;
+    if (conditional == nullptr) {
+      set_error(peek(), "unable to parse conditional statement");
+      return nullptr;
+    }
+  }
+
+  return std::make_unique<ast::ContinueStatement>(source, condition,
+                                                  std::move(conditional));
+}
+
+// variable_stmt
+//   : variable_decl
+//   | variable_decl EQUAL logical_or_expression
+//   | CONST variable_ident_decl EQUAL logical_or_expression
+std::unique_ptr<ast::VariableStatement> ParserImpl::variable_stmt() {
+  auto t = peek();
+  auto source = t.source();
+  if (t.IsConst()) {
+    next();  // Consume the peek
+
+    std::string name;
+    ast::type::Type* type;
+    std::tie(name, type) = variable_ident_decl();
+    if (has_error())
+      return nullptr;
+    if (name == "" || type == nullptr) {
+      set_error(peek(), "unable to parse variable declaration");
+      return nullptr;
+    }
+
+    t = next();
+    if (!t.IsEqual()) {
+      set_error(t, "missing = for constant declaration");
+      return nullptr;
+    }
+
+    auto initializer = logical_or_expression();
+    if (has_error())
+      return nullptr;
+    if (initializer == nullptr) {
+      set_error(peek(), "missing initializer for const declaration");
+      return nullptr;
+    }
+
+    auto var = std::make_unique<ast::Variable>(source, name,
+                                               ast::StorageClass::kNone, type);
+    var->set_is_const(true);
+    var->set_initializer(std::move(initializer));
+
+    return std::make_unique<ast::VariableStatement>(source, std::move(var));
+  }
+
+  auto var = variable_decl();
+  if (has_error())
+    return nullptr;
+  if (var == nullptr)
+    return nullptr;
+
+  t = peek();
+  if (t.IsEqual()) {
+    next();  // Consume the peek
+    auto initializer = logical_or_expression();
+    if (has_error())
+      return nullptr;
+    if (initializer == nullptr) {
+      set_error(peek(), "missing initializer for variable declaration");
+      return nullptr;
+    }
+    var->set_initializer(std::move(initializer));
+  }
+
+  return std::make_unique<ast::VariableStatement>(source, std::move(var));
+}
+
+// if_stmt
+//   : IF paren_rhs_stmt body_stmt
+//           {(elseif_stmt else_stmt?) | (else_stmt premerge_stmt?)}
+std::unique_ptr<ast::IfStatement> ParserImpl::if_stmt() {
+  auto t = peek();
+  if (!t.IsIf())
+    return nullptr;
+
+  auto source = t.source();
+  next();  // Consume the peek
+
+  auto condition = paren_rhs_stmt();
+  if (has_error())
+    return nullptr;
+  if (condition == nullptr) {
+    set_error(peek(), "unable to parse if condition");
+    return nullptr;
+  }
+
+  t = peek();
+  if (!t.IsBracketLeft()) {
+    set_error(t, "missing {");
+    return nullptr;
+  }
+
+  auto body = body_stmt();
+  if (has_error())
+    return nullptr;
+
+  auto elseif = elseif_stmt();
+  if (has_error())
+    return nullptr;
+
+  auto el = else_stmt();
+  if (has_error())
+    return nullptr;
+
+  auto stmt = std::make_unique<ast::IfStatement>(source, std::move(condition),
+                                                 std::move(body));
+  if (el != nullptr) {
+    if (elseif.size() == 0) {
+      auto premerge = premerge_stmt();
+      if (has_error())
+        return nullptr;
+
+      stmt->set_premerge(std::move(premerge));
+    }
+    elseif.push_back(std::move(el));
+  }
+  stmt->set_else_statements(std::move(elseif));
+
+  return stmt;
+}
+
+// elseif_stmt
+//   : ELSE_IF paren_rhs_stmt body_stmt elseif_stmt?
+std::vector<std::unique_ptr<ast::ElseStatement>> ParserImpl::elseif_stmt() {
+  auto t = peek();
+  if (!t.IsElseIf())
+    return {};
+
+  std::vector<std::unique_ptr<ast::ElseStatement>> ret;
+  for (;;) {
+    auto source = t.source();
+    next();  // Consume the peek
+
+    auto condition = paren_rhs_stmt();
+    if (has_error())
+      return {};
+    if (condition == nullptr) {
+      set_error(peek(), "unable to parse condition expression");
+      return {};
+    }
+
+    t = peek();
+    if (!t.IsBracketLeft()) {
+      set_error(t, "missing {");
+      return {};
+    }
+
+    auto body = body_stmt();
+    if (has_error())
+      return {};
+
+    ret.push_back(std::make_unique<ast::ElseStatement>(
+        source, std::move(condition), std::move(body)));
+
+    t = peek();
+    if (!t.IsElseIf())
+      break;
+  }
+
+  return ret;
+}
+
+// else_stmt
+//   : ELSE body_stmt
+std::unique_ptr<ast::ElseStatement> ParserImpl::else_stmt() {
+  auto t = peek();
+  if (!t.IsElse())
+    return nullptr;
+
+  auto source = t.source();
+  next();  // Consume the peek
+
+  t = peek();
+  if (!t.IsBracketLeft()) {
+    set_error(t, "missing {");
+    return nullptr;
+  }
+
+  auto body = body_stmt();
+  if (has_error())
+    return nullptr;
+
+  return std::make_unique<ast::ElseStatement>(source, std::move(body));
+}
+
+// premerge_stmt
+//   : PREMERGE body_stmt
+std::vector<std::unique_ptr<ast::Statement>> ParserImpl::premerge_stmt() {
+  auto t = peek();
+  if (!t.IsPremerge())
+    return {};
+
+  next();  // Consume the peek
+  return body_stmt();
+}
+
+// unless_stmt
+//   : UNLESS paren_rhs_stmt body_stmt
+std::unique_ptr<ast::UnlessStatement> ParserImpl::unless_stmt() {
+  auto t = peek();
+  if (!t.IsUnless())
+    return nullptr;
+
+  auto source = t.source();
+  next();  // Consume the peek
+
+  auto condition = paren_rhs_stmt();
+  if (has_error())
+    return nullptr;
+  if (condition == nullptr) {
+    set_error(peek(), "unable to parse unless condition");
+    return nullptr;
+  }
+
+  auto body = body_stmt();
+  if (has_error())
+    return nullptr;
+
+  return std::make_unique<ast::UnlessStatement>(source, std::move(condition),
+                                                std::move(body));
+}
+
+// regardless_stmt
+//   : REGARDLESS paren_rhs_stmt body_stmt
+std::unique_ptr<ast::RegardlessStatement> ParserImpl::regardless_stmt() {
+  auto t = peek();
+  if (!t.IsRegardless())
+    return nullptr;
+
+  auto source = t.source();
+  next();  // Consume the peek
+
+  auto condition = paren_rhs_stmt();
+  if (has_error())
+    return nullptr;
+  if (condition == nullptr) {
+    set_error(peek(), "unable to parse regardless condition");
+    return nullptr;
+  }
+
+  auto body = body_stmt();
+  if (has_error())
+    return nullptr;
+
+  return std::make_unique<ast::RegardlessStatement>(
+      source, std::move(condition), std::move(body));
+}
+
+// switch_stmt
+//   : SWITCH paren_rhs_stmt BRACKET_LEFT switch_body+ BRACKET_RIGHT
+std::unique_ptr<ast::SwitchStatement> ParserImpl::switch_stmt() {
+  auto t = peek();
+  if (!t.IsSwitch())
+    return nullptr;
+
+  auto source = t.source();
+  next();  // Consume the peek
+
+  auto condition = paren_rhs_stmt();
+  if (has_error())
+    return nullptr;
+  if (condition == nullptr) {
+    set_error(peek(), "unable to parse switch expression");
+    return nullptr;
+  }
+
+  t = next();
+  if (!t.IsBracketLeft()) {
+    set_error(t, "missing { for switch statement");
+    return nullptr;
+  }
+
+  std::vector<std::unique_ptr<ast::CaseStatement>> body;
+  for (;;) {
+    auto stmt = switch_body();
+    if (has_error())
+      return nullptr;
+    if (stmt == nullptr)
+      break;
+
+    body.push_back(std::move(stmt));
+  }
+
+  t = next();
+  if (!t.IsBracketRight()) {
+    set_error(t, "missing } for switch statement");
+    return nullptr;
+  }
+  return std::make_unique<ast::SwitchStatement>(source, std::move(condition),
+                                                std::move(body));
+}
+
+// switch_body
+//   : CASE const_literal COLON BRACKET_LEFT case_body BRACKET_RIGHT
+//   | DEFAULT COLON BRACKET_LEFT case_body BRACKET_RIGHT
+std::unique_ptr<ast::CaseStatement> ParserImpl::switch_body() {
+  auto t = peek();
+  if (!t.IsCase() && !t.IsDefault())
+    return nullptr;
+
+  auto source = t.source();
+  next();  // Consume the peek
+
+  auto stmt = std::make_unique<ast::CaseStatement>();
+  stmt->set_source(source);
+  if (t.IsCase()) {
+    auto cond = const_literal();
+    if (has_error())
+      return nullptr;
+    if (cond == nullptr) {
+      set_error(peek(), "unable to parse case conditional");
+      return nullptr;
+    }
+    stmt->set_condition(std::move(cond));
+  }
+
+  t = next();
+  if (!t.IsColon()) {
+    set_error(t, "missing : for case statement");
+    return nullptr;
+  }
+
+  t = next();
+  if (!t.IsBracketLeft()) {
+    set_error(t, "missing { for case statement");
+    return nullptr;
+  }
+
+  auto body = case_body();
+  if (has_error())
+    return nullptr;
+
+  stmt->set_body(std::move(body));
+
+  t = next();
+  if (!t.IsBracketRight()) {
+    set_error(t, "missing } for case statement");
+    return nullptr;
+  }
+
+  return stmt;
+}
+
+// case_body
+//   :
+//   | statement case_body
+//   | FALLTHROUGH SEMICOLON
+std::vector<std::unique_ptr<ast::Statement>> ParserImpl::case_body() {
+  std::vector<std::unique_ptr<ast::Statement>> ret;
+  for (;;) {
+    auto t = peek();
+    if (t.IsFallthrough()) {
+      auto source = t.source();
+      next();  // Consume the peek
+
+      t = next();
+      if (!t.IsSemicolon()) {
+        set_error(t, "missing ;");
+        return {};
+      }
+
+      ret.push_back(std::make_unique<ast::FallthroughStatement>(source));
+      break;
+    }
+
+    auto stmt = statement();
+    if (has_error())
+      return {};
+    if (stmt == nullptr)
+      break;
+
+    ret.push_back(std::move(stmt));
+  }
+
+  return ret;
+}
+
+// loop_stmt
+//   : LOOP BRACKET_LEFT statements continuing_stmt? BRACKET_RIGHT
+std::unique_ptr<ast::LoopStatement> ParserImpl::loop_stmt() {
+  auto t = peek();
+  if (!t.IsLoop())
+    return nullptr;
+
+  auto source = t.source();
+  next();  // Consume the peek
+
+  t = next();
+  if (!t.IsBracketLeft()) {
+    set_error(t, "missing { for loop");
+    return nullptr;
+  }
+
+  auto body = statements();
+  if (has_error())
+    return nullptr;
+
+  auto continuing = continuing_stmt();
+  if (has_error())
+    return nullptr;
+
+  t = next();
+  if (!t.IsBracketRight()) {
+    set_error(t, "missing } for loop");
+    return nullptr;
+  }
+
+  return std::make_unique<ast::LoopStatement>(source, std::move(body),
+                                              std::move(continuing));
+}
+
+// continuing_stmt
+//   : CONTINUING body_stmt
+std::vector<std::unique_ptr<ast::Statement>> ParserImpl::continuing_stmt() {
+  auto t = peek();
+  if (!t.IsContinuing())
+    return {};
+
+  next();  // Consume the peek
+  return body_stmt();
+}
+
+// const_literal
+//   : INT_LITERAL
+//   | UINT_LITERAL
+//   | FLOAT_LITERAL
+//   | TRUE
+//   | FALSE
+std::unique_ptr<ast::Literal> ParserImpl::const_literal() {
+  auto t = peek();
+  if (t.IsTrue()) {
+    next();  // Consume the peek
+    return std::make_unique<ast::BoolLiteral>(true);
+  }
+  if (t.IsFalse()) {
+    next();  // Consume the peek
+    return std::make_unique<ast::BoolLiteral>(false);
+  }
+  if (t.IsIntLiteral()) {
+    next();  // Consume the peek
+    return std::make_unique<ast::IntLiteral>(t.to_i32());
+  }
+  if (t.IsUintLiteral()) {
+    next();  // Consume the peek
+    return std::make_unique<ast::UintLiteral>(t.to_u32());
+  }
+  if (t.IsFloatLiteral()) {
+    next();  // Consume the peek
+    return std::make_unique<ast::FloatLiteral>(t.to_f32());
+  }
+  return nullptr;
+}
+
+// const_expr
+//   : type_decl PAREN_LEFT (const_expr COMMA)? const_expr PAREN_RIGHT
+//   | const_literal
+std::unique_ptr<ast::InitializerExpression> ParserImpl::const_expr() {
+  auto t = peek();
+  auto source = t.source();
+
+  auto type = type_decl();
+  if (type != nullptr) {
+    t = next();
+    if (!t.IsParenLeft()) {
+      set_error(t, "missing ( for type initializer");
+      return nullptr;
+    }
+
+    std::vector<std::unique_ptr<ast::Expression>> params;
+    auto param = const_expr();
+    if (has_error())
+      return nullptr;
+    if (param == nullptr) {
+      set_error(peek(), "unable to parse constant expression");
+      return nullptr;
+    }
+    params.push_back(std::move(param));
+    for (;;) {
+      t = peek();
+      if (!t.IsComma())
+        break;
+
+      next();  // Consume the peek
+
+      param = const_expr();
+      if (has_error())
+        return nullptr;
+      if (param == nullptr) {
+        set_error(peek(), "unable to parse constant expression");
+        return nullptr;
+      }
+      params.push_back(std::move(param));
+    }
+
+    t = next();
+    if (!t.IsParenRight()) {
+      set_error(t, "missing ) for type initializer");
+      return nullptr;
+    }
+    return std::make_unique<ast::TypeInitializerExpression>(source, type,
+                                                            std::move(params));
+  }
+
+  auto lit = const_literal();
+  if (has_error())
+    return nullptr;
+  if (lit == nullptr) {
+    set_error(peek(), "unable to parse const literal");
+    return nullptr;
+  }
+  return std::make_unique<ast::ConstInitializerExpression>(source,
+                                                           std::move(lit));
+}
+
+// primary_expression
+//   : (IDENT NAMESPACE)* IDENT
+//   | type_decl PAREN_LEFT argument_expression_list PAREN_RIGHT
+//   | const_literal
+//   | paren_rhs_stmt
+//   | CAST LESS_THAN type_decl GREATER_THAN paren_rhs_stmt
+//   | AS LESS_THAN type_decl GREATER_THAN paren_rhs_stmt
+std::unique_ptr<ast::Expression> ParserImpl::primary_expression() {
+  auto t = peek();
+  auto source = t.source();
+
+  auto lit = const_literal();
+  if (has_error())
+    return nullptr;
+  if (lit != nullptr) {
+    return std::make_unique<ast::ConstInitializerExpression>(source,
+                                                             std::move(lit));
+  }
+
+  t = peek();
+  if (t.IsParenLeft()) {
+    auto paren = paren_rhs_stmt();
+    if (has_error())
+      return nullptr;
+
+    return paren;
+  }
+
+  if (t.IsCast() || t.IsAs()) {
+    auto src = t;
+
+    next();  // Consume the peek
+
+    t = next();
+    if (!t.IsLessThan()) {
+      set_error(t, "missing < for " + src.to_name() + " expression");
+      return nullptr;
+    }
+
+    auto type = type_decl();
+    if (has_error())
+      return nullptr;
+    if (type == nullptr) {
+      set_error(peek(), "missing type for " + src.to_name() + " expression");
+      return nullptr;
+    }
+
+    t = next();
+    if (!t.IsGreaterThan()) {
+      set_error(t, "missing > for " + src.to_name() + " expression");
+      return nullptr;
+    }
+
+    auto params = paren_rhs_stmt();
+    if (has_error())
+      return nullptr;
+    if (params == nullptr) {
+      set_error(peek(), "unable to parse parameters");
+      return nullptr;
+    }
+
+    if (src.IsCast()) {
+      return std::make_unique<ast::CastExpression>(source, type,
+                                                   std::move(params));
+    } else {
+      return std::make_unique<ast::AsExpression>(source, type,
+                                                 std::move(params));
+    }
+
+  } else if (t.IsIdentifier()) {
+    next();  // Consume the peek
+
+    std::vector<std::string> ident;
+    ident.push_back(t.to_str());
+    for (;;) {
+      t = peek();
+      if (!t.IsNamespace())
+        break;
+
+      next();  // Consume the peek
+      t = next();
+      if (!t.IsIdentifier()) {
+        set_error(t, "identifier expected");
+        return nullptr;
+      }
+
+      ident.push_back(t.to_str());
+    }
+    return std::make_unique<ast::IdentifierExpression>(source,
+                                                       std::move(ident));
+  }
+
+  auto type = type_decl();
+  if (has_error())
+    return nullptr;
+  if (type != nullptr) {
+    t = next();
+    if (!t.IsParenLeft()) {
+      set_error(t, "missing ( for type initializer");
+      return nullptr;
+    }
+
+    auto params = argument_expression_list();
+    if (has_error())
+      return nullptr;
+
+    t = next();
+    if (!t.IsParenRight()) {
+      set_error(t, "missing ) for type initializer");
+      return nullptr;
+    }
+    return std::make_unique<ast::TypeInitializerExpression>(source, type,
+                                                            std::move(params));
+  }
+  return nullptr;
+}
+
+// argument_expression_list
+//   : (logical_or_expression COMMA)* logical_or_expression
+std::vector<std::unique_ptr<ast::Expression>>
+ParserImpl::argument_expression_list() {
+  auto arg = logical_or_expression();
+  if (has_error())
+    return {};
+  if (arg == nullptr) {
+    set_error(peek(), "unable to parse argument expression");
+    return {};
+  }
+
+  std::vector<std::unique_ptr<ast::Expression>> ret;
+  ret.push_back(std::move(arg));
+
+  for (;;) {
+    auto t = peek();
+    if (!t.IsComma())
+      break;
+
+    next();  // Consume the peek
+
+    arg = logical_or_expression();
+    if (has_error())
+      return {};
+    if (arg == nullptr) {
+      set_error(peek(), "unable to parse argument expression after comma");
+      return {};
+    }
+    ret.push_back(std::move(arg));
+  }
+  return ret;
+}
+
+// postfix_expr
+//   :
+//   | BRACE_LEFT logical_or_expression BRACE_RIGHT postfix_expr
+//   | PAREN_LEFT argument_expression_list* PAREN_RIGHT postfix_expr
+//   | PERIOD IDENTIFIER postfix_expr
+std::unique_ptr<ast::Expression> ParserImpl::postfix_expr(
+    std::unique_ptr<ast::Expression> prefix) {
+  std::unique_ptr<ast::Expression> expr = nullptr;
+
+  auto t = peek();
+  auto source = t.source();
+  if (t.IsBraceLeft()) {
+    next();  // Consume the peek
+
+    auto param = logical_or_expression();
+    if (has_error())
+      return nullptr;
+    if (param == nullptr) {
+      set_error(peek(), "unable to parse expression inside []");
+      return nullptr;
+    }
+
+    t = next();
+    if (!t.IsBraceRight()) {
+      set_error(t, "missing ] for array accessor");
+      return nullptr;
+    }
+    expr = std::make_unique<ast::ArrayAccessorExpression>(
+        source, std::move(prefix), std::move(param));
+
+  } else if (t.IsParenLeft()) {
+    next();  // Consume the peek
+
+    t = peek();
+    std::vector<std::unique_ptr<ast::Expression>> params;
+    if (!t.IsParenRight() && !t.IsEof()) {
+      params = argument_expression_list();
+      if (has_error())
+        return nullptr;
+    }
+
+    t = next();
+    if (!t.IsParenRight()) {
+      set_error(t, "missing ) for call expression");
+      return nullptr;
+    }
+    expr = std::make_unique<ast::CallExpression>(source, std::move(prefix),
+                                                 std::move(params));
+  } else if (t.IsPeriod()) {
+    next();  // Consume the peek
+
+    t = next();
+    if (!t.IsIdentifier()) {
+      set_error(t, "missing identifier for member accessor");
+      return nullptr;
+    }
+
+    expr = std::make_unique<ast::MemberAccessorExpression>(
+        source, std::move(prefix),
+        std::make_unique<ast::IdentifierExpression>(t.source(), t.to_str()));
+  } else {
+    return prefix;
+  }
+  return postfix_expr(std::move(expr));
+}
+
+// postfix_expression
+//   : primary_expression postfix_expr
+std::unique_ptr<ast::Expression> ParserImpl::postfix_expression() {
+  auto prefix = primary_expression();
+  if (has_error())
+    return nullptr;
+  if (prefix == nullptr)
+    return nullptr;
+
+  return postfix_expr(std::move(prefix));
+}
+
+// unary_expression
+//   : postfix_expression
+//   | MINUS unary_expression
+//   | BANG unary_expression
+//   | ANY PAREN_LEFT IDENT PAREN_RIGHT
+//   | ALL PAREN_LEFT IDENT PAREN_RIGHT
+//   | IS_NAN PAREN_LEFT IDENT PAREN_RIGHT
+//   | IS_INF PAREN_LEFT IDENT PAREN_RIGHT
+//   | IS_FINITE PAREN_LEFT IDENT PAREN_RIGHT
+//   | IS_NORMAL PAREN_LEFT IDENT PAREN_RIGHT
+//   | DOT PAREN_LEFT IDENT COMMA IDENT PAREN_RIGHT
+//   | OUTER_PRODUCT PAREN_LEFT IDENT COMMA IDENT PAREN_RIGHT
+//   | DPDX (LESS_THAN derivative_modifier GREATER_THAN)?
+//           PAREN_LEFT IDENT PAREN_RIGHT
+//   | DPDY (LESS_THAN derivative_modifier GREATER_THAN)?
+//           PAREN_LEFT IDENT PAREN_RIGHT
+//   | FWIDTH (LESS_THAN derivative_modifier GREATER_THAN)?
+//           PAREN_LEFT IDENT PAREN_RIGHT
+// # | unord_greater_than_equal(a, b)
+// # | unord_greater_than(a, b)
+// # | unord_less_than_equal(a, b)
+// # | unord_less_than(a, b)
+// # | unord_not_equal(a, b)
+// # | unord_equal(a, b)
+// # | signed_greater_than_equal(a, b)
+// # | signed_greater_than(a, b)
+// # | signed_less_than_equal(a, b)
+// # | signed_less_than(a, b)
+std::unique_ptr<ast::Expression> ParserImpl::unary_expression() {
+  auto t = peek();
+  auto source = t.source();
+  if (t.IsMinus() || t.IsBang()) {
+    auto name = t.to_name();
+
+    next();  // Consume the peek
+
+    auto op = ast::UnaryOp::kNegation;
+    if (t.IsBang())
+      op = ast::UnaryOp::kNot;
+
+    auto expr = unary_expression();
+    if (has_error())
+      return nullptr;
+    if (expr == nullptr) {
+      set_error(peek(),
+                "unable to parse right side of " + name + " expression");
+      return nullptr;
+    }
+    return std::make_unique<ast::UnaryOpExpression>(source, op,
+                                                    std::move(expr));
+  }
+  if (t.IsAny() || t.IsAll() || t.IsIsNan() || t.IsIsInf() || t.IsIsFinite() ||
+      t.IsIsNormal()) {
+    next();  // Consume the peek
+
+    auto op = ast::UnaryMethod::kAny;
+    if (t.IsAll())
+      op = ast::UnaryMethod::kAll;
+    else if (t.IsIsNan())
+      op = ast::UnaryMethod::kIsNan;
+    else if (t.IsIsInf())
+      op = ast::UnaryMethod::kIsInf;
+    else if (t.IsIsFinite())
+      op = ast::UnaryMethod::kIsFinite;
+    else if (t.IsIsNormal())
+      op = ast::UnaryMethod::kIsNormal;
+
+    t = next();
+    if (!t.IsParenLeft()) {
+      set_error(t, "missing ( for method call");
+      return nullptr;
+    }
+
+    t = next();
+    if (!t.IsIdentifier()) {
+      set_error(t, "missing identifier for method call");
+      return nullptr;
+    }
+    std::vector<std::unique_ptr<ast::Expression>> ident;
+    ident.push_back(
+        std::make_unique<ast::IdentifierExpression>(source, t.to_str()));
+
+    t = next();
+    if (!t.IsParenRight()) {
+      set_error(t, "missing ) for method call");
+      return nullptr;
+    }
+    return std::make_unique<ast::UnaryMethodExpression>(source, op,
+                                                        std::move(ident));
+  }
+  if (t.IsDot() || t.IsOuterProduct()) {
+    next();  // Consume the peek
+
+    auto op = ast::UnaryMethod::kDot;
+    if (t.IsOuterProduct())
+      op = ast::UnaryMethod::kOuterProduct;
+
+    t = next();
+    if (!t.IsParenLeft()) {
+      set_error(t, "missing ( for method call");
+      return nullptr;
+    }
+
+    t = next();
+    if (!t.IsIdentifier()) {
+      set_error(t, "missing identifier for method call");
+      return nullptr;
+    }
+    std::vector<std::unique_ptr<ast::Expression>> ident;
+    ident.push_back(
+        std::make_unique<ast::IdentifierExpression>(source, t.to_str()));
+
+    t = next();
+    if (!t.IsComma()) {
+      set_error(t, "missing , for method call");
+      return nullptr;
+    }
+
+    t = next();
+    if (!t.IsIdentifier()) {
+      set_error(t, "missing identifier for method call");
+      return nullptr;
+    }
+    ident.push_back(
+        std::make_unique<ast::IdentifierExpression>(source, t.to_str()));
+
+    t = next();
+    if (!t.IsParenRight()) {
+      set_error(t, "missing ) for method call");
+      return nullptr;
+    }
+
+    return std::make_unique<ast::UnaryMethodExpression>(source, op,
+                                                        std::move(ident));
+  }
+  if (t.IsDpdx() || t.IsDpdy() || t.IsFwidth()) {
+    next();  // Consume the peek
+
+    auto op = ast::UnaryDerivative::kDpdx;
+    if (t.IsDpdy())
+      op = ast::UnaryDerivative::kDpdy;
+    else if (t.IsFwidth())
+      op = ast::UnaryDerivative::kFwidth;
+
+    t = next();
+    auto mod = ast::DerivativeModifier::kNone;
+    if (t.IsLessThan()) {
+      mod = derivative_modifier();
+      if (has_error())
+        return nullptr;
+      if (mod == ast::DerivativeModifier::kNone) {
+        set_error(peek(), "unable to parse derivative modifier");
+        return nullptr;
+      }
+
+      t = next();
+      if (!t.IsGreaterThan()) {
+        set_error(t, "missing > for derivative modifier");
+        return nullptr;
+      }
+      t = next();
+    }
+    if (!t.IsParenLeft()) {
+      set_error(t, "missing ( for derivative method");
+      return nullptr;
+    }
+
+    t = next();
+    if (!t.IsIdentifier()) {
+      set_error(t, "missing identifier for derivative method");
+      return nullptr;
+    }
+    auto ident =
+        std::make_unique<ast::IdentifierExpression>(source, t.to_str());
+
+    t = next();
+    if (!t.IsParenRight()) {
+      set_error(t, "missing ) for derivative method");
+      return nullptr;
+    }
+
+    return std::make_unique<ast::UnaryDerivativeExpression>(source, op, mod,
+                                                            std::move(ident));
+  }
+  return postfix_expression();
+}
+
+// derivative_modifier
+//   : FINE
+//   | COARSE
+ast::DerivativeModifier ParserImpl::derivative_modifier() {
+  auto t = peek();
+  if (t.IsFine()) {
+    next();  // Consume the peek
+    return ast::DerivativeModifier::kFine;
+  }
+  if (t.IsCoarse()) {
+    next();  // Consume the peek
+    return ast::DerivativeModifier::kCoarse;
+  }
+  return ast::DerivativeModifier::kNone;
+}
+
+// multiplicative_expr
+//   :
+//   | STAR unary_expression multiplicative_expr
+//   | FORWARD_SLASH unary_expression multiplicative_expr
+//   | MODULO unary_expression multiplicative_expr
+std::unique_ptr<ast::Expression> ParserImpl::multiplicative_expr(
+    std::unique_ptr<ast::Expression> lhs) {
+  auto t = peek();
+
+  ast::Relation relation = ast::Relation::kNone;
+  if (t.IsStar())
+    relation = ast::Relation::kMultiply;
+  else if (t.IsForwardSlash())
+    relation = ast::Relation::kDivide;
+  else if (t.IsMod())
+    relation = ast::Relation::kModulo;
+  else
+    return lhs;
+
+  auto source = t.source();
+  auto name = t.to_name();
+  next();  // Consume the peek
+
+  auto rhs = unary_expression();
+  if (has_error())
+    return nullptr;
+  if (rhs == nullptr) {
+    set_error(peek(), "unable to parse right side of " + name + " expression");
+    return nullptr;
+  }
+  return multiplicative_expr(std::make_unique<ast::RelationalExpression>(
+      source, relation, std::move(lhs), std::move(rhs)));
+}
+
+// multiplicative_expression
+//   : unary_expression multiplicative_expr
+std::unique_ptr<ast::Expression> ParserImpl::multiplicative_expression() {
+  auto lhs = unary_expression();
+  if (has_error())
+    return nullptr;
+  if (lhs == nullptr)
+    return nullptr;
+
+  return multiplicative_expr(std::move(lhs));
+}
+
+// additive_expr
+//   :
+//   | PLUS multiplicative_expression additive_expr
+//   | MINUS multiplicative_expression additive_expr
+std::unique_ptr<ast::Expression> ParserImpl::additive_expr(
+    std::unique_ptr<ast::Expression> lhs) {
+  auto t = peek();
+
+  ast::Relation relation = ast::Relation::kNone;
+  if (t.IsPlus())
+    relation = ast::Relation::kAdd;
+  else if (t.IsMinus())
+    relation = ast::Relation::kSubtract;
+  else
+    return lhs;
+
+  auto source = t.source();
+  next();  // Consume the peek
+
+  auto rhs = multiplicative_expression();
+  if (has_error())
+    return nullptr;
+  if (rhs == nullptr) {
+    set_error(peek(), "unable to parse right side of + expression");
+    return nullptr;
+  }
+  return additive_expr(std::make_unique<ast::RelationalExpression>(
+      source, relation, std::move(lhs), std::move(rhs)));
+}
+
+// additive_expression
+//   : multiplicative_expression additive_expr
+std::unique_ptr<ast::Expression> ParserImpl::additive_expression() {
+  auto lhs = multiplicative_expression();
+  if (has_error())
+    return nullptr;
+  if (lhs == nullptr)
+    return nullptr;
+
+  return additive_expr(std::move(lhs));
+}
+
+// shift_expr
+//   :
+//   | LESS_THAN LESS_THAN additive_expression shift_expr
+//   | GREATER_THAN GREATER_THAN additive_expression shift_expr
+//   | GREATER_THAN GREATER_THAN GREATER_THAN additive_expression shift_expr
+std::unique_ptr<ast::Expression> ParserImpl::shift_expr(
+    std::unique_ptr<ast::Expression> lhs) {
+  auto t = peek();
+  auto source = t.source();
+  auto t2 = peek(1);
+  auto t3 = peek(2);
+
+  auto name = "";
+  ast::Relation relation = ast::Relation::kNone;
+  if (t.IsLessThan() && t2.IsLessThan()) {
+    next();  // Consume the t peek
+    next();  // Consume the t2 peek
+    relation = ast::Relation::kShiftLeft;
+    name = "<<";
+  } else if (t.IsGreaterThan() && t2.IsGreaterThan() && t3.IsGreaterThan()) {
+    next();  // Consume the t peek
+    next();  // Consume the t2 peek
+    next();  // Consume the t3 peek
+    relation = ast::Relation::kShiftRightArith;
+    name = ">>>";
+  } else if (t.IsGreaterThan() && t2.IsGreaterThan()) {
+    next();  // Consume the t peek
+    next();  // Consume the t2 peek
+    relation = ast::Relation::kShiftRight;
+    name = ">>";
+  } else {
+    return lhs;
+  }
+
+  auto rhs = additive_expression();
+  if (has_error())
+    return nullptr;
+  if (rhs == nullptr) {
+    set_error(peek(), std::string("unable to parse right side of ") + name +
+                          " expression");
+    return nullptr;
+  }
+  return shift_expr(std::make_unique<ast::RelationalExpression>(
+      source, relation, std::move(lhs), std::move(rhs)));
+}
+
+// shift_expression
+//   : additive_expression shift_expr
+std::unique_ptr<ast::Expression> ParserImpl::shift_expression() {
+  auto lhs = additive_expression();
+  if (has_error())
+    return nullptr;
+  if (lhs == nullptr)
+    return nullptr;
+
+  return shift_expr(std::move(lhs));
+}
+
+// relational_expr
+//   :
+//   | LESS_THAN shift_expression relational_expr
+//   | GREATER_THAN shift_expression relational_expr
+//   | LESS_THAN_EQUAL shift_expression relational_expr
+//   | GREATER_THAN_EQUAL shift_expression relational_expr
+std::unique_ptr<ast::Expression> ParserImpl::relational_expr(
+    std::unique_ptr<ast::Expression> lhs) {
+  auto t = peek();
+  ast::Relation relation = ast::Relation::kNone;
+  if (t.IsLessThan())
+    relation = ast::Relation::kLessThan;
+  else if (t.IsGreaterThan())
+    relation = ast::Relation::kGreaterThan;
+  else if (t.IsLessThanEqual())
+    relation = ast::Relation::kLessThanEqual;
+  else if (t.IsGreaterThanEqual())
+    relation = ast::Relation::kGreaterThanEqual;
+  else
+    return lhs;
+
+  auto source = t.source();
+  auto name = t.to_name();
+  next();  // Consume the peek
+
+  auto rhs = shift_expression();
+  if (has_error())
+    return nullptr;
+  if (rhs == nullptr) {
+    set_error(peek(), "unable to parse right side of " + name + " expression");
+    return nullptr;
+  }
+  return relational_expr(std::make_unique<ast::RelationalExpression>(
+      source, relation, std::move(lhs), std::move(rhs)));
+}
+
+// relational_expression
+//   : shift_expression relational_expr
+std::unique_ptr<ast::Expression> ParserImpl::relational_expression() {
+  auto lhs = shift_expression();
+  if (has_error())
+    return nullptr;
+  if (lhs == nullptr)
+    return nullptr;
+
+  return relational_expr(std::move(lhs));
+}
+
+// equality_expr
+//   :
+//   | EQUAL_EQUAL relational_expression equality_expr
+//   | NOT_EQUAL relational_expression equality_expr
+std::unique_ptr<ast::Expression> ParserImpl::equality_expr(
+    std::unique_ptr<ast::Expression> lhs) {
+  auto t = peek();
+  ast::Relation relation = ast::Relation::kNone;
+  if (t.IsEqualEqual())
+    relation = ast::Relation::kEqual;
+  else if (t.IsNotEqual())
+    relation = ast::Relation::kNotEqual;
+  else
+    return lhs;
+
+  auto source = t.source();
+  auto name = t.to_name();
+  next();  // Consume the peek
+
+  auto rhs = relational_expression();
+  if (has_error())
+    return nullptr;
+  if (rhs == nullptr) {
+    set_error(peek(), "unable to parse right side of " + name + " expression");
+    return nullptr;
+  }
+  return equality_expr(std::make_unique<ast::RelationalExpression>(
+      source, relation, std::move(lhs), std::move(rhs)));
+}
+
+// equality_expression
+//   : relational_expression equality_expr
+std::unique_ptr<ast::Expression> ParserImpl::equality_expression() {
+  auto lhs = relational_expression();
+  if (has_error())
+    return nullptr;
+  if (lhs == nullptr)
+    return nullptr;
+
+  return equality_expr(std::move(lhs));
+}
+
+// and_expr
+//   :
+//   | AND equality_expression and_expr
+std::unique_ptr<ast::Expression> ParserImpl::and_expr(
+    std::unique_ptr<ast::Expression> lhs) {
+  auto t = peek();
+  if (!t.IsAnd())
+    return lhs;
+
+  auto source = t.source();
+  next();  // Consume the peek
+
+  auto rhs = equality_expression();
+  if (has_error())
+    return nullptr;
+  if (rhs == nullptr) {
+    set_error(peek(), "unable to parse right side of & expression");
+    return nullptr;
+  }
+  return and_expr(std::make_unique<ast::RelationalExpression>(
+      source, ast::Relation::kAnd, std::move(lhs), std::move(rhs)));
+}
+
+// and_expression
+//   : equality_expression and_expr
+std::unique_ptr<ast::Expression> ParserImpl::and_expression() {
+  auto lhs = equality_expression();
+  if (has_error())
+    return nullptr;
+  if (lhs == nullptr)
+    return nullptr;
+
+  return and_expr(std::move(lhs));
+}
+
+// exclusive_or_expr
+//   :
+//   | XOR and_expression exclusive_or_expr
+std::unique_ptr<ast::Expression> ParserImpl::exclusive_or_expr(
+    std::unique_ptr<ast::Expression> lhs) {
+  auto t = peek();
+  if (!t.IsXor())
+    return lhs;
+
+  auto source = t.source();
+  next();  // Consume the peek
+
+  auto rhs = and_expression();
+  if (has_error())
+    return nullptr;
+  if (rhs == nullptr) {
+    set_error(peek(), "unable to parse right side of ^ expression");
+    return nullptr;
+  }
+  return exclusive_or_expr(std::make_unique<ast::RelationalExpression>(
+      source, ast::Relation::kXor, std::move(lhs), std::move(rhs)));
+}
+
+// exclusive_or_expression
+//   : and_expression exclusive_or_expr
+std::unique_ptr<ast::Expression> ParserImpl::exclusive_or_expression() {
+  auto lhs = and_expression();
+  if (has_error())
+    return nullptr;
+  if (lhs == nullptr)
+    return nullptr;
+
+  return exclusive_or_expr(std::move(lhs));
+}
+
+// inclusive_or_expr
+//   :
+//   | OR exclusive_or_expression inclusive_or_expr
+std::unique_ptr<ast::Expression> ParserImpl::inclusive_or_expr(
+    std::unique_ptr<ast::Expression> lhs) {
+  auto t = peek();
+  if (!t.IsOr())
+    return lhs;
+
+  auto source = t.source();
+  next();  // Consume the peek
+
+  auto rhs = exclusive_or_expression();
+  if (has_error())
+    return nullptr;
+  if (rhs == nullptr) {
+    set_error(peek(), "unable to parse right side of | expression");
+    return nullptr;
+  }
+  return inclusive_or_expr(std::make_unique<ast::RelationalExpression>(
+      source, ast::Relation::kOr, std::move(lhs), std::move(rhs)));
+}
+
+// inclusive_or_expression
+//   : exclusive_or_expression inclusive_or_expr
+std::unique_ptr<ast::Expression> ParserImpl::inclusive_or_expression() {
+  auto lhs = exclusive_or_expression();
+  if (has_error())
+    return nullptr;
+  if (lhs == nullptr)
+    return nullptr;
+
+  return inclusive_or_expr(std::move(lhs));
+}
+
+// logical_and_expr
+//   :
+//   | AND_AND inclusive_or_expression logical_and_expr
+std::unique_ptr<ast::Expression> ParserImpl::logical_and_expr(
+    std::unique_ptr<ast::Expression> lhs) {
+  auto t = peek();
+  if (!t.IsAndAnd())
+    return lhs;
+
+  auto source = t.source();
+  next();  // Consume the peek
+
+  auto rhs = inclusive_or_expression();
+  if (has_error())
+    return nullptr;
+  if (rhs == nullptr) {
+    set_error(peek(), "unable to parse right side of && expression");
+    return nullptr;
+  }
+  return logical_and_expr(std::make_unique<ast::RelationalExpression>(
+      source, ast::Relation::kLogicalAnd, std::move(lhs), std::move(rhs)));
+}
+
+// logical_and_expression
+//   : inclusive_or_expression logical_and_expr
+std::unique_ptr<ast::Expression> ParserImpl::logical_and_expression() {
+  auto lhs = inclusive_or_expression();
+  if (has_error())
+    return nullptr;
+  if (lhs == nullptr)
+    return nullptr;
+
+  return logical_and_expr(std::move(lhs));
+}
+
+// logical_or_expr
+//   :
+//   | OR_OR logical_and_expression logical_or_expr
+std::unique_ptr<ast::Expression> ParserImpl::logical_or_expr(
+    std::unique_ptr<ast::Expression> lhs) {
+  auto t = peek();
+  if (!t.IsOrOr())
+    return lhs;
+
+  auto source = t.source();
+  next();  // Consume the peek
+
+  auto rhs = logical_and_expression();
+  if (has_error())
+    return nullptr;
+  if (rhs == nullptr) {
+    set_error(peek(), "unable to parse right side of || expression");
+    return nullptr;
+  }
+  return logical_or_expr(std::make_unique<ast::RelationalExpression>(
+      source, ast::Relation::kLogicalOr, std::move(lhs), std::move(rhs)));
+}
+
+// logical_or_expression
+//   : logical_and_expression logical_or_expr
+std::unique_ptr<ast::Expression> ParserImpl::logical_or_expression() {
+  auto lhs = logical_and_expression();
+  if (has_error())
+    return nullptr;
+  if (lhs == nullptr)
+    return nullptr;
+
+  return logical_or_expr(std::move(lhs));
+}
+
+// assignment_stmt
+//   : unary_expression EQUAL logical_or_expression
+std::unique_ptr<ast::AssignmentStatement> ParserImpl::assignment_stmt() {
+  auto t = peek();
+  auto source = t.source();
+
+  auto lhs = unary_expression();
+  if (has_error())
+    return nullptr;
+  if (lhs == nullptr)
+    return nullptr;
+
+  t = next();
+  if (!t.IsEqual()) {
+    set_error(t, "missing = for assignment");
+    return nullptr;
+  }
+
+  auto rhs = logical_or_expression();
+  if (has_error())
+    return nullptr;
+  if (rhs == nullptr) {
+    set_error(peek(), "unable to parse right side of assignment");
+    return nullptr;
+  }
+
+  return std::make_unique<ast::AssignmentStatement>(source, std::move(lhs),
+                                                    std::move(rhs));
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl.h b/src/reader/wgsl/parser_impl.h
new file mode 100644
index 0000000..7fa3a23
--- /dev/null
+++ b/src/reader/wgsl/parser_impl.h
@@ -0,0 +1,363 @@
+// 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.
+
+#ifndef SRC_READER_WGSL_PARSER_IMPL_H_
+#define SRC_READER_WGSL_PARSER_IMPL_H_
+
+#include <deque>
+#include <memory>
+#include <string>
+#include <unordered_map>
+#include <utility>
+#include <vector>
+
+#include "src/ast/assignment_statement.h"
+#include "src/ast/builtin.h"
+#include "src/ast/derivative_modifier.h"
+#include "src/ast/entry_point.h"
+#include "src/ast/function.h"
+#include "src/ast/import.h"
+#include "src/ast/initializer_expression.h"
+#include "src/ast/literal.h"
+#include "src/ast/loop_statement.h"
+#include "src/ast/module.h"
+#include "src/ast/pipeline_stage.h"
+#include "src/ast/regardless_statement.h"
+#include "src/ast/statement.h"
+#include "src/ast/storage_class.h"
+#include "src/ast/struct.h"
+#include "src/ast/struct_decoration.h"
+#include "src/ast/struct_member.h"
+#include "src/ast/struct_member_decoration.h"
+#include "src/ast/type/type.h"
+#include "src/ast/unless_statement.h"
+#include "src/ast/variable.h"
+#include "src/ast/variable_decoration.h"
+#include "src/reader/wgsl/token.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+class Lexer;
+
+/// ParserImpl for WGSL source data
+class ParserImpl {
+ public:
+  /// Creates a new parser
+  /// @param input the input string to parse
+  explicit ParserImpl(const std::string& input);
+  ~ParserImpl();
+
+  /// Run the parser
+  /// @returns true if the parse was successful, false otherwise.
+  bool Parse();
+
+  /// @returns true if an error was encountered
+  bool has_error() const { return error_.size() > 0; }
+  /// @returns the parser error string
+  const std::string& error() const { return error_; }
+
+  /// @returns the module. The module in the parser will be reset after this.
+  ast::Module module() { return std::move(module_); }
+
+  /// @returns the next token
+  Token next();
+  /// @returns the next token without advancing
+  Token peek();
+  /// Peeks ahead and returns the token at |idx| head 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);
+  /// Sets the error from |t|
+  /// @param t the token to set the error from
+  void set_error(const Token& t);
+  /// Sets the error from |t| or |msg| if |t| is not in error
+  /// @param t the token to set the error from
+  /// @param msg the error message
+  void set_error(const Token& t, const std::string& msg);
+
+  /// Registers a type alias into the parser
+  /// @param name the alias name
+  /// @param type the alias'd type
+  void register_alias(const std::string& name, ast::type::Type* type);
+  /// Retrieves an aliased type
+  /// @param name The alias name to lookup
+  /// @returns the alias type for |name| or nullptr if not found
+  ast::type::Type* get_alias(const std::string& name);
+
+  /// Parses the `translation_unit` grammar element
+  void translation_unit();
+  /// Parses the `global_decl` grammar element
+  void global_decl();
+  /// Parses the `import_decl grammar element
+  /// @returns the import object or nullptr if an error was encountered
+  std::unique_ptr<ast::Import> import_decl();
+  /// Parses a `global_variable_decl` grammar element
+  /// @returns the variable parsed or nullptr
+  std::unique_ptr<ast::Variable> global_variable_decl();
+  /// Parses a `global_constant_decl` grammar element
+  /// @returns the const object or nullptr
+  std::unique_ptr<ast::Variable> global_constant_decl();
+  /// Parses a `variable_decoration_list` grammar element
+  /// @returns a vector of parsed variable decorations
+  std::vector<std::unique_ptr<ast::VariableDecoration>>
+  variable_decoration_list();
+  /// Parses a `variable_decoration` grammar element
+  /// @returns the variable decoration or nullptr if an error is encountered
+  std::unique_ptr<ast::VariableDecoration> variable_decoration();
+  /// Parses a `builtin_decoration` grammar element
+  /// @returns the builtin or Builtin::kNone if none matched
+  ast::Builtin builtin_decoration();
+  /// Parses a `variable_decl` grammar element
+  /// @returns the parsed variable or nullptr otherwise
+  std::unique_ptr<ast::Variable> variable_decl();
+  /// Parses a `variable_ident_decl` grammar element
+  /// @returns the identifier and type parsed or empty otherwise
+  std::pair<std::string, ast::type::Type*> variable_ident_decl();
+  /// Parses a `variable_storage_decoration` grammar element
+  /// @returns the storage class or StorageClass::kNone if none matched
+  ast::StorageClass variable_storage_decoration();
+  /// Parses a `type_alias` grammar element
+  /// @returns the type alias or nullptr on error
+  ast::type::AliasType* type_alias();
+  /// Parses a `type_decl` grammar element
+  /// @returns the parsed Type or nullptr if none matched. The returned type
+  //           is owned by the TypeManager.
+  ast::type::Type* type_decl();
+  /// Parses a `storage_class` grammar element
+  /// @returns the storage class or StorageClass::kNone if none matched
+  ast::StorageClass storage_class();
+  /// Parses a `struct_decl` grammar element
+  /// @returns the struct type or nullptr on error
+  std::unique_ptr<ast::type::StructType> struct_decl();
+  /// Parses a `struct_decoration_decl` grammar element
+  /// @returns the struct decoration or StructDecoraton::kNone
+  ast::StructDecoration struct_decoration_decl();
+  /// Parses a `struct_decoration` grammar element
+  /// @returns the struct decoration or StructDecoraton::kNone if none matched
+  ast::StructDecoration struct_decoration();
+  /// Parses a `struct_body_decl` grammar element
+  /// @returns the struct members
+  std::vector<std::unique_ptr<ast::StructMember>> struct_body_decl();
+  /// Parses a `struct_member` grammar element
+  /// @returns the struct member or nullptr
+  std::unique_ptr<ast::StructMember> struct_member();
+  /// Parses a `struct_member_decoration_decl` grammar element
+  /// @returns the list of decorations
+  std::vector<std::unique_ptr<ast::StructMemberDecoration>>
+  struct_member_decoration_decl();
+  /// Parses a `struct_member_decoration` grammar element
+  /// @returns the decoration or nullptr if none found
+  std::unique_ptr<ast::StructMemberDecoration> struct_member_decoration();
+  /// Parses a `function_decl` grammar element
+  /// @returns the parsed function, nullptr otherwise
+  std::unique_ptr<ast::Function> function_decl();
+  /// Parses a `function_type_decl` grammar element
+  /// @returns the parsed type or nullptr otherwise
+  ast::type::Type* function_type_decl();
+  /// Parses a `function_header` grammar element
+  /// @returns the parsed function nullptr otherwise
+  std::unique_ptr<ast::Function> function_header();
+  /// Parses a `param_list` grammar element
+  /// @returns the parsed variables
+  std::vector<std::unique_ptr<ast::Variable>> param_list();
+  /// Parses a `entry_point_decl` grammar element
+  /// @returns the EntryPoint or nullptr on error
+  std::unique_ptr<ast::EntryPoint> entry_point_decl();
+  /// Parses a `pipeline_stage` grammar element
+  /// @returns the pipeline stage or PipelineStage::kNone if none matched
+  ast::PipelineStage pipeline_stage();
+  /// Parses a `body_stmt` grammar element
+  /// @returns the parsed statements
+  std::vector<std::unique_ptr<ast::Statement>> body_stmt();
+  /// Parses a `paren_rhs_stmt` grammar element
+  /// @returns the parsed element or nullptr
+  std::unique_ptr<ast::Expression> paren_rhs_stmt();
+  /// Parses a `statements` grammar element
+  /// @returns the statements parsed
+  std::vector<std::unique_ptr<ast::Statement>> statements();
+  /// Parses a `statement` grammar element
+  /// @returns the parsed statement or nullptr
+  std::unique_ptr<ast::Statement> statement();
+  /// Parses a `break_stmt` gramamr element
+  /// @returns the parsed statement or nullptr
+  std::unique_ptr<ast::BreakStatement> break_stmt();
+  /// Parses a `continue_stmt` grammar element
+  /// @returns the parsed statement or nullptr
+  std::unique_ptr<ast::ContinueStatement> continue_stmt();
+  /// Parses a `variable_stmt` grammar element
+  /// @returns the parsed variable or nullptr
+  std::unique_ptr<ast::VariableStatement> variable_stmt();
+  /// Parses a `if_stmt` grammar element
+  /// @returns the parsed statement or nullptr
+  std::unique_ptr<ast::IfStatement> if_stmt();
+  /// Parses a `elseif_stmt` grammar element
+  /// @returns the parsed elements
+  std::vector<std::unique_ptr<ast::ElseStatement>> elseif_stmt();
+  /// Parses a `else_stmt` grammar element
+  /// @returns the parsed statement or nullptr
+  std::unique_ptr<ast::ElseStatement> else_stmt();
+  /// Parses a `premerge_stmt` grammar element
+  /// @returns the parsed statements
+  std::vector<std::unique_ptr<ast::Statement>> premerge_stmt();
+  /// Parses a `unless_stmt` grammar element
+  /// @returns the parsed element or nullptr
+  std::unique_ptr<ast::UnlessStatement> unless_stmt();
+  /// Parses a `regardless_stmt` grammar element
+  /// @returns the parsed element or nullptr
+  std::unique_ptr<ast::RegardlessStatement> regardless_stmt();
+  /// Parses a `switch_stmt` grammar element
+  /// @returns the parsed statement or nullptr
+  std::unique_ptr<ast::SwitchStatement> switch_stmt();
+  /// Parses a `switch_body` grammar element
+  /// @returns the parsed statement or nullptr
+  std::unique_ptr<ast::CaseStatement> switch_body();
+  /// Parses a `case_body` grammar element
+  /// @returns the parsed statements
+  std::vector<std::unique_ptr<ast::Statement>> case_body();
+  /// Parses a `loop_stmt` grammar element
+  /// @returns the parsed loop or nullptr
+  std::unique_ptr<ast::LoopStatement> loop_stmt();
+  /// Parses a `continuing_stmt` grammar element
+  /// @returns the parsed statements
+  std::vector<std::unique_ptr<ast::Statement>> continuing_stmt();
+  /// Parses a `const_literal` grammar element
+  /// @returns the const literal parsed or nullptr if none found
+  std::unique_ptr<ast::Literal> const_literal();
+  /// Parses a `const_expr` grammar element
+  /// @returns the parsed initializer expression or nullptr on error
+  std::unique_ptr<ast::InitializerExpression> const_expr();
+  /// Parses a `primary_expression` grammar element
+  /// @returns the parsed expression or nullptr
+  std::unique_ptr<ast::Expression> primary_expression();
+  /// Parses a `argument_expression_list` grammar element
+  /// @returns the list of arguments
+  std::vector<std::unique_ptr<ast::Expression>> argument_expression_list();
+  /// Parses the recursive portion of the postfix_expression
+  /// @param prefix the left side of the expression
+  /// @returns the parsed expression or nullptr
+  std::unique_ptr<ast::Expression> postfix_expr(
+      std::unique_ptr<ast::Expression> prefix);
+  /// Parses a `postfix_expression` grammar elment
+  /// @returns the parsed expression or nullptr
+  std::unique_ptr<ast::Expression> postfix_expression();
+  /// Parses a `unary_expression` grammar element
+  /// @returns the parsed expression or nullptr
+  std::unique_ptr<ast::Expression> unary_expression();
+  /// Parses a `derivative_modifier` grammar element
+  /// @returns the modifier or DerivativeModifier::kNone if none matched
+  ast::DerivativeModifier derivative_modifier();
+  /// Parses the recursive part of the `multiplicative_expression`
+  /// @param lhs the left side of the expression
+  /// @returns the parsed expression or nullptr
+  std::unique_ptr<ast::Expression> multiplicative_expr(
+      std::unique_ptr<ast::Expression> lhs);
+  /// Parses the `multiplicative_expression` grammar element
+  /// @returns the parsed expression or nullptr
+  std::unique_ptr<ast::Expression> multiplicative_expression();
+  /// Parses the recursive part of the `additive_expression`
+  /// @param lhs the left side of the expression
+  /// @returns the parsed expression or nullptr
+  std::unique_ptr<ast::Expression> additive_expr(
+      std::unique_ptr<ast::Expression> lhs);
+  /// Parses the `additive_expression` grammar element
+  /// @returns the parsed expression or nullptr
+  std::unique_ptr<ast::Expression> additive_expression();
+  /// Parses the recursive part of the `shift_expression`
+  /// @param lhs the left side of the expression
+  /// @returns the parsed expression or nullptr
+  std::unique_ptr<ast::Expression> shift_expr(
+      std::unique_ptr<ast::Expression> lhs);
+  /// Parses the `shift_expression` grammar element
+  /// @returns the parsed expression or nullptr
+  std::unique_ptr<ast::Expression> shift_expression();
+  /// Parses the recursive part of the `relational_expression`
+  /// @param lhs the left side of the expression
+  /// @returns the parsed expression or nullptr
+  std::unique_ptr<ast::Expression> relational_expr(
+      std::unique_ptr<ast::Expression> lhs);
+  /// Parses the `relational_expression` grammar element
+  /// @returns the parsed expression or nullptr
+  std::unique_ptr<ast::Expression> relational_expression();
+  /// Parses the recursive part of the `equality_expression`
+  /// @param lhs the left side of the expression
+  /// @returns the parsed expression or nullptr
+  std::unique_ptr<ast::Expression> equality_expr(
+      std::unique_ptr<ast::Expression> lhs);
+  /// Parses the `equality_expression` grammar element
+  /// @returns the parsed expression or nullptr
+  std::unique_ptr<ast::Expression> equality_expression();
+  /// Parses the recursive part of the `and_expression`
+  /// @param lhs the left side of the expression
+  /// @returns the parsed expression or nullptr
+  std::unique_ptr<ast::Expression> and_expr(
+      std::unique_ptr<ast::Expression> lhs);
+  /// Parses the `and_expression` grammar element
+  /// @returns the parsed expression or nullptr
+  std::unique_ptr<ast::Expression> and_expression();
+  /// Parses the recursive part of the `exclusive_or_expression`
+  /// @param lhs the left side of the expression
+  /// @returns the parsed expression or nullptr
+  std::unique_ptr<ast::Expression> exclusive_or_expr(
+      std::unique_ptr<ast::Expression> lhs);
+  /// Parses the `exclusive_or_expression` grammar elememnt
+  /// @returns the parsed expression or nullptr
+  std::unique_ptr<ast::Expression> exclusive_or_expression();
+  /// Parses the recursive part of the `inclusive_or_expression`
+  /// @param lhs the left side of the expression
+  /// @returns the parsed expression or nullptr
+  std::unique_ptr<ast::Expression> inclusive_or_expr(
+      std::unique_ptr<ast::Expression> lhs);
+  /// Parses the `inclusive_or_expression` grammar element
+  /// @returns the parsed expression or nullptr
+  std::unique_ptr<ast::Expression> inclusive_or_expression();
+  /// Parses the recursive part of the `logical_and_expression`
+  /// @param lhs the left side of the expression
+  /// @returns the parsed expression or nullptr
+  std::unique_ptr<ast::Expression> logical_and_expr(
+      std::unique_ptr<ast::Expression> lhs);
+  /// Parses a `logical_and_expression` grammar element
+  /// @returns the parsed expression or nullptr
+  std::unique_ptr<ast::Expression> logical_and_expression();
+  /// Parses the recursive part of the `logical_or_expression`
+  /// @param lhs the left side of the expression
+  /// @returns the parsed expression or nullptr
+  std::unique_ptr<ast::Expression> logical_or_expr(
+      std::unique_ptr<ast::Expression> lhs);
+  /// Parses a `logical_or_expression` grammar element
+  /// @returns the parsed expression or nullptr
+  std::unique_ptr<ast::Expression> logical_or_expression();
+  /// Parses a `assignment_stmt` grammar element
+  /// @returns the parsed assignment or nullptr
+  std::unique_ptr<ast::AssignmentStatement> assignment_stmt();
+
+ private:
+  ast::type::Type* type_decl_pointer(Token t);
+  ast::type::Type* type_decl_vector(Token t);
+  ast::type::Type* type_decl_array(Token t);
+  ast::type::Type* type_decl_matrix(Token t);
+
+  std::string error_;
+  std::unique_ptr<Lexer> lexer_;
+  std::deque<Token> token_queue_;
+  std::unordered_map<std::string, ast::type::Type*> registered_aliases_;
+  ast::Module module_;
+};
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
+
+#endif  // SRC_READER_WGSL_PARSER_IMPL_H_
diff --git a/src/reader/wgsl/parser_impl_additive_expression_test.cc b/src/reader/wgsl/parser_impl_additive_expression_test.cc
new file mode 100644
index 0000000..d69d61e
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_additive_expression_test.cc
@@ -0,0 +1,97 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/bool_literal.h"
+#include "src/ast/const_initializer_expression.h"
+#include "src/ast/identifier_expression.h"
+#include "src/ast/relational_expression.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, AdditiveExpression_Parses_Plus) {
+  ParserImpl p{"a + true"};
+  auto e = p.additive_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+
+  ASSERT_TRUE(e->IsRelational());
+  auto rel = e->AsRelational();
+  EXPECT_EQ(ast::Relation::kAdd, rel->relation());
+
+  ASSERT_TRUE(rel->lhs()->IsIdentifier());
+  auto ident = rel->lhs()->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 1);
+  EXPECT_EQ(ident->name()[0], "a");
+
+  ASSERT_TRUE(rel->rhs()->IsInitializer());
+  ASSERT_TRUE(rel->rhs()->AsInitializer()->IsConstInitializer());
+  auto init = rel->rhs()->AsInitializer()->AsConstInitializer();
+  ASSERT_TRUE(init->literal()->IsBool());
+  ASSERT_TRUE(init->literal()->AsBool()->IsTrue());
+}
+
+TEST_F(ParserImplTest, AdditiveExpression_Parses_Minus) {
+  ParserImpl p{"a - true"};
+  auto e = p.additive_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+
+  ASSERT_TRUE(e->IsRelational());
+  auto rel = e->AsRelational();
+  EXPECT_EQ(ast::Relation::kSubtract, rel->relation());
+
+  ASSERT_TRUE(rel->lhs()->IsIdentifier());
+  auto ident = rel->lhs()->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 1);
+  EXPECT_EQ(ident->name()[0], "a");
+
+  ASSERT_TRUE(rel->rhs()->IsInitializer());
+  ASSERT_TRUE(rel->rhs()->AsInitializer()->IsConstInitializer());
+  auto init = rel->rhs()->AsInitializer()->AsConstInitializer();
+  ASSERT_TRUE(init->literal()->IsBool());
+  ASSERT_TRUE(init->literal()->AsBool()->IsTrue());
+}
+
+TEST_F(ParserImplTest, AdditiveExpression_InvalidLHS) {
+  ParserImpl p{"if (a) {} + true"};
+  auto e = p.additive_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_EQ(e, nullptr);
+}
+
+TEST_F(ParserImplTest, AdditiveExpression_InvalidRHS) {
+  ParserImpl p{"true + if (a) {}"};
+  auto e = p.additive_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:8: unable to parse right side of + expression");
+}
+
+TEST_F(ParserImplTest, AdditiveExpression_NoOr_ReturnsLHS) {
+  ParserImpl p{"a true"};
+  auto e = p.additive_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsIdentifier());
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_and_expression_test.cc b/src/reader/wgsl/parser_impl_and_expression_test.cc
new file mode 100644
index 0000000..be1c581
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_and_expression_test.cc
@@ -0,0 +1,75 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/bool_literal.h"
+#include "src/ast/const_initializer_expression.h"
+#include "src/ast/identifier_expression.h"
+#include "src/ast/relational_expression.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, AndExpression_Parses) {
+  ParserImpl p{"a & true"};
+  auto e = p.and_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+
+  ASSERT_TRUE(e->IsRelational());
+  auto rel = e->AsRelational();
+  EXPECT_EQ(ast::Relation::kAnd, rel->relation());
+
+  ASSERT_TRUE(rel->lhs()->IsIdentifier());
+  auto ident = rel->lhs()->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 1);
+  EXPECT_EQ(ident->name()[0], "a");
+
+  ASSERT_TRUE(rel->rhs()->IsInitializer());
+  ASSERT_TRUE(rel->rhs()->AsInitializer()->IsConstInitializer());
+  auto init = rel->rhs()->AsInitializer()->AsConstInitializer();
+  ASSERT_TRUE(init->literal()->IsBool());
+  ASSERT_TRUE(init->literal()->AsBool()->IsTrue());
+}
+
+TEST_F(ParserImplTest, AndExpression_InvalidLHS) {
+  ParserImpl p{"if (a) {} & true"};
+  auto e = p.and_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_EQ(e, nullptr);
+}
+
+TEST_F(ParserImplTest, AndExpression_InvalidRHS) {
+  ParserImpl p{"true & if (a) {}"};
+  auto e = p.and_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:8: unable to parse right side of & expression");
+}
+
+TEST_F(ParserImplTest, AndExpression_NoOr_ReturnsLHS) {
+  ParserImpl p{"a true"};
+  auto e = p.and_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsIdentifier());
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_argument_expression_list_test.cc b/src/reader/wgsl/parser_impl_argument_expression_list_test.cc
new file mode 100644
index 0000000..e053532
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_argument_expression_list_test.cc
@@ -0,0 +1,67 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/array_accessor_expression.h"
+#include "src/ast/const_initializer_expression.h"
+#include "src/ast/identifier_expression.h"
+#include "src/ast/int_literal.h"
+#include "src/ast/unary_derivative_expression.h"
+#include "src/ast/unary_method_expression.h"
+#include "src/ast/unary_op_expression.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, ArgumentExpressionList_Parses) {
+  ParserImpl p{"a"};
+  auto e = p.argument_expression_list();
+  ASSERT_FALSE(p.has_error()) << p.error();
+
+  ASSERT_EQ(e.size(), 1);
+  ASSERT_TRUE(e[0]->IsIdentifier());
+}
+
+TEST_F(ParserImplTest, ArgumentExpressionList_ParsesMultiple) {
+  ParserImpl p{"a, -33, 1+2"};
+  auto e = p.argument_expression_list();
+  ASSERT_FALSE(p.has_error()) << p.error();
+
+  ASSERT_EQ(e.size(), 3);
+  ASSERT_TRUE(e[0]->IsIdentifier());
+  ASSERT_TRUE(e[1]->IsInitializer());
+  ASSERT_TRUE(e[2]->IsRelational());
+}
+
+TEST_F(ParserImplTest, ArgumentExpressionList_HandlesMissingExpression) {
+  ParserImpl p{"a, "};
+  auto e = p.argument_expression_list();
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(p.error(), "1:4: unable to parse argument expression after comma");
+}
+
+TEST_F(ParserImplTest, ArgumentExpressionList_HandlesInvalidExpression) {
+  ParserImpl p{"if(a) {}"};
+  auto e = p.argument_expression_list();
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(p.error(), "1:1: unable to parse argument expression");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_assignment_stmt_test.cc b/src/reader/wgsl/parser_impl_assignment_stmt_test.cc
new file mode 100644
index 0000000..9de59f3
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_assignment_stmt_test.cc
@@ -0,0 +1,136 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/array_accessor_expression.h"
+#include "src/ast/assignment_statement.h"
+#include "src/ast/const_initializer_expression.h"
+#include "src/ast/identifier_expression.h"
+#include "src/ast/int_literal.h"
+#include "src/ast/literal.h"
+#include "src/ast/member_accessor_expression.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, AssignmentStmt_Parses_ToVariable) {
+  ParserImpl p{"a = 123"};
+  auto e = p.assignment_stmt();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+
+  ASSERT_TRUE(e->IsAssign());
+  ASSERT_NE(e->lhs(), nullptr);
+  ASSERT_NE(e->rhs(), nullptr);
+
+  ASSERT_TRUE(e->lhs()->IsIdentifier());
+  auto ident = e->lhs()->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 1);
+  EXPECT_EQ(ident->name()[0], "a");
+
+  ASSERT_TRUE(e->rhs()->IsInitializer());
+  ASSERT_TRUE(e->rhs()->AsInitializer()->IsConstInitializer());
+
+  auto init = e->rhs()->AsInitializer()->AsConstInitializer();
+  ASSERT_NE(init->literal(), nullptr);
+  ASSERT_TRUE(init->literal()->IsInt());
+  EXPECT_EQ(init->literal()->AsInt()->value(), 123);
+}
+
+TEST_F(ParserImplTest, AssignmentStmt_Parses_ToMember) {
+  ParserImpl p{"a.b.c[2].d = 123"};
+  auto e = p.assignment_stmt();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+
+  ASSERT_TRUE(e->IsAssign());
+  ASSERT_NE(e->lhs(), nullptr);
+  ASSERT_NE(e->rhs(), nullptr);
+
+  ASSERT_TRUE(e->rhs()->IsInitializer());
+  ASSERT_TRUE(e->rhs()->AsInitializer()->IsConstInitializer());
+  auto init = e->rhs()->AsInitializer()->AsConstInitializer();
+  ASSERT_NE(init->literal(), nullptr);
+  ASSERT_TRUE(init->literal()->IsInt());
+  EXPECT_EQ(init->literal()->AsInt()->value(), 123);
+
+  ASSERT_TRUE(e->lhs()->IsMemberAccessor());
+  auto mem = e->lhs()->AsMemberAccessor();
+
+  ASSERT_TRUE(mem->member()->IsIdentifier());
+  auto ident = mem->member()->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 1);
+  EXPECT_EQ(ident->name()[0], "d");
+
+  ASSERT_TRUE(mem->structure()->IsArrayAccessor());
+  auto ary = mem->structure()->AsArrayAccessor();
+
+  ASSERT_TRUE(ary->idx_expr()->IsInitializer());
+  ASSERT_TRUE(ary->idx_expr()->AsInitializer()->IsConstInitializer());
+  init = ary->idx_expr()->AsInitializer()->AsConstInitializer();
+  ASSERT_NE(init->literal(), nullptr);
+  ASSERT_TRUE(init->literal()->IsInt());
+  EXPECT_EQ(init->literal()->AsInt()->value(), 2);
+
+  ASSERT_TRUE(ary->array()->IsMemberAccessor());
+  mem = ary->array()->AsMemberAccessor();
+  ASSERT_TRUE(mem->member()->IsIdentifier());
+  ident = mem->member()->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 1);
+  EXPECT_EQ(ident->name()[0], "c");
+
+  ASSERT_TRUE(mem->structure()->IsMemberAccessor());
+  mem = mem->structure()->AsMemberAccessor();
+
+  ASSERT_TRUE(mem->structure()->IsIdentifier());
+  ident = mem->structure()->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 1);
+  EXPECT_EQ(ident->name()[0], "a");
+
+  ASSERT_TRUE(mem->member()->IsIdentifier());
+  ident = mem->member()->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 1);
+  EXPECT_EQ(ident->name()[0], "b");
+}
+
+TEST_F(ParserImplTest, AssignmentStmt_MissingEqual) {
+  ParserImpl p{"a.b.c[2].d 123"};
+  auto e = p.assignment_stmt();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:12: missing = for assignment");
+}
+
+TEST_F(ParserImplTest, AssignmentStmt_InvalidLHS) {
+  ParserImpl p{"if (true) {} = 123"};
+  auto e = p.assignment_stmt();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_EQ(e, nullptr);
+}
+
+TEST_F(ParserImplTest, AssignmentStmt_InvalidRHS) {
+  ParserImpl p{"a.b.c[2].d = if (true) {}"};
+  auto e = p.assignment_stmt();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:14: unable to parse right side of assignment");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_body_stmt_test.cc b/src/reader/wgsl/parser_impl_body_stmt_test.cc
new file mode 100644
index 0000000..6f57a60
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_body_stmt_test.cc
@@ -0,0 +1,61 @@
+// 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 "gtest/gtest.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, BodyStmt) {
+  ParserImpl p{R"({
+  kill;
+  nop;
+  return 1 + b / 2;
+})"};
+  auto e = p.body_stmt();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_EQ(e.size(), 3);
+  EXPECT_TRUE(e[0]->IsKill());
+  EXPECT_TRUE(e[1]->IsNop());
+  EXPECT_TRUE(e[2]->IsReturn());
+}
+
+TEST_F(ParserImplTest, BodyStmt_Empty) {
+  ParserImpl p{"{}"};
+  auto e = p.body_stmt();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  EXPECT_EQ(e.size(), 0);
+}
+
+TEST_F(ParserImplTest, BodyStmt_InvalidStmt) {
+  ParserImpl p{"{fn main() -> void {}}"};
+  auto e = p.body_stmt();
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(p.error(), "1:2: missing }");
+}
+
+TEST_F(ParserImplTest, BodyStmt_MissingRightParen) {
+  ParserImpl p{"{return;"};
+  auto e = p.body_stmt();
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(p.error(), "1:9: missing }");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_break_stmt_test.cc b/src/reader/wgsl/parser_impl_break_stmt_test.cc
new file mode 100644
index 0000000..932871e
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_break_stmt_test.cc
@@ -0,0 +1,77 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/break_statement.h"
+#include "src/ast/return_statement.h"
+#include "src/ast/statement.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, BreakStmt) {
+  ParserImpl p{"break"};
+  auto e = p.break_stmt();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsBreak());
+  EXPECT_EQ(e->condition(), ast::StatementCondition::kNone);
+  EXPECT_EQ(e->conditional(), nullptr);
+}
+
+TEST_F(ParserImplTest, BreakStmt_WithIf) {
+  ParserImpl p{"break if (a == b)"};
+  auto e = p.break_stmt();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsBreak());
+  EXPECT_EQ(e->condition(), ast::StatementCondition::kIf);
+  ASSERT_NE(e->conditional(), nullptr);
+  EXPECT_TRUE(e->conditional()->IsRelational());
+}
+
+TEST_F(ParserImplTest, BreakStmt_WithUnless) {
+  ParserImpl p{"break unless (a == b)"};
+  auto e = p.break_stmt();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsBreak());
+  EXPECT_EQ(e->condition(), ast::StatementCondition::kUnless);
+  ASSERT_NE(e->conditional(), nullptr);
+  EXPECT_TRUE(e->conditional()->IsRelational());
+}
+
+TEST_F(ParserImplTest, BreakStmt_InvalidRHS) {
+  ParserImpl p{"break if (a = b)"};
+  auto e = p.break_stmt();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:13: expected )");
+}
+
+TEST_F(ParserImplTest, BreakStmt_MissingRHS) {
+  ParserImpl p{"break if"};
+  auto e = p.break_stmt();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:9: expected (");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_builtin_decoration_test.cc b/src/reader/wgsl/parser_impl_builtin_decoration_test.cc
new file mode 100644
index 0000000..4b5edf1
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_builtin_decoration_test.cc
@@ -0,0 +1,74 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/builtin.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+struct BuiltinData {
+  const char* input;
+  ast::Builtin result;
+};
+inline std::ostream& operator<<(std::ostream& out, BuiltinData data) {
+  out << std::string(data.input);
+  return out;
+}
+using BuiltinTest = testing::TestWithParam<BuiltinData>;
+TEST_P(BuiltinTest, Parses) {
+  auto params = GetParam();
+  ParserImpl p{params.input};
+
+  auto builtin = p.builtin_decoration();
+  ASSERT_FALSE(p.has_error());
+  EXPECT_EQ(builtin, params.result);
+
+  auto t = p.next();
+  EXPECT_TRUE(t.IsEof());
+}
+INSTANTIATE_TEST_SUITE_P(
+    ParserImplTest,
+    BuiltinTest,
+    testing::Values(
+        BuiltinData{"position", ast::Builtin::kPosition},
+        BuiltinData{"vertex_idx", ast::Builtin::kVertexIdx},
+        BuiltinData{"instance_idx", ast::Builtin::kInstanceIdx},
+        BuiltinData{"front_facing", ast::Builtin::kFrontFacing},
+        BuiltinData{"frag_coord", ast::Builtin::kFragCoord},
+        BuiltinData{"frag_depth", ast::Builtin::kFragDepth},
+        BuiltinData{"num_workgroups", ast::Builtin::kNumWorkgroups},
+        BuiltinData{"workgroup_size", ast::Builtin::kWorkgroupSize},
+        BuiltinData{"local_invocation_id", ast::Builtin::kLocalInvocationId},
+        BuiltinData{"local_invocation_idx", ast::Builtin::kLocalInvocationIdx},
+        BuiltinData{"global_invocation_id",
+                    ast::Builtin::kGlobalInvocationId}));
+
+TEST_F(ParserImplTest, BuiltinDecoration_NoMatch) {
+  ParserImpl p{"not-a-builtin"};
+  auto builtin = p.builtin_decoration();
+  ASSERT_EQ(builtin, ast::Builtin::kNone);
+
+  auto t = p.next();
+  EXPECT_TRUE(t.IsIdentifier());
+  EXPECT_EQ(t.to_str(), "not");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_case_body_test.cc b/src/reader/wgsl/parser_impl_case_body_test.cc
new file mode 100644
index 0000000..621a8a4
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_case_body_test.cc
@@ -0,0 +1,69 @@
+// 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 "gtest/gtest.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, CaseBody_Empty) {
+  ParserImpl p{""};
+  auto e = p.case_body();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  EXPECT_EQ(e.size(), 0);
+}
+
+TEST_F(ParserImplTest, CaseBody_Statements) {
+  ParserImpl p{R"(
+  var a: i32;
+  a = 2;)"};
+
+  auto e = p.case_body();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_EQ(e.size(), 2);
+  EXPECT_TRUE(e[0]->IsVariable());
+  EXPECT_TRUE(e[1]->IsAssign());
+}
+
+TEST_F(ParserImplTest, CaseBody_InvalidStatement) {
+  ParserImpl p{"a ="};
+  auto e = p.case_body();
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(e.size(), 0);
+  EXPECT_EQ(p.error(), "1:4: unable to parse right side of assignment");
+}
+
+TEST_F(ParserImplTest, CaseBody_Fallthrough) {
+  ParserImpl p{"fallthrough;"};
+  auto e = p.case_body();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_EQ(e.size(), 1);
+  EXPECT_TRUE(e[0]->IsFallthrough());
+}
+
+TEST_F(ParserImplTest, CaseBody_Fallthrough_MissingSemicolon) {
+  ParserImpl p{"fallthrough"};
+  auto e = p.case_body();
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(e.size(), 0);
+  EXPECT_EQ(p.error(), "1:12: missing ;");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_const_expr_test.cc b/src/reader/wgsl/parser_impl_const_expr_test.cc
new file mode 100644
index 0000000..ffdb348
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_const_expr_test.cc
@@ -0,0 +1,127 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/bool_literal.h"
+#include "src/ast/const_initializer_expression.h"
+#include "src/ast/float_literal.h"
+#include "src/ast/type/vector_type.h"
+#include "src/ast/type_initializer_expression.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, ConstExpr_TypeDecl) {
+  ParserImpl p{"vec2<f32>(1., 2.)"};
+  auto e = p.const_expr();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsInitializer());
+  ASSERT_TRUE(e->AsInitializer()->IsTypeInitializer());
+
+  auto t = e->AsInitializer()->AsTypeInitializer();
+  ASSERT_TRUE(t->type()->IsVector());
+  EXPECT_EQ(t->type()->AsVector()->size(), 2);
+
+  ASSERT_EQ(t->values().size(), 2);
+  auto& v = t->values();
+
+  ASSERT_TRUE(v[0]->IsInitializer());
+  ASSERT_TRUE(v[0]->AsInitializer()->IsConstInitializer());
+  auto c = v[0]->AsInitializer()->AsConstInitializer();
+  ASSERT_TRUE(c->literal()->IsFloat());
+  EXPECT_FLOAT_EQ(c->literal()->AsFloat()->value(), 1.);
+
+  ASSERT_TRUE(v[1]->IsInitializer());
+  ASSERT_TRUE(v[1]->AsInitializer()->IsConstInitializer());
+  c = v[1]->AsInitializer()->AsConstInitializer();
+  ASSERT_TRUE(c->literal()->IsFloat());
+  EXPECT_FLOAT_EQ(c->literal()->AsFloat()->value(), 2.);
+}
+
+TEST_F(ParserImplTest, ConstExpr_TypeDecl_MissingRightParen) {
+  ParserImpl p{"vec2<f32>(1., 2."};
+  auto e = p.const_expr();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:17: missing ) for type initializer");
+}
+
+TEST_F(ParserImplTest, ConstExpr_TypeDecl_MissingLeftParen) {
+  ParserImpl p{"vec2<f32> 1., 2.)"};
+  auto e = p.const_expr();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:11: missing ( for type initializer");
+}
+
+TEST_F(ParserImplTest, ConstExpr_TypeDecl_HangingComma) {
+  ParserImpl p{"vec2<f32>(1.,)"};
+  auto e = p.const_expr();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:14: unable to parse const literal");
+}
+
+TEST_F(ParserImplTest, ConstExpr_TypeDecl_MissingComma) {
+  ParserImpl p{"vec2<f32>(1. 2."};
+  auto e = p.const_expr();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:14: missing ) for type initializer");
+}
+
+TEST_F(ParserImplTest, ConstExpr_MissingExpr) {
+  ParserImpl p{"vec2<f32>()"};
+  auto e = p.const_expr();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:11: unable to parse const literal");
+}
+
+TEST_F(ParserImplTest, ConstExpr_InvalidExpr) {
+  ParserImpl p{"vec2<f32>(1., if(a) {})"};
+  auto e = p.const_expr();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:15: unable to parse const literal");
+}
+
+TEST_F(ParserImplTest, ConstExpr_ConstLiteral) {
+  ParserImpl p{"true"};
+  auto e = p.const_expr();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsInitializer());
+  ASSERT_TRUE(e->AsInitializer()->IsConstInitializer());
+  auto c = e->AsInitializer()->AsConstInitializer();
+  ASSERT_TRUE(c->literal()->IsBool());
+  EXPECT_TRUE(c->literal()->AsBool()->IsTrue());
+}
+
+TEST_F(ParserImplTest, ConstExpr_ConstLiteral_Invalid) {
+  ParserImpl p{"invalid"};
+  auto e = p.const_expr();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:1: unknown type alias 'invalid'");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_const_literal_test.cc b/src/reader/wgsl/parser_impl_const_literal_test.cc
new file mode 100644
index 0000000..f9572ea
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_const_literal_test.cc
@@ -0,0 +1,88 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/bool_literal.h"
+#include "src/ast/float_literal.h"
+#include "src/ast/int_literal.h"
+#include "src/ast/uint_literal.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, ConstLiteral_Int) {
+  ParserImpl p{"-234"};
+  auto c = p.const_literal();
+  ASSERT_FALSE(p.has_error());
+  ASSERT_NE(c, nullptr);
+  ASSERT_TRUE(c->IsInt());
+  EXPECT_EQ(c->AsInt()->value(), -234);
+}
+
+TEST_F(ParserImplTest, ConstLiteral_Uint) {
+  ParserImpl p{"234u"};
+  auto c = p.const_literal();
+  ASSERT_FALSE(p.has_error());
+  ASSERT_NE(c, nullptr);
+  ASSERT_TRUE(c->IsUint());
+  EXPECT_EQ(c->AsUint()->value(), 234u);
+}
+
+TEST_F(ParserImplTest, ConstLiteral_Float) {
+  ParserImpl p{"234.e12"};
+  auto c = p.const_literal();
+  ASSERT_FALSE(p.has_error());
+  ASSERT_NE(c, nullptr);
+  ASSERT_TRUE(c->IsFloat());
+  EXPECT_FLOAT_EQ(c->AsFloat()->value(), 234e12);
+}
+
+TEST_F(ParserImplTest, ConstLiteral_InvalidFloat) {
+  ParserImpl p{"1.2e+256"};
+  auto c = p.const_literal();
+  ASSERT_EQ(c, nullptr);
+}
+
+TEST_F(ParserImplTest, ConstLiteral_True) {
+  ParserImpl p{"true"};
+  auto c = p.const_literal();
+  ASSERT_FALSE(p.has_error());
+  ASSERT_NE(c, nullptr);
+  ASSERT_TRUE(c->IsBool());
+  EXPECT_TRUE(c->AsBool()->IsTrue());
+}
+
+TEST_F(ParserImplTest, ConstLiteral_False) {
+  ParserImpl p{"false"};
+  auto c = p.const_literal();
+  ASSERT_FALSE(p.has_error());
+  ASSERT_NE(c, nullptr);
+  ASSERT_TRUE(c->IsBool());
+  EXPECT_TRUE(c->AsBool()->IsFalse());
+}
+
+TEST_F(ParserImplTest, ConstLiteral_NoMatch) {
+  ParserImpl p{"another-token"};
+  auto c = p.const_literal();
+  ASSERT_FALSE(p.has_error());
+  ASSERT_EQ(c, nullptr);
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_continue_stmt_test.cc b/src/reader/wgsl/parser_impl_continue_stmt_test.cc
new file mode 100644
index 0000000..335cdf2
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_continue_stmt_test.cc
@@ -0,0 +1,77 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/continue_statement.h"
+#include "src/ast/return_statement.h"
+#include "src/ast/statement.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, ContinueStmt) {
+  ParserImpl p{"continue"};
+  auto e = p.continue_stmt();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsContinue());
+  EXPECT_EQ(e->condition(), ast::StatementCondition::kNone);
+  EXPECT_EQ(e->conditional(), nullptr);
+}
+
+TEST_F(ParserImplTest, ContinueStmt_WithIf) {
+  ParserImpl p{"continue if (a == b)"};
+  auto e = p.continue_stmt();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsContinue());
+  EXPECT_EQ(e->condition(), ast::StatementCondition::kIf);
+  ASSERT_NE(e->conditional(), nullptr);
+  EXPECT_TRUE(e->conditional()->IsRelational());
+}
+
+TEST_F(ParserImplTest, ContinueStmt_WithUnless) {
+  ParserImpl p{"continue unless (a == b)"};
+  auto e = p.continue_stmt();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsContinue());
+  EXPECT_EQ(e->condition(), ast::StatementCondition::kUnless);
+  ASSERT_NE(e->conditional(), nullptr);
+  EXPECT_TRUE(e->conditional()->IsRelational());
+}
+
+TEST_F(ParserImplTest, ContinueStmt_InvalidRHS) {
+  ParserImpl p{"continue if (a = b)"};
+  auto e = p.continue_stmt();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:16: expected )");
+}
+
+TEST_F(ParserImplTest, ContinueStmt_MissingRHS) {
+  ParserImpl p{"continue if"};
+  auto e = p.continue_stmt();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:12: expected (");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_continuing_stmt_test.cc b/src/reader/wgsl/parser_impl_continuing_stmt_test.cc
new file mode 100644
index 0000000..93b79c0
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_continuing_stmt_test.cc
@@ -0,0 +1,42 @@
+// 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 "gtest/gtest.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, ContinuingStmt) {
+  ParserImpl p{"continuing { nop; }"};
+  auto e = p.continuing_stmt();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_EQ(e.size(), 1);
+  ASSERT_TRUE(e[0]->IsNop());
+}
+
+TEST_F(ParserImplTest, ContinuingStmt_InvalidBody) {
+  ParserImpl p{"continuing { nop }"};
+  auto e = p.continuing_stmt();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e.size(), 0);
+  EXPECT_EQ(p.error(), "1:18: missing ;");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_derivative_modifier_test.cc b/src/reader/wgsl/parser_impl_derivative_modifier_test.cc
new file mode 100644
index 0000000..4c471d2
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_derivative_modifier_test.cc
@@ -0,0 +1,65 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/derivative_modifier.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+struct DerivativeModifierData {
+  const char* input;
+  ast::DerivativeModifier result;
+};
+inline std::ostream& operator<<(std::ostream& out,
+                                DerivativeModifierData data) {
+  out << std::string(data.input);
+  return out;
+}
+using DerivativeModifierTest = testing::TestWithParam<DerivativeModifierData>;
+TEST_P(DerivativeModifierTest, Parses) {
+  auto params = GetParam();
+  ParserImpl p{params.input};
+
+  auto mod = p.derivative_modifier();
+  ASSERT_FALSE(p.has_error());
+  EXPECT_EQ(mod, params.result);
+
+  auto t = p.next();
+  EXPECT_TRUE(t.IsEof());
+}
+INSTANTIATE_TEST_SUITE_P(
+    ParserImplTest,
+    DerivativeModifierTest,
+    testing::Values(
+        DerivativeModifierData{"fine", ast::DerivativeModifier::kFine},
+        DerivativeModifierData{"coarse", ast::DerivativeModifier::kCoarse}));
+
+TEST_F(ParserImplTest, DerivativeModifier_NoMatch) {
+  ParserImpl p{"not-a-modifier"};
+  auto stage = p.derivative_modifier();
+  ASSERT_EQ(stage, ast::DerivativeModifier::kNone);
+
+  auto t = p.next();
+  EXPECT_TRUE(t.IsIdentifier());
+  EXPECT_EQ(t.to_str(), "not");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_else_stmt_test.cc b/src/reader/wgsl/parser_impl_else_stmt_test.cc
new file mode 100644
index 0000000..cbfeff6
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_else_stmt_test.cc
@@ -0,0 +1,53 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/else_statement.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, ElseStmt) {
+  ParserImpl p{"else { a = b; c = d; }"};
+  auto e = p.else_stmt();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsElse());
+  ASSERT_EQ(e->condition(), nullptr);
+  EXPECT_EQ(e->body().size(), 2);
+}
+
+TEST_F(ParserImplTest, ElseStmt_InvalidBody) {
+  ParserImpl p{"else { fn main() -> void {}}"};
+  auto e = p.else_stmt();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:8: missing }");
+}
+
+TEST_F(ParserImplTest, ElseStmt_MissingBody) {
+  ParserImpl p{"else"};
+  auto e = p.else_stmt();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:5: missing {");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_elseif_stmt_test.cc b/src/reader/wgsl/parser_impl_elseif_stmt_test.cc
new file mode 100644
index 0000000..b597fed
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_elseif_stmt_test.cc
@@ -0,0 +1,70 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/else_statement.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, ElseIfStmt) {
+  ParserImpl p{"elseif (a == 4) { a = b; c = d; }"};
+  auto e = p.elseif_stmt();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_EQ(e.size(), 1);
+
+  ASSERT_TRUE(e[0]->IsElse());
+  ASSERT_NE(e[0]->condition(), nullptr);
+  ASSERT_TRUE(e[0]->condition()->IsRelational());
+  EXPECT_EQ(e[0]->body().size(), 2);
+}
+
+TEST_F(ParserImplTest, ElseIfStmt_Multiple) {
+  ParserImpl p{"elseif (a == 4) { a = b; c = d; } elseif(c) { d = 2; }"};
+  auto e = p.elseif_stmt();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_EQ(e.size(), 2);
+
+  ASSERT_TRUE(e[0]->IsElse());
+  ASSERT_NE(e[0]->condition(), nullptr);
+  ASSERT_TRUE(e[0]->condition()->IsRelational());
+  EXPECT_EQ(e[0]->body().size(), 2);
+
+  ASSERT_TRUE(e[1]->IsElse());
+  ASSERT_NE(e[1]->condition(), nullptr);
+  ASSERT_TRUE(e[1]->condition()->IsIdentifier());
+  EXPECT_EQ(e[1]->body().size(), 1);
+}
+
+TEST_F(ParserImplTest, ElseIfStmt_InvalidBody) {
+  ParserImpl p{"elseif (true) { fn main() -> void {}}"};
+  auto e = p.elseif_stmt();
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(p.error(), "1:17: missing }");
+}
+
+TEST_F(ParserImplTest, ElseIfStmt_MissingBody) {
+  ParserImpl p{"elseif (true)"};
+  auto e = p.elseif_stmt();
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(p.error(), "1:14: missing {");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_entry_point_decl_test.cc b/src/reader/wgsl/parser_impl_entry_point_decl_test.cc
new file mode 100644
index 0000000..3afdc90
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_entry_point_decl_test.cc
@@ -0,0 +1,121 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/variable.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, EntryPoint_Parses) {
+  ParserImpl p{"entry_point fragment = main"};
+  auto e = p.entry_point_decl();
+  ASSERT_NE(e, nullptr);
+  ASSERT_FALSE(p.has_error());
+  EXPECT_EQ(e->stage(), ast::PipelineStage::kFragment);
+  EXPECT_EQ(e->name(), "main");
+  EXPECT_EQ(e->function_name(), "main");
+}
+
+TEST_F(ParserImplTest, EntryPoint_ParsesWithStringName) {
+  ParserImpl p{R"(entry_point vertex as "main" = vtx_main)"};
+  auto e = p.entry_point_decl();
+  ASSERT_NE(e, nullptr);
+  ASSERT_FALSE(p.has_error());
+  EXPECT_EQ(e->stage(), ast::PipelineStage::kVertex);
+  EXPECT_EQ(e->name(), "main");
+  EXPECT_EQ(e->function_name(), "vtx_main");
+}
+
+TEST_F(ParserImplTest, EntryPoint_ParsesWithIdentName) {
+  ParserImpl p{R"(entry_point vertex as main = vtx_main)"};
+  auto e = p.entry_point_decl();
+  ASSERT_NE(e, nullptr);
+  ASSERT_FALSE(p.has_error());
+  EXPECT_EQ(e->stage(), ast::PipelineStage::kVertex);
+  EXPECT_EQ(e->name(), "main");
+  EXPECT_EQ(e->function_name(), "vtx_main");
+}
+
+TEST_F(ParserImplTest, EntryPoint_MissingFnName) {
+  ParserImpl p{R"(entry_point vertex as main =)"};
+  auto e = p.entry_point_decl();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:29: invalid function name for entry point");
+}
+
+TEST_F(ParserImplTest, EntryPoint_InvalidFnName) {
+  ParserImpl p{R"(entry_point vertex as main = 123)"};
+  auto e = p.entry_point_decl();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:30: invalid function name for entry point");
+}
+
+TEST_F(ParserImplTest, EntryPoint_MissingEqual) {
+  ParserImpl p{R"(entry_point vertex as main vtx_main)"};
+  auto e = p.entry_point_decl();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:28: missing = for entry point");
+}
+
+TEST_F(ParserImplTest, EntryPoint_MissingName) {
+  ParserImpl p{R"(entry_point vertex as = vtx_main)"};
+  auto e = p.entry_point_decl();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:23: invalid name for entry point");
+}
+
+TEST_F(ParserImplTest, EntryPoint_InvalidName) {
+  ParserImpl p{R"(entry_point vertex as 123 = vtx_main)"};
+  auto e = p.entry_point_decl();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:23: invalid name for entry point");
+}
+
+TEST_F(ParserImplTest, EntryPoint_MissingStageWithIdent) {
+  ParserImpl p{R"(entry_point as 123 = vtx_main)"};
+  auto e = p.entry_point_decl();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:13: missing pipeline stage for entry point");
+}
+
+TEST_F(ParserImplTest, EntryPoint_MissingStage) {
+  ParserImpl p{R"(entry_point = vtx_main)"};
+  auto e = p.entry_point_decl();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:13: missing pipeline stage for entry point");
+}
+
+TEST_F(ParserImplTest, EntryPoint_InvalidStage) {
+  ParserImpl p{R"(entry_point invalid = vtx_main)"};
+  auto e = p.entry_point_decl();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:13: missing pipeline stage for entry point");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_equality_expression_test.cc b/src/reader/wgsl/parser_impl_equality_expression_test.cc
new file mode 100644
index 0000000..73a3922
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_equality_expression_test.cc
@@ -0,0 +1,97 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/bool_literal.h"
+#include "src/ast/const_initializer_expression.h"
+#include "src/ast/identifier_expression.h"
+#include "src/ast/relational_expression.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, EqualityExpression_Parses_Equal) {
+  ParserImpl p{"a == true"};
+  auto e = p.equality_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+
+  ASSERT_TRUE(e->IsRelational());
+  auto rel = e->AsRelational();
+  EXPECT_EQ(ast::Relation::kEqual, rel->relation());
+
+  ASSERT_TRUE(rel->lhs()->IsIdentifier());
+  auto ident = rel->lhs()->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 1);
+  EXPECT_EQ(ident->name()[0], "a");
+
+  ASSERT_TRUE(rel->rhs()->IsInitializer());
+  ASSERT_TRUE(rel->rhs()->AsInitializer()->IsConstInitializer());
+  auto init = rel->rhs()->AsInitializer()->AsConstInitializer();
+  ASSERT_TRUE(init->literal()->IsBool());
+  ASSERT_TRUE(init->literal()->AsBool()->IsTrue());
+}
+
+TEST_F(ParserImplTest, EqualityExpression_Parses_NotEqual) {
+  ParserImpl p{"a != true"};
+  auto e = p.equality_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+
+  ASSERT_TRUE(e->IsRelational());
+  auto rel = e->AsRelational();
+  EXPECT_EQ(ast::Relation::kNotEqual, rel->relation());
+
+  ASSERT_TRUE(rel->lhs()->IsIdentifier());
+  auto ident = rel->lhs()->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 1);
+  EXPECT_EQ(ident->name()[0], "a");
+
+  ASSERT_TRUE(rel->rhs()->IsInitializer());
+  ASSERT_TRUE(rel->rhs()->AsInitializer()->IsConstInitializer());
+  auto init = rel->rhs()->AsInitializer()->AsConstInitializer();
+  ASSERT_TRUE(init->literal()->IsBool());
+  ASSERT_TRUE(init->literal()->AsBool()->IsTrue());
+}
+
+TEST_F(ParserImplTest, EqualityExpression_InvalidLHS) {
+  ParserImpl p{"if (a) {} == true"};
+  auto e = p.equality_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_EQ(e, nullptr);
+}
+
+TEST_F(ParserImplTest, EqualityExpression_InvalidRHS) {
+  ParserImpl p{"true == if (a) {}"};
+  auto e = p.equality_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:9: unable to parse right side of == expression");
+}
+
+TEST_F(ParserImplTest, EqualityExpression_NoOr_ReturnsLHS) {
+  ParserImpl p{"a true"};
+  auto e = p.equality_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsIdentifier());
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_exclusive_or_expression_test.cc b/src/reader/wgsl/parser_impl_exclusive_or_expression_test.cc
new file mode 100644
index 0000000..dfc7af9
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_exclusive_or_expression_test.cc
@@ -0,0 +1,75 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/bool_literal.h"
+#include "src/ast/const_initializer_expression.h"
+#include "src/ast/identifier_expression.h"
+#include "src/ast/relational_expression.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, ExclusiveOrExpression_Parses) {
+  ParserImpl p{"a ^ true"};
+  auto e = p.exclusive_or_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+
+  ASSERT_TRUE(e->IsRelational());
+  auto rel = e->AsRelational();
+  EXPECT_EQ(ast::Relation::kXor, rel->relation());
+
+  ASSERT_TRUE(rel->lhs()->IsIdentifier());
+  auto ident = rel->lhs()->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 1);
+  EXPECT_EQ(ident->name()[0], "a");
+
+  ASSERT_TRUE(rel->rhs()->IsInitializer());
+  ASSERT_TRUE(rel->rhs()->AsInitializer()->IsConstInitializer());
+  auto init = rel->rhs()->AsInitializer()->AsConstInitializer();
+  ASSERT_TRUE(init->literal()->IsBool());
+  ASSERT_TRUE(init->literal()->AsBool()->IsTrue());
+}
+
+TEST_F(ParserImplTest, ExclusiveOrExpression_InvalidLHS) {
+  ParserImpl p{"if (a) {} ^ true"};
+  auto e = p.exclusive_or_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_EQ(e, nullptr);
+}
+
+TEST_F(ParserImplTest, ExclusiveOrExpression_InvalidRHS) {
+  ParserImpl p{"true ^ if (a) {}"};
+  auto e = p.exclusive_or_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:8: unable to parse right side of ^ expression");
+}
+
+TEST_F(ParserImplTest, ExclusiveOrExpression_NoOr_ReturnsLHS) {
+  ParserImpl p{"a true"};
+  auto e = p.exclusive_or_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsIdentifier());
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_function_decl_test.cc b/src/reader/wgsl/parser_impl_function_decl_test.cc
new file mode 100644
index 0000000..869e3d5
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_function_decl_test.cc
@@ -0,0 +1,65 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/function.h"
+#include "src/ast/type/type.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, FunctionDecl) {
+  ParserImpl p{"fn main(a : i32, b : f32) -> void { return; }"};
+  auto f = p.function_decl();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(f, nullptr);
+
+  EXPECT_EQ(f->name(), "main");
+  ASSERT_NE(f->return_type(), nullptr);
+  EXPECT_TRUE(f->return_type()->IsVoid());
+
+  ASSERT_EQ(f->params().size(), 2);
+  EXPECT_EQ(f->params()[0]->name(), "a");
+  EXPECT_EQ(f->params()[1]->name(), "b");
+
+  ASSERT_NE(f->return_type(), nullptr);
+  EXPECT_TRUE(f->return_type()->IsVoid());
+
+  ASSERT_EQ(f->body().size(), 1);
+  EXPECT_TRUE(f->body()[0]->IsReturn());
+}
+
+TEST_F(ParserImplTest, FunctionDecl_InvalidHeader) {
+  ParserImpl p{"fn main() -> { }"};
+  auto f = p.function_decl();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(f, nullptr);
+  EXPECT_EQ(p.error(), "1:14: unable to determine function return type");
+}
+
+TEST_F(ParserImplTest, FunctionDecl_InvalidBody) {
+  ParserImpl p{"fn main() -> void { return }"};
+  auto f = p.function_decl();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(f, nullptr);
+  EXPECT_EQ(p.error(), "1:28: missing ;");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_function_header_test.cc b/src/reader/wgsl/parser_impl_function_header_test.cc
new file mode 100644
index 0000000..1ac9bc6
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_function_header_test.cc
@@ -0,0 +1,105 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/function.h"
+#include "src/ast/type/type.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, FunctionHeader) {
+  ParserImpl p{"fn main(a : i32, b: f32) -> void"};
+  auto f = p.function_header();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(f, nullptr);
+
+  EXPECT_EQ(f->name(), "main");
+  ASSERT_EQ(f->params().size(), 2);
+  EXPECT_EQ(f->params()[0]->name(), "a");
+  EXPECT_EQ(f->params()[1]->name(), "b");
+  EXPECT_TRUE(f->return_type()->IsVoid());
+}
+
+TEST_F(ParserImplTest, FunctionHeader_MissingIdent) {
+  ParserImpl p{"fn () ->"};
+  auto f = p.function_header();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(f, nullptr);
+  EXPECT_EQ(p.error(), "1:4: missing identifier for function");
+}
+
+TEST_F(ParserImplTest, FunctionHeader_InvalidIdent) {
+  ParserImpl p{"fn 133main() -> i32"};
+  auto f = p.function_header();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(f, nullptr);
+  EXPECT_EQ(p.error(), "1:4: missing identifier for function");
+}
+
+TEST_F(ParserImplTest, FunctionHeader_MissingParenLeft) {
+  ParserImpl p{"fn main) -> i32"};
+  auto f = p.function_header();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(f, nullptr);
+  EXPECT_EQ(p.error(), "1:8: missing ( for function declaration");
+}
+
+TEST_F(ParserImplTest, FunctionHeader_InvalidParamList) {
+  ParserImpl p{"fn main(a :i32,) -> i32"};
+  auto f = p.function_header();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(f, nullptr);
+  EXPECT_EQ(p.error(), "1:15: found , but no variable declaration");
+}
+
+TEST_F(ParserImplTest, FunctionHeader_MissingParenRight) {
+  ParserImpl p{"fn main( -> i32"};
+  auto f = p.function_header();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(f, nullptr);
+  EXPECT_EQ(p.error(), "1:10: missing ) for function declaration");
+}
+
+TEST_F(ParserImplTest, FunctionHeader_MissingArrow) {
+  ParserImpl p{"fn main() i32"};
+  auto f = p.function_header();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(f, nullptr);
+  EXPECT_EQ(p.error(), "1:11: missing -> for function declaration");
+}
+
+TEST_F(ParserImplTest, FunctionHeader_InvalidReturnType) {
+  ParserImpl p{"fn main() -> invalid"};
+  auto f = p.function_header();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(f, nullptr);
+  EXPECT_EQ(p.error(), "1:14: unknown type alias 'invalid'");
+}
+
+TEST_F(ParserImplTest, FunctionHeader_MissingReturnType) {
+  ParserImpl p{"fn main() ->"};
+  auto f = p.function_header();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(f, nullptr);
+  EXPECT_EQ(p.error(), "1:13: unable to determine function return type");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_function_type_decl_test.cc b/src/reader/wgsl/parser_impl_function_type_decl_test.cc
new file mode 100644
index 0000000..59e17bc
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_function_type_decl_test.cc
@@ -0,0 +1,65 @@
+// 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 <memory>
+
+#include "gtest/gtest.h"
+#include "src/ast/type/f32_type.h"
+#include "src/ast/type/vector_type.h"
+#include "src/ast/type/void_type.h"
+#include "src/reader/wgsl/parser_impl.h"
+#include "src/type_manager.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, FunctionTypeDecl_Void) {
+  auto tm = TypeManager::Instance();
+  auto v = tm->Get(std::make_unique<ast::type::VoidType>());
+
+  ParserImpl p{"void"};
+  auto e = p.function_type_decl();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_EQ(e, v);
+
+  TypeManager::Destroy();
+}
+
+TEST_F(ParserImplTest, FunctionTypeDecl_Type) {
+  auto tm = TypeManager::Instance();
+  auto f32 = tm->Get(std::make_unique<ast::type::F32Type>());
+  auto vec2 = tm->Get(std::make_unique<ast::type::VectorType>(f32, 2));
+
+  ParserImpl p{"vec2<f32>"};
+  auto e = p.function_type_decl();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_EQ(e, vec2);
+
+  TypeManager::Destroy();
+}
+
+TEST_F(ParserImplTest, FunctionTypeDecl_InvalidType) {
+  ParserImpl p{"vec2<invalid>"};
+  auto e = p.function_type_decl();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:6: unknown type alias 'invalid'");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_global_constant_decl_test.cc b/src/reader/wgsl/parser_impl_global_constant_decl_test.cc
new file mode 100644
index 0000000..5f9768b
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_global_constant_decl_test.cc
@@ -0,0 +1,75 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/decorated_variable.h"
+#include "src/ast/variable_decoration.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, GlobalConstantDecl) {
+  ParserImpl p{"const a : f32 = 1."};
+  auto e = p.global_constant_decl();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+
+  EXPECT_TRUE(e->is_const());
+  EXPECT_EQ(e->name(), "a");
+  ASSERT_NE(e->type(), nullptr);
+  EXPECT_TRUE(e->type()->IsF32());
+
+  ASSERT_NE(e->initializer(), nullptr);
+  EXPECT_TRUE(e->initializer()->IsInitializer());
+}
+
+TEST_F(ParserImplTest, GlobalConstantDecl_MissingEqual) {
+  ParserImpl p{"const a: f32 1."};
+  auto e = p.global_constant_decl();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:14: missing = for const declaration");
+}
+
+TEST_F(ParserImplTest, GlobalConstantDecl_InvalidVariable) {
+  ParserImpl p{"const a: invalid = 1."};
+  auto e = p.global_constant_decl();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:10: unknown type alias 'invalid'");
+}
+
+TEST_F(ParserImplTest, GlobalConstantDecl_InvalidExpression) {
+  ParserImpl p{"const a: f32 = if (a) {}"};
+  auto e = p.global_constant_decl();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:16: unable to parse const literal");
+}
+
+TEST_F(ParserImplTest, GlobalConstantDecl_MissingExpression) {
+  ParserImpl p{"const a: f32 ="};
+  auto e = p.global_constant_decl();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:15: unable to parse const literal");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_global_decl_test.cc b/src/reader/wgsl/parser_impl_global_decl_test.cc
new file mode 100644
index 0000000..86c22a7
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_global_decl_test.cc
@@ -0,0 +1,178 @@
+// 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 "gtest/gtest.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, GlobalDecl_Semicolon) {
+  ParserImpl p(";");
+  p.global_decl();
+  ASSERT_FALSE(p.has_error()) << p.error();
+}
+
+TEST_F(ParserImplTest, GlobalDecl_Import) {
+  ParserImpl p{R"(import "GLSL.std.430" as glsl;)"};
+  p.global_decl();
+  ASSERT_FALSE(p.has_error()) << p.error();
+
+  auto m = p.module();
+  ASSERT_EQ(1, m.imports().size());
+
+  const auto& import = m.imports()[0];
+  EXPECT_EQ("GLSL.std.430", import->path());
+  EXPECT_EQ("glsl", import->name());
+}
+
+TEST_F(ParserImplTest, GlobalDecl_Import_Invalid) {
+  ParserImpl p{R"(import as glsl;)"};
+  p.global_decl();
+
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(p.error(), "1:8: missing path for import");
+}
+
+TEST_F(ParserImplTest, GlobalDecl_Import_Invalid_MissingSemicolon) {
+  ParserImpl p{R"(import "GLSL.std.430" as glsl)"};
+  p.global_decl();
+
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(p.error(), "1:30: missing ';' for import");
+}
+
+TEST_F(ParserImplTest, GlobalDecl_GlobalVariable) {
+  ParserImpl p{"var<out> a : vec2<i32> = vec2<i32>(1, 2);"};
+  p.global_decl();
+  ASSERT_FALSE(p.has_error()) << p.error();
+
+  auto m = p.module();
+  ASSERT_EQ(m.global_variables().size(), 1);
+
+  auto v = m.global_variables()[0].get();
+  EXPECT_EQ(v->name(), "a");
+}
+
+TEST_F(ParserImplTest, GlobalDecl_GlobalVariable_Invalid) {
+  ParserImpl p{"var<out> a : vec2<invalid>;"};
+  p.global_decl();
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(p.error(), "1:19: unknown type alias 'invalid'");
+}
+
+TEST_F(ParserImplTest, GlobalDecl_GlobalVariable_MissingSemicolon) {
+  ParserImpl p{"var<out> a : vec2<i32>"};
+  p.global_decl();
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(p.error(), "1:23: missing ';' for variable declaration");
+}
+
+TEST_F(ParserImplTest, GlobalDecl_GlobalConstant) {
+  ParserImpl p{"const a : i32 = 2;"};
+  p.global_decl();
+  ASSERT_FALSE(p.has_error()) << p.error();
+
+  auto m = p.module();
+  ASSERT_EQ(m.global_variables().size(), 1);
+
+  auto v = m.global_variables()[0].get();
+  EXPECT_EQ(v->name(), "a");
+}
+
+TEST_F(ParserImplTest, GlobalDecl_GlobalConstant_Invalid) {
+  ParserImpl p{"const a : vec2<i32>;"};
+  p.global_decl();
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(p.error(), "1:20: missing = for const declaration");
+}
+
+TEST_F(ParserImplTest, GlobalDecl_GlobalConstant_MissingSemicolon) {
+  ParserImpl p{"const a : vec2<i32> = vec2<i32>(1, 2)"};
+  p.global_decl();
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(p.error(), "1:38: missing ';' for constant declaration");
+}
+
+TEST_F(ParserImplTest, GlobalDecl_EntryPoint) {
+  ParserImpl p{"entry_point vertex = main;"};
+  p.global_decl();
+  ASSERT_FALSE(p.has_error()) << p.error();
+
+  auto m = p.module();
+  ASSERT_EQ(m.entry_points().size(), 1);
+  EXPECT_EQ(m.entry_points()[0]->name(), "main");
+}
+
+TEST_F(ParserImplTest, GlobalDecl_EntryPoint_Invalid) {
+  ParserImpl p{"entry_point main;"};
+  p.global_decl();
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(p.error(), "1:13: missing pipeline stage for entry point");
+}
+
+TEST_F(ParserImplTest, GlobalDecl_EntryPoint_MissingSemicolon) {
+  ParserImpl p{"entry_point vertex = main"};
+  p.global_decl();
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(p.error(), "1:26: missing ';' for entry point");
+}
+
+TEST_F(ParserImplTest, GlobalDecl_TypeAlias) {
+  ParserImpl p{"type A = i32;"};
+  p.global_decl();
+  ASSERT_FALSE(p.has_error()) << p.error();
+
+  auto m = p.module();
+  ASSERT_EQ(m.alias_types().size(), 1);
+  EXPECT_EQ(m.alias_types()[0]->name(), "A");
+}
+
+TEST_F(ParserImplTest, GlobalDecl_TypeAlias_Invalid) {
+  ParserImpl p{"type A = invalid;"};
+  p.global_decl();
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(p.error(), "1:10: unknown type alias 'invalid'");
+}
+
+TEST_F(ParserImplTest, GlobalDecl_TypeAlias_MissingSemicolon) {
+  ParserImpl p{"type A = i32"};
+  p.global_decl();
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(p.error(), "1:13: missing ';' for type alias");
+}
+
+TEST_F(ParserImplTest, GlobalDecl_Function) {
+  ParserImpl p{"fn main() -> void { return; }"};
+  p.global_decl();
+  ASSERT_FALSE(p.has_error()) << p.error();
+
+  auto m = p.module();
+  ASSERT_EQ(m.functions().size(), 1);
+  EXPECT_EQ(m.functions()[0]->name(), "main");
+}
+
+TEST_F(ParserImplTest, GlobalDecl_Function_Invalid) {
+  ParserImpl p{"fn main() -> { return; }"};
+  p.global_decl();
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(p.error(), "1:14: unable to determine function return type");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_global_variable_decl_test.cc b/src/reader/wgsl/parser_impl_global_variable_decl_test.cc
new file mode 100644
index 0000000..528135c
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_global_variable_decl_test.cc
@@ -0,0 +1,106 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/decorated_variable.h"
+#include "src/ast/variable_decoration.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, GlobalVariableDecl_WithoutInitializer) {
+  ParserImpl p{"var<out> a : f32"};
+  auto e = p.global_variable_decl();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+
+  EXPECT_EQ(e->name(), "a");
+  EXPECT_TRUE(e->type()->IsF32());
+  EXPECT_EQ(e->storage_class(), ast::StorageClass::kOutput);
+
+  ASSERT_EQ(e->initializer(), nullptr);
+  ASSERT_FALSE(e->IsDecorated());
+}
+
+TEST_F(ParserImplTest, GlobalVariableDecl_WithInitializer) {
+  ParserImpl p{"var<out> a : f32 = 1."};
+  auto e = p.global_variable_decl();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+
+  EXPECT_EQ(e->name(), "a");
+  EXPECT_TRUE(e->type()->IsF32());
+  EXPECT_EQ(e->storage_class(), ast::StorageClass::kOutput);
+
+  ASSERT_NE(e->initializer(), nullptr);
+  ASSERT_TRUE(e->initializer()->IsInitializer());
+  ASSERT_TRUE(e->initializer()->AsInitializer()->IsConstInitializer());
+
+  ASSERT_FALSE(e->IsDecorated());
+}
+
+TEST_F(ParserImplTest, GlobalVariableDecl_WithDecoration) {
+  ParserImpl p{"[[binding 2, set 1]] var<out> a : f32"};
+  auto e = p.global_variable_decl();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsDecorated());
+
+  EXPECT_EQ(e->name(), "a");
+  ASSERT_NE(e->type(), nullptr);
+  EXPECT_TRUE(e->type()->IsF32());
+  EXPECT_EQ(e->storage_class(), ast::StorageClass::kOutput);
+
+  ASSERT_EQ(e->initializer(), nullptr);
+
+  ASSERT_TRUE(e->IsDecorated());
+  auto v = e->AsDecorated();
+
+  auto& decos = v->decorations();
+  ASSERT_EQ(decos.size(), 2);
+  ASSERT_TRUE(decos[0]->IsBinding());
+  ASSERT_TRUE(decos[1]->IsSet());
+}
+
+TEST_F(ParserImplTest, GlobalVariableDecl_InvalidDecoration) {
+  ParserImpl p{"[[binding]] var<out> a : f32"};
+  auto e = p.global_variable_decl();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:10: invalid value for binding decoration");
+}
+
+TEST_F(ParserImplTest, GlobalVariableDecl_InvalidConstExpr) {
+  ParserImpl p{"var<out> a : f32 = if (a) {}"};
+  auto e = p.global_variable_decl();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:20: unable to parse const literal");
+}
+
+TEST_F(ParserImplTest, GlobalVariableDecl_InvalidVariableDecl) {
+  ParserImpl p{"var<invalid> a : f32;"};
+  auto e = p.global_variable_decl();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:5: invalid storage class for variable decoration");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_if_stmt_test.cc b/src/reader/wgsl/parser_impl_if_stmt_test.cc
new file mode 100644
index 0000000..32fa433
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_if_stmt_test.cc
@@ -0,0 +1,143 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/else_statement.h"
+#include "src/ast/if_statement.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, IfStmt) {
+  ParserImpl p{"if (a == 4) { a = b; c = d; }"};
+  auto e = p.if_stmt();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+
+  ASSERT_TRUE(e->IsIf());
+  ASSERT_NE(e->condition(), nullptr);
+  ASSERT_TRUE(e->condition()->IsRelational());
+  EXPECT_EQ(e->body().size(), 2);
+  EXPECT_EQ(e->else_statements().size(), 0);
+  EXPECT_EQ(e->premerge().size(), 0);
+}
+
+TEST_F(ParserImplTest, IfStmt_WithElse) {
+  ParserImpl p{"if (a == 4) { a = b; c = d; } elseif(c) { d = 2; } else {}"};
+  auto e = p.if_stmt();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+
+  ASSERT_TRUE(e->IsIf());
+  ASSERT_NE(e->condition(), nullptr);
+  ASSERT_TRUE(e->condition()->IsRelational());
+  EXPECT_EQ(e->body().size(), 2);
+
+  ASSERT_EQ(e->else_statements().size(), 2);
+  ASSERT_NE(e->else_statements()[0]->condition(), nullptr);
+  ASSERT_TRUE(e->else_statements()[0]->condition()->IsIdentifier());
+  EXPECT_EQ(e->else_statements()[0]->body().size(), 1);
+
+  ASSERT_EQ(e->else_statements()[1]->condition(), nullptr);
+  EXPECT_EQ(e->else_statements()[1]->body().size(), 0);
+}
+
+TEST_F(ParserImplTest, IfStmt_WithPremerge) {
+  ParserImpl p{R"(if (a == 4) {
+  a = b;
+  c = d;
+} else {
+  d = 2;
+} premerge {
+  a = 2;
+})"};
+  auto e = p.if_stmt();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+
+  ASSERT_TRUE(e->IsIf());
+  ASSERT_NE(e->condition(), nullptr);
+  ASSERT_TRUE(e->condition()->IsRelational());
+  EXPECT_EQ(e->body().size(), 2);
+
+  ASSERT_EQ(e->else_statements().size(), 1);
+  ASSERT_EQ(e->else_statements()[0]->condition(), nullptr);
+  EXPECT_EQ(e->else_statements()[0]->body().size(), 1);
+
+  ASSERT_EQ(e->premerge().size(), 1);
+}
+
+TEST_F(ParserImplTest, IfStmt_InvalidCondition) {
+  ParserImpl p{"if (a = 3) {}"};
+  auto e = p.if_stmt();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:7: expected )");
+}
+
+TEST_F(ParserImplTest, IfStmt_MissingCondition) {
+  ParserImpl p{"if {}"};
+  auto e = p.if_stmt();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:4: expected (");
+}
+
+TEST_F(ParserImplTest, IfStmt_InvalidBody) {
+  ParserImpl p{"if (a) { fn main() -> void {}}"};
+  auto e = p.if_stmt();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:10: missing }");
+}
+
+TEST_F(ParserImplTest, IfStmt_MissingBody) {
+  ParserImpl p{"if (a)"};
+  auto e = p.if_stmt();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:7: missing {");
+}
+
+TEST_F(ParserImplTest, IfStmt_InvalidElseif) {
+  ParserImpl p{"if (a) {} elseif (a) { fn main() -> a{}}"};
+  auto e = p.if_stmt();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:24: missing }");
+}
+
+TEST_F(ParserImplTest, IfStmt_InvalidElse) {
+  ParserImpl p{"if (a) {} else { fn main() -> a{}}"};
+  auto e = p.if_stmt();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:18: missing }");
+}
+
+TEST_F(ParserImplTest, IfStmt_InvalidPremerge) {
+  ParserImpl p{"if (a) {} else {} premerge { fn main() -> a{}}"};
+  auto e = p.if_stmt();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:30: missing }");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_import_decl_test.cc b/src/reader/wgsl/parser_impl_import_decl_test.cc
new file mode 100644
index 0000000..bfd6cc9
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_import_decl_test.cc
@@ -0,0 +1,95 @@
+// 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 "gtest/gtest.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, ImportDecl_Import) {
+  ParserImpl p{R"(import "GLSL.std.450" as glsl)"};
+
+  auto import = p.import_decl();
+  ASSERT_NE(import, nullptr);
+  ASSERT_FALSE(p.has_error()) << p.error();
+
+  EXPECT_EQ("GLSL.std.450", import->path());
+  EXPECT_EQ("glsl", import->name());
+  EXPECT_EQ(1, import->line());
+  EXPECT_EQ(1, import->column());
+}
+
+TEST_F(ParserImplTest, ImportDecl_Import_WithNamespace) {
+  ParserImpl p{R"(import "GLSL.std.450" as std::glsl)"};
+  auto import = p.import_decl();
+  ASSERT_NE(import, nullptr);
+  ASSERT_FALSE(p.has_error()) << p.error();
+  EXPECT_EQ("std::glsl", import->name());
+}
+
+TEST_F(ParserImplTest, ImportDecl_Invalid_MissingPath) {
+  ParserImpl p{R"(import as glsl)"};
+  auto import = p.import_decl();
+  ASSERT_EQ(import, nullptr);
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(p.error(), "1:8: missing path for import");
+}
+
+TEST_F(ParserImplTest, ImportDecl_Invalid_EmptyPath) {
+  ParserImpl p{R"(import "" as glsl)"};
+  auto import = p.import_decl();
+  ASSERT_EQ(import, nullptr);
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(p.error(), "1:8: import path must not be empty");
+}
+
+TEST_F(ParserImplTest, ImportDecl_Invalid_NameMissingTerminatingIdentifier) {
+  ParserImpl p{R"(import "GLSL.std.450" as glsl::)"};
+  auto import = p.import_decl();
+  ASSERT_EQ(import, nullptr);
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(p.error(), "1:32: invalid name for import");
+}
+
+TEST_F(ParserImplTest, ImportDecl_Invalid_NameInvalid) {
+  ParserImpl p{R"(import "GLSL.std.450" as 12glsl)"};
+  auto import = p.import_decl();
+  ASSERT_EQ(import, nullptr);
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(p.error(), "1:26: invalid name for import");
+}
+
+TEST_F(ParserImplTest, ImportDecl_Invalid_MissingName) {
+  ParserImpl p{R"(import "GLSL.std.450" as)"};
+  auto import = p.import_decl();
+  ASSERT_EQ(import, nullptr);
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(p.error(), "1:25: missing name for import");
+}
+
+TEST_F(ParserImplTest, ImportDecl_Invalid_MissingAs) {
+  ParserImpl p{R"(import "GLSL.std.450" glsl)"};
+  auto import = p.import_decl();
+  ASSERT_EQ(import, nullptr);
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(p.error(), "1:23: missing 'as' for import");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_inclusive_or_expression_test.cc b/src/reader/wgsl/parser_impl_inclusive_or_expression_test.cc
new file mode 100644
index 0000000..38223fb
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_inclusive_or_expression_test.cc
@@ -0,0 +1,75 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/bool_literal.h"
+#include "src/ast/const_initializer_expression.h"
+#include "src/ast/identifier_expression.h"
+#include "src/ast/relational_expression.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, InclusiveOrExpression_Parses) {
+  ParserImpl p{"a | true"};
+  auto e = p.inclusive_or_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+
+  ASSERT_TRUE(e->IsRelational());
+  auto rel = e->AsRelational();
+  EXPECT_EQ(ast::Relation::kOr, rel->relation());
+
+  ASSERT_TRUE(rel->lhs()->IsIdentifier());
+  auto ident = rel->lhs()->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 1);
+  EXPECT_EQ(ident->name()[0], "a");
+
+  ASSERT_TRUE(rel->rhs()->IsInitializer());
+  ASSERT_TRUE(rel->rhs()->AsInitializer()->IsConstInitializer());
+  auto init = rel->rhs()->AsInitializer()->AsConstInitializer();
+  ASSERT_TRUE(init->literal()->IsBool());
+  ASSERT_TRUE(init->literal()->AsBool()->IsTrue());
+}
+
+TEST_F(ParserImplTest, InclusiveOrExpression_InvalidLHS) {
+  ParserImpl p{"if (a) {} | true"};
+  auto e = p.inclusive_or_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_EQ(e, nullptr);
+}
+
+TEST_F(ParserImplTest, InclusiveOrExpression_InvalidRHS) {
+  ParserImpl p{"true | if (a) {}"};
+  auto e = p.inclusive_or_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:8: unable to parse right side of | expression");
+}
+
+TEST_F(ParserImplTest, InclusiveOrExpression_NoOr_ReturnsLHS) {
+  ParserImpl p{"a true"};
+  auto e = p.inclusive_or_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsIdentifier());
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_logical_and_expression_test.cc b/src/reader/wgsl/parser_impl_logical_and_expression_test.cc
new file mode 100644
index 0000000..c48cca5
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_logical_and_expression_test.cc
@@ -0,0 +1,75 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/bool_literal.h"
+#include "src/ast/const_initializer_expression.h"
+#include "src/ast/identifier_expression.h"
+#include "src/ast/relational_expression.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, LogicalAndExpression_Parses) {
+  ParserImpl p{"a && true"};
+  auto e = p.logical_and_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+
+  ASSERT_TRUE(e->IsRelational());
+  auto rel = e->AsRelational();
+  EXPECT_EQ(ast::Relation::kLogicalAnd, rel->relation());
+
+  ASSERT_TRUE(rel->lhs()->IsIdentifier());
+  auto ident = rel->lhs()->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 1);
+  EXPECT_EQ(ident->name()[0], "a");
+
+  ASSERT_TRUE(rel->rhs()->IsInitializer());
+  ASSERT_TRUE(rel->rhs()->AsInitializer()->IsConstInitializer());
+  auto init = rel->rhs()->AsInitializer()->AsConstInitializer();
+  ASSERT_TRUE(init->literal()->IsBool());
+  ASSERT_TRUE(init->literal()->AsBool()->IsTrue());
+}
+
+TEST_F(ParserImplTest, LogicalAndExpression_InvalidLHS) {
+  ParserImpl p{"if (a) {} && true"};
+  auto e = p.logical_and_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_EQ(e, nullptr);
+}
+
+TEST_F(ParserImplTest, LogicalAndExpression_InvalidRHS) {
+  ParserImpl p{"true && if (a) {}"};
+  auto e = p.logical_and_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:9: unable to parse right side of && expression");
+}
+
+TEST_F(ParserImplTest, LogicalAndExpression_NoOr_ReturnsLHS) {
+  ParserImpl p{"a true"};
+  auto e = p.logical_and_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsIdentifier());
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_logical_or_expression_test.cc b/src/reader/wgsl/parser_impl_logical_or_expression_test.cc
new file mode 100644
index 0000000..9c90b66
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_logical_or_expression_test.cc
@@ -0,0 +1,75 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/bool_literal.h"
+#include "src/ast/const_initializer_expression.h"
+#include "src/ast/identifier_expression.h"
+#include "src/ast/relational_expression.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, LogicalOrExpression_Parses) {
+  ParserImpl p{"a || true"};
+  auto e = p.logical_or_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+
+  ASSERT_TRUE(e->IsRelational());
+  auto rel = e->AsRelational();
+  EXPECT_EQ(ast::Relation::kLogicalOr, rel->relation());
+
+  ASSERT_TRUE(rel->lhs()->IsIdentifier());
+  auto ident = rel->lhs()->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 1);
+  EXPECT_EQ(ident->name()[0], "a");
+
+  ASSERT_TRUE(rel->rhs()->IsInitializer());
+  ASSERT_TRUE(rel->rhs()->AsInitializer()->IsConstInitializer());
+  auto init = rel->rhs()->AsInitializer()->AsConstInitializer();
+  ASSERT_TRUE(init->literal()->IsBool());
+  ASSERT_TRUE(init->literal()->AsBool()->IsTrue());
+}
+
+TEST_F(ParserImplTest, LogicalOrExpression_InvalidLHS) {
+  ParserImpl p{"if (a) {} || true"};
+  auto e = p.logical_or_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_EQ(e, nullptr);
+}
+
+TEST_F(ParserImplTest, LogicalOrExpression_InvalidRHS) {
+  ParserImpl p{"true || if (a) {}"};
+  auto e = p.logical_or_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:9: unable to parse right side of || expression");
+}
+
+TEST_F(ParserImplTest, LogicalOrExpression_NoOr_ReturnsLHS) {
+  ParserImpl p{"a true"};
+  auto e = p.logical_or_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsIdentifier());
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_loop_stmt_test.cc b/src/reader/wgsl/parser_impl_loop_stmt_test.cc
new file mode 100644
index 0000000..8896582
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_loop_stmt_test.cc
@@ -0,0 +1,102 @@
+// 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 "gtest/gtest.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, LoopStmt_BodyNoContinuing) {
+  ParserImpl p{"loop { nop; }"};
+  auto e = p.loop_stmt();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+
+  ASSERT_EQ(e->body().size(), 1);
+  EXPECT_TRUE(e->body()[0]->IsNop());
+
+  EXPECT_EQ(e->continuing().size(), 0);
+}
+
+TEST_F(ParserImplTest, LoopStmt_BodyWithContinuing) {
+  ParserImpl p{"loop { nop; continuing { kill; }}"};
+  auto e = p.loop_stmt();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+
+  ASSERT_EQ(e->body().size(), 1);
+  EXPECT_TRUE(e->body()[0]->IsNop());
+
+  EXPECT_EQ(e->continuing().size(), 1);
+  EXPECT_TRUE(e->continuing()[0]->IsKill());
+}
+
+TEST_F(ParserImplTest, LoopStmt_NoBodyNoContinuing) {
+  ParserImpl p{"loop { }"};
+  auto e = p.loop_stmt();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_EQ(e->body().size(), 0);
+  ASSERT_EQ(e->continuing().size(), 0);
+}
+
+TEST_F(ParserImplTest, LoopStmt_NoBodyWithContinuing) {
+  ParserImpl p{"loop { continuing { kill; }}"};
+  auto e = p.loop_stmt();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_EQ(e->body().size(), 0);
+  ASSERT_EQ(e->continuing().size(), 1);
+  EXPECT_TRUE(e->continuing()[0]->IsKill());
+}
+
+TEST_F(ParserImplTest, LoopStmt_MissingBracketLeft) {
+  ParserImpl p{"loop kill; }"};
+  auto e = p.loop_stmt();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:6: missing { for loop");
+}
+
+TEST_F(ParserImplTest, LoopStmt_MissingBracketRight) {
+  ParserImpl p{"loop { kill; "};
+  auto e = p.loop_stmt();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:14: missing } for loop");
+}
+
+TEST_F(ParserImplTest, LoopStmt_InvalidStatements) {
+  ParserImpl p{"loop { kill }"};
+  auto e = p.loop_stmt();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:13: missing ;");
+}
+
+TEST_F(ParserImplTest, LoopStmt_InvalidContinuing) {
+  ParserImpl p{"loop { continuing { kill }}"};
+  auto e = p.loop_stmt();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:26: missing ;");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_multiplicative_expression_test.cc b/src/reader/wgsl/parser_impl_multiplicative_expression_test.cc
new file mode 100644
index 0000000..bf32a1d
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_multiplicative_expression_test.cc
@@ -0,0 +1,119 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/bool_literal.h"
+#include "src/ast/const_initializer_expression.h"
+#include "src/ast/identifier_expression.h"
+#include "src/ast/relational_expression.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, MultiplicativeExpression_Parses_Multiply) {
+  ParserImpl p{"a * true"};
+  auto e = p.multiplicative_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+
+  ASSERT_TRUE(e->IsRelational());
+  auto rel = e->AsRelational();
+  EXPECT_EQ(ast::Relation::kMultiply, rel->relation());
+
+  ASSERT_TRUE(rel->lhs()->IsIdentifier());
+  auto ident = rel->lhs()->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 1);
+  EXPECT_EQ(ident->name()[0], "a");
+
+  ASSERT_TRUE(rel->rhs()->IsInitializer());
+  ASSERT_TRUE(rel->rhs()->AsInitializer()->IsConstInitializer());
+  auto init = rel->rhs()->AsInitializer()->AsConstInitializer();
+  ASSERT_TRUE(init->literal()->IsBool());
+  ASSERT_TRUE(init->literal()->AsBool()->IsTrue());
+}
+
+TEST_F(ParserImplTest, MultiplicativeExpression_Parses_Divide) {
+  ParserImpl p{"a / true"};
+  auto e = p.multiplicative_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+
+  ASSERT_TRUE(e->IsRelational());
+  auto rel = e->AsRelational();
+  EXPECT_EQ(ast::Relation::kDivide, rel->relation());
+
+  ASSERT_TRUE(rel->lhs()->IsIdentifier());
+  auto ident = rel->lhs()->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 1);
+  EXPECT_EQ(ident->name()[0], "a");
+
+  ASSERT_TRUE(rel->rhs()->IsInitializer());
+  ASSERT_TRUE(rel->rhs()->AsInitializer()->IsConstInitializer());
+  auto init = rel->rhs()->AsInitializer()->AsConstInitializer();
+  ASSERT_TRUE(init->literal()->IsBool());
+  ASSERT_TRUE(init->literal()->AsBool()->IsTrue());
+}
+
+TEST_F(ParserImplTest, MultiplicativeExpression_Parses_Modulo) {
+  ParserImpl p{"a % true"};
+  auto e = p.multiplicative_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+
+  ASSERT_TRUE(e->IsRelational());
+  auto rel = e->AsRelational();
+  EXPECT_EQ(ast::Relation::kModulo, rel->relation());
+
+  ASSERT_TRUE(rel->lhs()->IsIdentifier());
+  auto ident = rel->lhs()->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 1);
+  EXPECT_EQ(ident->name()[0], "a");
+
+  ASSERT_TRUE(rel->rhs()->IsInitializer());
+  ASSERT_TRUE(rel->rhs()->AsInitializer()->IsConstInitializer());
+  auto init = rel->rhs()->AsInitializer()->AsConstInitializer();
+  ASSERT_TRUE(init->literal()->IsBool());
+  ASSERT_TRUE(init->literal()->AsBool()->IsTrue());
+}
+
+TEST_F(ParserImplTest, MultiplicativeExpression_InvalidLHS) {
+  ParserImpl p{"if (a) {} * true"};
+  auto e = p.multiplicative_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_EQ(e, nullptr);
+}
+
+TEST_F(ParserImplTest, MultiplicativeExpression_InvalidRHS) {
+  ParserImpl p{"true * if (a) {}"};
+  auto e = p.multiplicative_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:8: unable to parse right side of * expression");
+}
+
+TEST_F(ParserImplTest, MultiplicativeExpression_NoOr_ReturnsLHS) {
+  ParserImpl p{"a true"};
+  auto e = p.multiplicative_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsIdentifier());
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_param_list_test.cc b/src/reader/wgsl/parser_impl_param_list_test.cc
new file mode 100644
index 0000000..05f4acd
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_param_list_test.cc
@@ -0,0 +1,85 @@
+// 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 <memory>
+
+#include "gtest/gtest.h"
+#include "src/ast/type/f32_type.h"
+#include "src/ast/type/i32_type.h"
+#include "src/ast/type/vector_type.h"
+#include "src/ast/variable.h"
+#include "src/reader/wgsl/parser_impl.h"
+#include "src/type_manager.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, ParamList_Single) {
+  auto tm = TypeManager::Instance();
+  auto i32 = tm->Get(std::make_unique<ast::type::I32Type>());
+
+  ParserImpl p{"a : i32"};
+  auto e = p.param_list();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  EXPECT_EQ(e.size(), 1);
+
+  EXPECT_EQ(e[0]->name(), "a");
+  EXPECT_EQ(e[0]->type(), i32);
+
+  TypeManager::Destroy();
+}
+
+TEST_F(ParserImplTest, ParamList_Multiple) {
+  auto tm = TypeManager::Instance();
+  auto i32 = tm->Get(std::make_unique<ast::type::I32Type>());
+  auto f32 = tm->Get(std::make_unique<ast::type::F32Type>());
+  auto vec2 = tm->Get(std::make_unique<ast::type::VectorType>(f32, 2));
+
+  ParserImpl p{"a : i32, b: f32, c: vec2<f32>"};
+  auto e = p.param_list();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  EXPECT_EQ(e.size(), 3);
+
+  EXPECT_EQ(e[0]->name(), "a");
+  EXPECT_EQ(e[0]->type(), i32);
+
+  EXPECT_EQ(e[1]->name(), "b");
+  EXPECT_EQ(e[1]->type(), f32);
+
+  EXPECT_EQ(e[2]->name(), "c");
+  EXPECT_EQ(e[2]->type(), vec2);
+
+  TypeManager::Destroy();
+}
+
+TEST_F(ParserImplTest, ParamList_Empty) {
+  ParserImpl p{""};
+  auto e = p.param_list();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  EXPECT_EQ(e.size(), 0);
+}
+
+TEST_F(ParserImplTest, ParamList_HangingComma) {
+  ParserImpl p{"a : i32,"};
+  auto e = p.param_list();
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(p.error(), "1:8: found , but no variable declaration");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_paren_rhs_stmt_test.cc b/src/reader/wgsl/parser_impl_paren_rhs_stmt_test.cc
new file mode 100644
index 0000000..42cae73
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_paren_rhs_stmt_test.cc
@@ -0,0 +1,66 @@
+// 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 "gtest/gtest.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, ParenRhsStmt) {
+  ParserImpl p{"(a + b)"};
+  auto e = p.paren_rhs_stmt();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsRelational());
+}
+
+TEST_F(ParserImplTest, ParenRhsStmt_MissingLeftParen) {
+  ParserImpl p{"true)"};
+  auto e = p.paren_rhs_stmt();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:1: expected (");
+}
+
+TEST_F(ParserImplTest, ParenRhsStmt_MissingRightParen) {
+  ParserImpl p{"(true"};
+  auto e = p.paren_rhs_stmt();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:6: expected )");
+}
+
+TEST_F(ParserImplTest, ParenRhsStmt_InvalidExpression) {
+  ParserImpl p{"(if (a() {})"};
+  auto e = p.paren_rhs_stmt();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:2: unable to parse expression");
+}
+
+TEST_F(ParserImplTest, ParenRhsStmt_MissingExpression) {
+  ParserImpl p{"()"};
+  auto e = p.paren_rhs_stmt();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:2: unable to parse expression");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_pipeline_stage_test.cc b/src/reader/wgsl/parser_impl_pipeline_stage_test.cc
new file mode 100644
index 0000000..fc4983c
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_pipeline_stage_test.cc
@@ -0,0 +1,65 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/pipeline_stage.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+struct PipelineStageData {
+  const char* input;
+  ast::PipelineStage result;
+};
+inline std::ostream& operator<<(std::ostream& out, PipelineStageData data) {
+  out << std::string(data.input);
+  return out;
+}
+using PipelineStageTest = testing::TestWithParam<PipelineStageData>;
+TEST_P(PipelineStageTest, Parses) {
+  auto params = GetParam();
+  ParserImpl p{params.input};
+
+  auto stage = p.pipeline_stage();
+  ASSERT_FALSE(p.has_error());
+  EXPECT_EQ(stage, params.result);
+
+  auto t = p.next();
+  EXPECT_TRUE(t.IsEof());
+}
+INSTANTIATE_TEST_SUITE_P(
+    ParserImplTest,
+    PipelineStageTest,
+    testing::Values(
+        PipelineStageData{"vertex", ast::PipelineStage::kVertex},
+        PipelineStageData{"fragment", ast::PipelineStage::kFragment},
+        PipelineStageData{"compute", ast::PipelineStage::kCompute}));
+
+TEST_F(ParserImplTest, PipelineStage_NoMatch) {
+  ParserImpl p{"not-a-stage"};
+  auto stage = p.pipeline_stage();
+  ASSERT_EQ(stage, ast::PipelineStage::kNone);
+
+  auto t = p.next();
+  EXPECT_TRUE(t.IsIdentifier());
+  EXPECT_EQ(t.to_str(), "not");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_postfix_expression_test.cc b/src/reader/wgsl/parser_impl_postfix_expression_test.cc
new file mode 100644
index 0000000..01782ee
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_postfix_expression_test.cc
@@ -0,0 +1,200 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/array_accessor_expression.h"
+#include "src/ast/call_expression.h"
+#include "src/ast/const_initializer_expression.h"
+#include "src/ast/identifier_expression.h"
+#include "src/ast/int_literal.h"
+#include "src/ast/member_accessor_expression.h"
+#include "src/ast/unary_derivative_expression.h"
+#include "src/ast/unary_method_expression.h"
+#include "src/ast/unary_op_expression.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, PostfixExpression_Array_ConstantIndex) {
+  ParserImpl p{"a[1]"};
+  auto e = p.postfix_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+
+  ASSERT_TRUE(e->IsArrayAccessor());
+  auto ary = e->AsArrayAccessor();
+
+  ASSERT_TRUE(ary->array()->IsIdentifier());
+  auto ident = ary->array()->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 1);
+  EXPECT_EQ(ident->name()[0], "a");
+
+  ASSERT_TRUE(ary->idx_expr()->IsInitializer());
+  ASSERT_TRUE(ary->idx_expr()->AsInitializer()->IsConstInitializer());
+  auto c = ary->idx_expr()->AsInitializer()->AsConstInitializer();
+  ASSERT_TRUE(c->literal()->IsInt());
+  EXPECT_EQ(c->literal()->AsInt()->value(), 1);
+}
+
+TEST_F(ParserImplTest, PostfixExpression_Array_ExpressionIndex) {
+  ParserImpl p{"a[1 + b / 4]"};
+  auto e = p.postfix_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+
+  ASSERT_TRUE(e->IsArrayAccessor());
+  auto ary = e->AsArrayAccessor();
+
+  ASSERT_TRUE(ary->array()->IsIdentifier());
+  auto ident = ary->array()->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 1);
+  EXPECT_EQ(ident->name()[0], "a");
+
+  ASSERT_TRUE(ary->idx_expr()->IsRelational());
+}
+
+TEST_F(ParserImplTest, PostfixExpression_Array_MissingIndex) {
+  ParserImpl p{"a[]"};
+  auto e = p.postfix_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:3: unable to parse expression inside []");
+}
+
+TEST_F(ParserImplTest, PostfixExpression_Array_MissingRightBrace) {
+  ParserImpl p{"a[1"};
+  auto e = p.postfix_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:4: missing ] for array accessor");
+}
+
+TEST_F(ParserImplTest, PostfixExpression_Array_InvalidIndex) {
+  ParserImpl p{"a[if(a() {})]"};
+  auto e = p.postfix_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:3: unable to parse expression inside []");
+}
+
+TEST_F(ParserImplTest, PostfixExpression_Call_Empty) {
+  ParserImpl p{"a()"};
+  auto e = p.postfix_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+
+  ASSERT_TRUE(e->IsCall());
+  auto c = e->AsCall();
+
+  ASSERT_TRUE(c->func()->IsIdentifier());
+  auto func = c->func()->AsIdentifier();
+  ASSERT_EQ(func->name().size(), 1);
+  EXPECT_EQ(func->name()[0], "a");
+
+  EXPECT_EQ(c->params().size(), 0);
+}
+
+TEST_F(ParserImplTest, PostfixExpression_Call_WithArgs) {
+  ParserImpl p{"std::test(1, b, 2 + 3 / b)"};
+  auto e = p.postfix_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+
+  ASSERT_TRUE(e->IsCall());
+  auto c = e->AsCall();
+
+  ASSERT_TRUE(c->func()->IsIdentifier());
+  auto func = c->func()->AsIdentifier();
+  ASSERT_EQ(func->name().size(), 2);
+  EXPECT_EQ(func->name()[0], "std");
+  EXPECT_EQ(func->name()[1], "test");
+
+  EXPECT_EQ(c->params().size(), 3);
+  EXPECT_TRUE(c->params()[0]->IsInitializer());
+  EXPECT_TRUE(c->params()[1]->IsIdentifier());
+  EXPECT_TRUE(c->params()[2]->IsRelational());
+}
+
+TEST_F(ParserImplTest, PostfixExpression_Call_InvalidArg) {
+  ParserImpl p{"a(if(a) {})"};
+  auto e = p.postfix_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:3: unable to parse argument expression");
+}
+
+TEST_F(ParserImplTest, PostfixExpression_Call_HangingComma) {
+  ParserImpl p{"a(b, )"};
+  auto e = p.postfix_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:6: unable to parse argument expression after comma");
+}
+
+TEST_F(ParserImplTest, PostfixExpression_Call_MissingRightParen) {
+  ParserImpl p{"a("};
+  auto e = p.postfix_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:3: missing ) for call expression");
+}
+
+TEST_F(ParserImplTest, PostfixExpression_MemberAccessor) {
+  ParserImpl p{"a.b"};
+  auto e = p.postfix_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsMemberAccessor());
+
+  auto m = e->AsMemberAccessor();
+  ASSERT_TRUE(m->structure()->IsIdentifier());
+  ASSERT_EQ(m->structure()->AsIdentifier()->name().size(), 1);
+  EXPECT_EQ(m->structure()->AsIdentifier()->name()[0], "a");
+
+  ASSERT_TRUE(m->member()->IsIdentifier());
+  ASSERT_EQ(m->member()->AsIdentifier()->name().size(), 1);
+  EXPECT_EQ(m->member()->AsIdentifier()->name()[0], "b");
+}
+
+TEST_F(ParserImplTest, PostfixExpression_MemberAccesssor_InvalidIdent) {
+  ParserImpl p{"a.if"};
+  auto e = p.postfix_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:3: missing identifier for member accessor");
+}
+
+TEST_F(ParserImplTest, PostfixExpression_MemberAccessor_MissingIdent) {
+  ParserImpl p{"a."};
+  auto e = p.postfix_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:3: missing identifier for member accessor");
+}
+
+TEST_F(ParserImplTest, PostfixExpression_NonMatch_returnLHS) {
+  ParserImpl p{"a b"};
+  auto e = p.postfix_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsIdentifier());
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_premerge_stmt_test.cc b/src/reader/wgsl/parser_impl_premerge_stmt_test.cc
new file mode 100644
index 0000000..55c3744
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_premerge_stmt_test.cc
@@ -0,0 +1,42 @@
+// 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 "gtest/gtest.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, PremergeStmt) {
+  ParserImpl p{"premerge { nop; }"};
+  auto e = p.premerge_stmt();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_EQ(e.size(), 1);
+  ASSERT_TRUE(e[0]->IsNop());
+}
+
+TEST_F(ParserImplTest, PremergeStmt_InvalidBody) {
+  ParserImpl p{"premerge { nop }"};
+  auto e = p.premerge_stmt();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e.size(), 0);
+  EXPECT_EQ(p.error(), "1:16: missing ;");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_primary_expression_test.cc b/src/reader/wgsl/parser_impl_primary_expression_test.cc
new file mode 100644
index 0000000..f9d676a
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_primary_expression_test.cc
@@ -0,0 +1,335 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/array_accessor_expression.h"
+#include "src/ast/as_expression.h"
+#include "src/ast/bool_literal.h"
+#include "src/ast/cast_expression.h"
+#include "src/ast/const_initializer_expression.h"
+#include "src/ast/identifier_expression.h"
+#include "src/ast/int_literal.h"
+#include "src/ast/type/f32_type.h"
+#include "src/ast/type/i32_type.h"
+#include "src/ast/type_initializer_expression.h"
+#include "src/ast/unary_derivative_expression.h"
+#include "src/ast/unary_method_expression.h"
+#include "src/ast/unary_op_expression.h"
+#include "src/reader/wgsl/parser_impl.h"
+#include "src/type_manager.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, PrimaryExpression_Ident) {
+  ParserImpl p{"a"};
+  auto e = p.primary_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsIdentifier());
+  auto ident = e->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 1);
+  EXPECT_EQ(ident->name()[0], "a");
+}
+
+TEST_F(ParserImplTest, PrimaryExpression_Ident_WithNamespace) {
+  ParserImpl p{"a::b::c::d"};
+  auto e = p.primary_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsIdentifier());
+  auto ident = e->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 4);
+  EXPECT_EQ(ident->name()[0], "a");
+  EXPECT_EQ(ident->name()[1], "b");
+  EXPECT_EQ(ident->name()[2], "c");
+  EXPECT_EQ(ident->name()[3], "d");
+}
+
+TEST_F(ParserImplTest, PrimaryExpression_Ident_MissingIdent) {
+  ParserImpl p{"a::"};
+  auto e = p.primary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:4: identifier expected");
+}
+
+TEST_F(ParserImplTest, PrimaryExpression_TypeDecl) {
+  ParserImpl p{"vec4<i32>(1, 2, 3, 4))"};
+  auto e = p.primary_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsInitializer());
+  ASSERT_TRUE(e->AsInitializer()->IsTypeInitializer());
+  auto ty = e->AsInitializer()->AsTypeInitializer();
+
+  ASSERT_EQ(ty->values().size(), 4);
+  const auto& val = ty->values();
+  ASSERT_TRUE(val[0]->IsInitializer());
+  ASSERT_TRUE(val[0]->AsInitializer()->IsConstInitializer());
+  auto ident = val[0]->AsInitializer()->AsConstInitializer();
+  ASSERT_TRUE(ident->literal()->IsInt());
+  EXPECT_EQ(ident->literal()->AsInt()->value(), 1);
+
+  ASSERT_TRUE(val[1]->IsInitializer());
+  ASSERT_TRUE(val[1]->AsInitializer()->IsConstInitializer());
+  ident = val[1]->AsInitializer()->AsConstInitializer();
+  ASSERT_TRUE(ident->literal()->IsInt());
+  EXPECT_EQ(ident->literal()->AsInt()->value(), 2);
+
+  ASSERT_TRUE(val[2]->IsInitializer());
+  ASSERT_TRUE(val[2]->AsInitializer()->IsConstInitializer());
+  ident = val[2]->AsInitializer()->AsConstInitializer();
+  ASSERT_TRUE(ident->literal()->IsInt());
+  EXPECT_EQ(ident->literal()->AsInt()->value(), 3);
+
+  ASSERT_TRUE(val[3]->IsInitializer());
+  ASSERT_TRUE(val[3]->AsInitializer()->IsConstInitializer());
+  ident = val[3]->AsInitializer()->AsConstInitializer();
+  ASSERT_TRUE(ident->literal()->IsInt());
+  EXPECT_EQ(ident->literal()->AsInt()->value(), 4);
+}
+
+TEST_F(ParserImplTest, PrimaryExpression_TypeDecl_InvalidTypeDecl) {
+  ParserImpl p{"vec4<if>(2., 3., 4., 5.)"};
+  auto e = p.primary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:6: unable to determine subtype for vector");
+}
+
+TEST_F(ParserImplTest, PrimaryExpression_TypeDecl_MissingLeftParen) {
+  ParserImpl p{"vec4<f32> 2., 3., 4., 5.)"};
+  auto e = p.primary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:11: missing ( for type initializer");
+}
+
+TEST_F(ParserImplTest, PrimaryExpression_TypeDecl_MissingRightParen) {
+  ParserImpl p{"vec4<f32>(2., 3., 4., 5."};
+  auto e = p.primary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:25: missing ) for type initializer");
+}
+
+TEST_F(ParserImplTest, PrimaryExpression_TypeDecl_InvalidValue) {
+  ParserImpl p{"i32(if(a) {})"};
+  auto e = p.primary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:5: unable to parse argument expression");
+}
+
+TEST_F(ParserImplTest, PrimaryExpression_ConstLiteral_True) {
+  ParserImpl p{"true"};
+  auto e = p.primary_expression();
+  ASSERT_FALSE(p.has_error());
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsInitializer());
+  ASSERT_TRUE(e->AsInitializer()->IsConstInitializer());
+  auto init = e->AsInitializer()->AsConstInitializer();
+  ASSERT_TRUE(init->literal()->IsBool());
+  EXPECT_TRUE(init->literal()->AsBool()->IsTrue());
+}
+
+TEST_F(ParserImplTest, PrimaryExpression_ParenExpr) {
+  ParserImpl p{"(a == b)"};
+  auto e = p.primary_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsRelational());
+}
+
+TEST_F(ParserImplTest, PrimaryExpression_ParenExpr_MissingRightParen) {
+  ParserImpl p{"(a == b"};
+  auto e = p.primary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:8: expected )");
+}
+
+TEST_F(ParserImplTest, PrimaryExpression_ParenExpr_MissingExpr) {
+  ParserImpl p{"()"};
+  auto e = p.primary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:2: unable to parse expression");
+}
+
+TEST_F(ParserImplTest, PrimaryExpression_ParenExpr_InvalidExpr) {
+  ParserImpl p{"(if (a) {})"};
+  auto e = p.primary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:2: unable to parse expression");
+}
+
+TEST_F(ParserImplTest, PrimaryExpression_Cast) {
+  auto tm = TypeManager::Instance();
+  auto f32_type = tm->Get(std::make_unique<ast::type::F32Type>());
+
+  ParserImpl p{"cast<f32>(1)"};
+  auto e = p.primary_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsCast());
+
+  auto c = e->AsCast();
+  ASSERT_EQ(c->type(), f32_type);
+
+  ASSERT_TRUE(c->expr()->IsInitializer());
+  ASSERT_TRUE(c->expr()->AsInitializer()->IsConstInitializer());
+
+  TypeManager::Destroy();
+}
+
+TEST_F(ParserImplTest, PrimaryExpression_Cast_MissingGreaterThan) {
+  ParserImpl p{"cast<f32(1)"};
+  auto e = p.primary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:9: missing > for cast expression");
+}
+
+TEST_F(ParserImplTest, PrimaryExpression_Cast_MissingType) {
+  ParserImpl p{"cast<>(1)"};
+  auto e = p.primary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:6: missing type for cast expression");
+}
+
+TEST_F(ParserImplTest, PrimaryExpression_Cast_InvalidType) {
+  ParserImpl p{"cast<invalid>(1)"};
+  auto e = p.primary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:6: unknown type alias 'invalid'");
+}
+
+TEST_F(ParserImplTest, PrimaryExpression_Cast_MissingLeftParen) {
+  ParserImpl p{"cast<f32>1)"};
+  auto e = p.primary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:10: expected (");
+}
+
+TEST_F(ParserImplTest, PrimaryExpression_Cast_MissingRightParen) {
+  ParserImpl p{"cast<f32>(1"};
+  auto e = p.primary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:12: expected )");
+}
+
+TEST_F(ParserImplTest, PrimaryExpression_Cast_MissingExpression) {
+  ParserImpl p{"cast<f32>()"};
+  auto e = p.primary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:11: unable to parse expression");
+}
+
+TEST_F(ParserImplTest, PrimaryExpression_Cast_InvalidExpression) {
+  ParserImpl p{"cast<f32>(if (a) {})"};
+  auto e = p.primary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:11: unable to parse expression");
+}
+
+TEST_F(ParserImplTest, PrimaryExpression_As) {
+  auto tm = TypeManager::Instance();
+  auto f32_type = tm->Get(std::make_unique<ast::type::F32Type>());
+
+  ParserImpl p{"as<f32>(1)"};
+  auto e = p.primary_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsAs());
+
+  auto c = e->AsAs();
+  ASSERT_EQ(c->type(), f32_type);
+
+  ASSERT_TRUE(c->expr()->IsInitializer());
+  ASSERT_TRUE(c->expr()->AsInitializer()->IsConstInitializer());
+
+  TypeManager::Destroy();
+}
+
+TEST_F(ParserImplTest, PrimaryExpression_As_MissingGreaterThan) {
+  ParserImpl p{"as<f32(1)"};
+  auto e = p.primary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:7: missing > for as expression");
+}
+
+TEST_F(ParserImplTest, PrimaryExpression_As_MissingType) {
+  ParserImpl p{"as<>(1)"};
+  auto e = p.primary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:4: missing type for as expression");
+}
+
+TEST_F(ParserImplTest, PrimaryExpression_As_InvalidType) {
+  ParserImpl p{"as<invalid>(1)"};
+  auto e = p.primary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:4: unknown type alias 'invalid'");
+}
+
+TEST_F(ParserImplTest, PrimaryExpression_As_MissingLeftParen) {
+  ParserImpl p{"as<f32>1)"};
+  auto e = p.primary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:8: expected (");
+}
+
+TEST_F(ParserImplTest, PrimaryExpression_As_MissingRightParen) {
+  ParserImpl p{"as<f32>(1"};
+  auto e = p.primary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:10: expected )");
+}
+
+TEST_F(ParserImplTest, PrimaryExpression_As_MissingExpression) {
+  ParserImpl p{"as<f32>()"};
+  auto e = p.primary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:9: unable to parse expression");
+}
+
+TEST_F(ParserImplTest, PrimaryExpression_As_InvalidExpression) {
+  ParserImpl p{"as<f32>(if (a) {})"};
+  auto e = p.primary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:9: unable to parse expression");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_regardless_stmt_test.cc b/src/reader/wgsl/parser_impl_regardless_stmt_test.cc
new file mode 100644
index 0000000..ea2f05b
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_regardless_stmt_test.cc
@@ -0,0 +1,62 @@
+// 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 "gtest/gtest.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, RegardlessStmt) {
+  ParserImpl p{"regardless (a) { kill; }"};
+  auto e = p.regardless_stmt();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsRegardless());
+  ASSERT_NE(e->condition(), nullptr);
+  EXPECT_TRUE(e->condition()->IsIdentifier());
+  ASSERT_EQ(e->body().size(), 1);
+  EXPECT_TRUE(e->body()[0]->IsKill());
+}
+
+TEST_F(ParserImplTest, RegardlessStmt_InvalidCondition) {
+  ParserImpl p{"regardless(if(a){}) {}"};
+  auto e = p.regardless_stmt();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:12: unable to parse expression");
+}
+
+TEST_F(ParserImplTest, RegardlessStmt_EmptyCondition) {
+  ParserImpl p{"regardless() {}"};
+  auto e = p.regardless_stmt();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:12: unable to parse expression");
+}
+
+TEST_F(ParserImplTest, RegardlessStmt_InvalidBody) {
+  ParserImpl p{"regardless(a + 2 - 5 == true) { kill }"};
+  auto e = p.regardless_stmt();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:38: missing ;");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_relational_expression_test.cc b/src/reader/wgsl/parser_impl_relational_expression_test.cc
new file mode 100644
index 0000000..8f236b0
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_relational_expression_test.cc
@@ -0,0 +1,141 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/bool_literal.h"
+#include "src/ast/const_initializer_expression.h"
+#include "src/ast/identifier_expression.h"
+#include "src/ast/relational_expression.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, RelationalExpression_Parses_LessThan) {
+  ParserImpl p{"a < true"};
+  auto e = p.relational_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+
+  ASSERT_TRUE(e->IsRelational());
+  auto rel = e->AsRelational();
+  EXPECT_EQ(ast::Relation::kLessThan, rel->relation());
+
+  ASSERT_TRUE(rel->lhs()->IsIdentifier());
+  auto ident = rel->lhs()->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 1);
+  EXPECT_EQ(ident->name()[0], "a");
+
+  ASSERT_TRUE(rel->rhs()->IsInitializer());
+  ASSERT_TRUE(rel->rhs()->AsInitializer()->IsConstInitializer());
+  auto init = rel->rhs()->AsInitializer()->AsConstInitializer();
+  ASSERT_TRUE(init->literal()->IsBool());
+  ASSERT_TRUE(init->literal()->AsBool()->IsTrue());
+}
+
+TEST_F(ParserImplTest, RelationalExpression_Parses_GreaterThan) {
+  ParserImpl p{"a > true"};
+  auto e = p.relational_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+
+  ASSERT_TRUE(e->IsRelational());
+  auto rel = e->AsRelational();
+  EXPECT_EQ(ast::Relation::kGreaterThan, rel->relation());
+
+  ASSERT_TRUE(rel->lhs()->IsIdentifier());
+  auto ident = rel->lhs()->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 1);
+  EXPECT_EQ(ident->name()[0], "a");
+
+  ASSERT_TRUE(rel->rhs()->IsInitializer());
+  ASSERT_TRUE(rel->rhs()->AsInitializer()->IsConstInitializer());
+  auto init = rel->rhs()->AsInitializer()->AsConstInitializer();
+  ASSERT_TRUE(init->literal()->IsBool());
+  ASSERT_TRUE(init->literal()->AsBool()->IsTrue());
+}
+
+TEST_F(ParserImplTest, RelationalExpression_Parses_LessThanEqual) {
+  ParserImpl p{"a <= true"};
+  auto e = p.relational_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+
+  ASSERT_TRUE(e->IsRelational());
+  auto rel = e->AsRelational();
+  EXPECT_EQ(ast::Relation::kLessThanEqual, rel->relation());
+
+  ASSERT_TRUE(rel->lhs()->IsIdentifier());
+  auto ident = rel->lhs()->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 1);
+  EXPECT_EQ(ident->name()[0], "a");
+
+  ASSERT_TRUE(rel->rhs()->IsInitializer());
+  ASSERT_TRUE(rel->rhs()->AsInitializer()->IsConstInitializer());
+  auto init = rel->rhs()->AsInitializer()->AsConstInitializer();
+  ASSERT_TRUE(init->literal()->IsBool());
+  ASSERT_TRUE(init->literal()->AsBool()->IsTrue());
+}
+
+TEST_F(ParserImplTest, RelationalExpression_Parses_GreaterThanEqual) {
+  ParserImpl p{"a >= true"};
+  auto e = p.relational_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+
+  ASSERT_TRUE(e->IsRelational());
+  auto rel = e->AsRelational();
+  EXPECT_EQ(ast::Relation::kGreaterThanEqual, rel->relation());
+
+  ASSERT_TRUE(rel->lhs()->IsIdentifier());
+  auto ident = rel->lhs()->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 1);
+  EXPECT_EQ(ident->name()[0], "a");
+
+  ASSERT_TRUE(rel->rhs()->IsInitializer());
+  ASSERT_TRUE(rel->rhs()->AsInitializer()->IsConstInitializer());
+  auto init = rel->rhs()->AsInitializer()->AsConstInitializer();
+  ASSERT_TRUE(init->literal()->IsBool());
+  ASSERT_TRUE(init->literal()->AsBool()->IsTrue());
+}
+
+TEST_F(ParserImplTest, RelationalExpression_InvalidLHS) {
+  ParserImpl p{"if (a) {} < true"};
+  auto e = p.relational_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_EQ(e, nullptr);
+}
+
+TEST_F(ParserImplTest, RelationalExpression_InvalidRHS) {
+  ParserImpl p{"true < if (a) {}"};
+  auto e = p.relational_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:8: unable to parse right side of < expression");
+}
+
+TEST_F(ParserImplTest, RelationalExpression_NoOr_ReturnsLHS) {
+  ParserImpl p{"a true"};
+  auto e = p.relational_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsIdentifier());
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_shift_expression_test.cc b/src/reader/wgsl/parser_impl_shift_expression_test.cc
new file mode 100644
index 0000000..21fe620
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_shift_expression_test.cc
@@ -0,0 +1,119 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/bool_literal.h"
+#include "src/ast/const_initializer_expression.h"
+#include "src/ast/identifier_expression.h"
+#include "src/ast/relational_expression.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, ShiftExpression_Parses_ShiftLeft) {
+  ParserImpl p{"a << true"};
+  auto e = p.shift_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+
+  ASSERT_TRUE(e->IsRelational());
+  auto rel = e->AsRelational();
+  EXPECT_EQ(ast::Relation::kShiftLeft, rel->relation());
+
+  ASSERT_TRUE(rel->lhs()->IsIdentifier());
+  auto ident = rel->lhs()->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 1);
+  EXPECT_EQ(ident->name()[0], "a");
+
+  ASSERT_TRUE(rel->rhs()->IsInitializer());
+  ASSERT_TRUE(rel->rhs()->AsInitializer()->IsConstInitializer());
+  auto init = rel->rhs()->AsInitializer()->AsConstInitializer();
+  ASSERT_TRUE(init->literal()->IsBool());
+  ASSERT_TRUE(init->literal()->AsBool()->IsTrue());
+}
+
+TEST_F(ParserImplTest, ShiftExpression_Parses_ShiftRight) {
+  ParserImpl p{"a >> true"};
+  auto e = p.shift_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+
+  ASSERT_TRUE(e->IsRelational());
+  auto rel = e->AsRelational();
+  EXPECT_EQ(ast::Relation::kShiftRight, rel->relation());
+
+  ASSERT_TRUE(rel->lhs()->IsIdentifier());
+  auto ident = rel->lhs()->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 1);
+  EXPECT_EQ(ident->name()[0], "a");
+
+  ASSERT_TRUE(rel->rhs()->IsInitializer());
+  ASSERT_TRUE(rel->rhs()->AsInitializer()->IsConstInitializer());
+  auto init = rel->rhs()->AsInitializer()->AsConstInitializer();
+  ASSERT_TRUE(init->literal()->IsBool());
+  ASSERT_TRUE(init->literal()->AsBool()->IsTrue());
+}
+
+TEST_F(ParserImplTest, ShiftExpression_Parses_ShiftRightArith) {
+  ParserImpl p{"a >>> true"};
+  auto e = p.shift_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+
+  ASSERT_TRUE(e->IsRelational());
+  auto rel = e->AsRelational();
+  EXPECT_EQ(ast::Relation::kShiftRightArith, rel->relation());
+
+  ASSERT_TRUE(rel->lhs()->IsIdentifier());
+  auto ident = rel->lhs()->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 1);
+  EXPECT_EQ(ident->name()[0], "a");
+
+  ASSERT_TRUE(rel->rhs()->IsInitializer());
+  ASSERT_TRUE(rel->rhs()->AsInitializer()->IsConstInitializer());
+  auto init = rel->rhs()->AsInitializer()->AsConstInitializer();
+  ASSERT_TRUE(init->literal()->IsBool());
+  ASSERT_TRUE(init->literal()->AsBool()->IsTrue());
+}
+
+TEST_F(ParserImplTest, ShiftExpression_InvalidLHS) {
+  ParserImpl p{"if (a) {} << true"};
+  auto e = p.shift_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_EQ(e, nullptr);
+}
+
+TEST_F(ParserImplTest, ShiftExpression_InvalidRHS) {
+  ParserImpl p{"true << if (a) {}"};
+  auto e = p.shift_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:9: unable to parse right side of << expression");
+}
+
+TEST_F(ParserImplTest, ShiftExpression_NoOr_ReturnsLHS) {
+  ParserImpl p{"a true"};
+  auto e = p.shift_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsIdentifier());
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_statement_test.cc b/src/reader/wgsl/parser_impl_statement_test.cc
new file mode 100644
index 0000000..005eb82
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_statement_test.cc
@@ -0,0 +1,290 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/return_statement.h"
+#include "src/ast/statement.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, Statement) {
+  ParserImpl p{"return;"};
+  auto e = p.statement();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  EXPECT_TRUE(e->IsReturn());
+}
+
+TEST_F(ParserImplTest, Statement_Semicolon) {
+  ParserImpl p{";"};
+  auto e = p.statement();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_EQ(e, nullptr);
+}
+
+TEST_F(ParserImplTest, Statement_Return_NoValue) {
+  ParserImpl p{"return;"};
+  auto e = p.statement();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+
+  ASSERT_TRUE(e->IsReturn());
+  auto ret = e->AsReturn();
+  ASSERT_EQ(ret->value(), nullptr);
+}
+
+TEST_F(ParserImplTest, Statement_Return_Value) {
+  ParserImpl p{"return a + b * (.1 - .2);"};
+  auto e = p.statement();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+
+  ASSERT_TRUE(e->IsReturn());
+  auto ret = e->AsReturn();
+  ASSERT_NE(ret->value(), nullptr);
+  EXPECT_TRUE(ret->value()->IsRelational());
+}
+
+TEST_F(ParserImplTest, Statement_Return_MissingSemi) {
+  ParserImpl p{"return"};
+  auto e = p.statement();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:7: missing ;");
+}
+
+TEST_F(ParserImplTest, Statement_Return_Invalid) {
+  ParserImpl p{"return if(a) {};"};
+  auto e = p.statement();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:8: missing ;");
+}
+
+TEST_F(ParserImplTest, Statement_If) {
+  ParserImpl p{"if (a) {}"};
+  auto e = p.statement();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsIf());
+}
+
+TEST_F(ParserImplTest, Statement_If_Invalid) {
+  ParserImpl p{"if (a) { fn main() -> {}}"};
+  auto e = p.statement();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:10: missing }");
+}
+
+TEST_F(ParserImplTest, Statement_Unless) {
+  ParserImpl p{"unless (a) {}"};
+  auto e = p.statement();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsUnless());
+}
+
+TEST_F(ParserImplTest, Statement_Unless_Invalid) {
+  ParserImpl p{"unless () {}"};
+  auto e = p.statement();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:9: unable to parse expression");
+}
+
+TEST_F(ParserImplTest, Statement_Regardless) {
+  ParserImpl p{"regardless (a) {}"};
+  auto e = p.statement();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsRegardless());
+}
+
+TEST_F(ParserImplTest, Statement_Regardless_Invalid) {
+  ParserImpl p{"regardless () {}"};
+  auto e = p.statement();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:13: unable to parse expression");
+}
+
+TEST_F(ParserImplTest, Statement_Variable) {
+  ParserImpl p{"var a : i32 = 1;"};
+  auto e = p.statement();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsVariable());
+}
+
+TEST_F(ParserImplTest, Statement_Variable_Invalid) {
+  ParserImpl p{"var a : i32 =;"};
+  auto e = p.statement();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:14: missing initializer for variable declaration");
+}
+
+TEST_F(ParserImplTest, Statement_Variable_MissingSemicolon) {
+  ParserImpl p{"var a : i32"};
+  auto e = p.statement();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:12: missing ;");
+}
+
+TEST_F(ParserImplTest, Statement_Switch) {
+  ParserImpl p{"switch (a) {}"};
+  auto e = p.statement();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsSwitch());
+}
+
+TEST_F(ParserImplTest, Statement_Switch_Invalid) {
+  ParserImpl p{"switch (a) { case: {}}"};
+  auto e = p.statement();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:18: unable to parse case conditional");
+}
+
+TEST_F(ParserImplTest, Statement_Loop) {
+  ParserImpl p{"loop {}"};
+  auto e = p.statement();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsLoop());
+}
+
+TEST_F(ParserImplTest, Statement_Loop_Invalid) {
+  ParserImpl p{"loop kill; }"};
+  auto e = p.statement();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:6: missing { for loop");
+}
+
+TEST_F(ParserImplTest, Statement_Assignment) {
+  ParserImpl p{"a = b;"};
+  auto e = p.statement();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  EXPECT_TRUE(e->IsAssign());
+}
+
+TEST_F(ParserImplTest, Statement_Assignment_Invalid) {
+  ParserImpl p{"a = if(b) {};"};
+  auto e = p.statement();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:5: unable to parse right side of assignment");
+}
+
+TEST_F(ParserImplTest, Statement_Assignment_MissingSemicolon) {
+  ParserImpl p{"a = b"};
+  auto e = p.statement();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:6: missing ;");
+}
+
+TEST_F(ParserImplTest, Statement_Break) {
+  ParserImpl p{"break;"};
+  auto e = p.statement();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  EXPECT_TRUE(e->IsBreak());
+}
+
+TEST_F(ParserImplTest, Statement_Break_Invalid) {
+  ParserImpl p{"break if (a = b);"};
+  auto e = p.statement();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:13: expected )");
+}
+
+TEST_F(ParserImplTest, Statement_Break_MissingSemicolon) {
+  ParserImpl p{"break if (a == b)"};
+  auto e = p.statement();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:18: missing ;");
+}
+
+TEST_F(ParserImplTest, Statement_Continue) {
+  ParserImpl p{"continue;"};
+  auto e = p.statement();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  EXPECT_TRUE(e->IsContinue());
+}
+
+TEST_F(ParserImplTest, Statement_Continue_Invalid) {
+  ParserImpl p{"continue if (a = b);"};
+  auto e = p.statement();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:16: expected )");
+}
+
+TEST_F(ParserImplTest, Statement_Continue_MissingSemicolon) {
+  ParserImpl p{"continue if (a == b)"};
+  auto e = p.statement();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:21: missing ;");
+}
+
+TEST_F(ParserImplTest, Statement_Kill) {
+  ParserImpl p{"kill;"};
+  auto e = p.statement();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  EXPECT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsKill());
+}
+
+TEST_F(ParserImplTest, Statement_Kill_MissingSemicolon) {
+  ParserImpl p{"kill"};
+  auto e = p.statement();
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:5: missing ;");
+}
+
+TEST_F(ParserImplTest, Statement_Nop) {
+  ParserImpl p{"nop;"};
+  auto e = p.statement();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  EXPECT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsNop());
+}
+
+TEST_F(ParserImplTest, Statement_Nop_MissingSemicolon) {
+  ParserImpl p{"nop"};
+  auto e = p.statement();
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:4: missing ;");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_statements_test.cc b/src/reader/wgsl/parser_impl_statements_test.cc
new file mode 100644
index 0000000..f993794
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_statements_test.cc
@@ -0,0 +1,44 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/statement.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, Statements) {
+  ParserImpl p{"nop; kill; return;"};
+  auto e = p.statements();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_EQ(e.size(), 3);
+  EXPECT_TRUE(e[0]->IsNop());
+  EXPECT_TRUE(e[1]->IsKill());
+  EXPECT_TRUE(e[2]->IsReturn());
+}
+
+TEST_F(ParserImplTest, Statements_Empty) {
+  ParserImpl p{""};
+  auto e = p.statements();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_EQ(e.size(), 0);
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_storage_class_test.cc b/src/reader/wgsl/parser_impl_storage_class_test.cc
new file mode 100644
index 0000000..f479d00
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_storage_class_test.cc
@@ -0,0 +1,73 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/storage_class.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+struct StorageClassData {
+  const char* input;
+  ast::StorageClass result;
+};
+inline std::ostream& operator<<(std::ostream& out, StorageClassData data) {
+  out << std::string(data.input);
+  return out;
+}
+using StorageClassTest = testing::TestWithParam<StorageClassData>;
+TEST_P(StorageClassTest, Parses) {
+  auto params = GetParam();
+  ParserImpl p{params.input};
+
+  auto sc = p.storage_class();
+  ASSERT_FALSE(p.has_error());
+  EXPECT_EQ(sc, params.result);
+
+  auto t = p.next();
+  EXPECT_TRUE(t.IsEof());
+}
+INSTANTIATE_TEST_SUITE_P(
+    ParserImplTest,
+    StorageClassTest,
+    testing::Values(
+        StorageClassData{"in", ast::StorageClass::kInput},
+        StorageClassData{"out", ast::StorageClass::kOutput},
+        StorageClassData{"uniform", ast::StorageClass::kUniform},
+        StorageClassData{"workgroup", ast::StorageClass::kWorkgroup},
+        StorageClassData{"uniform_constant",
+                         ast::StorageClass::kUniformConstant},
+        StorageClassData{"storage_buffer", ast::StorageClass::kStorageBuffer},
+        StorageClassData{"image", ast::StorageClass::kImage},
+        StorageClassData{"push_constant", ast::StorageClass::kPushConstant},
+        StorageClassData{"private", ast::StorageClass::kPrivate},
+        StorageClassData{"function", ast::StorageClass::kFunction}));
+
+TEST_F(ParserImplTest, StorageClass_NoMatch) {
+  ParserImpl p{"not-a-storage-class"};
+  auto sc = p.storage_class();
+  ASSERT_EQ(sc, ast::StorageClass::kNone);
+
+  auto t = p.next();
+  EXPECT_TRUE(t.IsIdentifier());
+  EXPECT_EQ(t.to_str(), "not");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_struct_body_decl_test.cc b/src/reader/wgsl/parser_impl_struct_body_decl_test.cc
new file mode 100644
index 0000000..62c4f57
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_struct_body_decl_test.cc
@@ -0,0 +1,80 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/type/i32_type.h"
+#include "src/reader/wgsl/parser_impl.h"
+#include "src/type_manager.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, StructBodyDecl_Parses) {
+  auto i32 =
+      TypeManager::Instance()->Get(std::make_unique<ast::type::I32Type>());
+
+  ParserImpl p{"{a : i32;}"};
+  auto m = p.struct_body_decl();
+  ASSERT_FALSE(p.has_error());
+  ASSERT_EQ(m.size(), 1);
+
+  const auto& mem = m[0];
+  EXPECT_EQ(mem->name(), "a");
+  EXPECT_EQ(mem->type(), i32);
+  EXPECT_EQ(mem->decorations().size(), 0);
+
+  TypeManager::Destroy();
+}
+
+TEST_F(ParserImplTest, StructBodyDecl_ParsesEmpty) {
+  ParserImpl p{"{}"};
+  auto m = p.struct_body_decl();
+  ASSERT_FALSE(p.has_error());
+  ASSERT_EQ(m.size(), 0);
+}
+
+TEST_F(ParserImplTest, StructBodyDecl_InvalidMember) {
+  ParserImpl p{R"(
+{
+  [[offset nan]] a : i32;
+})"};
+  auto m = p.struct_body_decl();
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(p.error(), "3:12: invalid value for offset decoration");
+}
+
+TEST_F(ParserImplTest, StructBodyDecl_MissingClosingBracket) {
+  ParserImpl p{"{a : i32;"};
+  auto m = p.struct_body_decl();
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(p.error(), "1:10: missing } for struct declaration");
+}
+
+TEST_F(ParserImplTest, StructBodyDecl_InvalidToken) {
+  ParserImpl p{R"(
+{
+  a : i32;
+  1.23
+} )"};
+  auto m = p.struct_body_decl();
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(p.error(), "4:3: invalid identifier declaration");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_struct_decl_test.cc b/src/reader/wgsl/parser_impl_struct_decl_test.cc
new file mode 100644
index 0000000..87e98cb
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_struct_decl_test.cc
@@ -0,0 +1,95 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/type/struct_type.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, StructDecl_Parses) {
+  ParserImpl p{R"(
+struct {
+  a : i32;
+  [[offset 4 ]] b : f32;
+})"};
+  auto s = p.struct_decl();
+  ASSERT_FALSE(p.has_error());
+  ASSERT_NE(s, nullptr);
+  ASSERT_EQ(s->impl()->members().size(), 2);
+  EXPECT_EQ(s->impl()->members()[0]->name(), "a");
+  EXPECT_EQ(s->impl()->members()[1]->name(), "b");
+}
+
+TEST_F(ParserImplTest, StructDecl_ParsesWithDecoration) {
+  ParserImpl p{R"(
+[[block]] struct {
+  a : f32;
+  b : f32;
+})"};
+  auto s = p.struct_decl();
+  ASSERT_FALSE(p.has_error());
+  ASSERT_NE(s, nullptr);
+  ASSERT_EQ(s->impl()->members().size(), 2);
+  EXPECT_EQ(s->impl()->members()[0]->name(), "a");
+  EXPECT_EQ(s->impl()->members()[1]->name(), "b");
+}
+
+TEST_F(ParserImplTest, StructDecl_EmptyMembers) {
+  ParserImpl p{"struct {}"};
+  auto s = p.struct_decl();
+  ASSERT_FALSE(p.has_error());
+  ASSERT_NE(s, nullptr);
+  ASSERT_EQ(s->impl()->members().size(), 0);
+}
+
+TEST_F(ParserImplTest, StructDecl_MissingBracketLeft) {
+  ParserImpl p{"struct }"};
+  auto s = p.struct_decl();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(s, nullptr);
+  EXPECT_EQ(p.error(), "1:8: missing { for struct declaration");
+}
+
+TEST_F(ParserImplTest, StructDecl_InvalidStructBody) {
+  ParserImpl p{"struct { a : B; }"};
+  auto s = p.struct_decl();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(s, nullptr);
+  EXPECT_EQ(p.error(), "1:14: unknown type alias 'B'");
+}
+
+TEST_F(ParserImplTest, StructDecl_InvalidStructDecorationDecl) {
+  ParserImpl p{"[[block struct { a : i32; }"};
+  auto s = p.struct_decl();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(s, nullptr);
+  EXPECT_EQ(p.error(), "1:9: missing ]] for struct decoration");
+}
+
+TEST_F(ParserImplTest, StructDecl_MissingStruct) {
+  ParserImpl p{"[[block]] {}"};
+  auto s = p.struct_decl();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(s, nullptr);
+  EXPECT_EQ(p.error(), "1:11: missing struct declaration");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_struct_decoration_decl_test.cc b/src/reader/wgsl/parser_impl_struct_decoration_decl_test.cc
new file mode 100644
index 0000000..d6e1cc4
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_struct_decoration_decl_test.cc
@@ -0,0 +1,47 @@
+// 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 "gtest/gtest.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, StructDecorationDecl_Parses) {
+  ParserImpl p{"[[block]]"};
+  auto d = p.struct_decoration_decl();
+  ASSERT_FALSE(p.has_error());
+  EXPECT_EQ(d, ast::StructDecoration::kBlock);
+}
+
+TEST_F(ParserImplTest, StructDecorationDecl_MissingAttrRight) {
+  ParserImpl p{"[[block"};
+  p.struct_decoration_decl();
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(p.error(), "1:8: missing ]] for struct decoration");
+}
+
+TEST_F(ParserImplTest, StructDecorationDecl_InvalidDecoration) {
+  ParserImpl p{"[[invalid]]"};
+  p.struct_decoration_decl();
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(p.error(), "1:3: unknown struct decoration");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_struct_decoration_test.cc b/src/reader/wgsl/parser_impl_struct_decoration_test.cc
new file mode 100644
index 0000000..3f5aec3
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_struct_decoration_test.cc
@@ -0,0 +1,62 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/struct_decoration.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+struct StructDecorationData {
+  const char* input;
+  ast::StructDecoration result;
+};
+inline std::ostream& operator<<(std::ostream& out, StructDecorationData data) {
+  out << std::string(data.input);
+  return out;
+}
+using StructDecorationTest = testing::TestWithParam<StructDecorationData>;
+TEST_P(StructDecorationTest, Parses) {
+  auto params = GetParam();
+  ParserImpl p{params.input};
+
+  auto deco = p.struct_decoration();
+  ASSERT_FALSE(p.has_error());
+  EXPECT_EQ(deco, params.result);
+
+  auto t = p.next();
+  EXPECT_TRUE(t.IsEof());
+}
+INSTANTIATE_TEST_SUITE_P(ParserImplTest,
+                         StructDecorationTest,
+                         testing::Values(StructDecorationData{
+                             "block", ast::StructDecoration::kBlock}));
+
+TEST_F(ParserImplTest, StructDecoration_NoMatch) {
+  ParserImpl p{"not-a-stage"};
+  auto deco = p.struct_decoration();
+  ASSERT_EQ(deco, ast::StructDecoration::kNone);
+
+  auto t = p.next();
+  EXPECT_TRUE(t.IsIdentifier());
+  EXPECT_EQ(t.to_str(), "not");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_struct_member_decoration_decl_test.cc b/src/reader/wgsl/parser_impl_struct_member_decoration_decl_test.cc
new file mode 100644
index 0000000..3e8069d
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_struct_member_decoration_decl_test.cc
@@ -0,0 +1,70 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/struct_member_offset_decoration.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, StructMemberDecorationDecl_EmptyStr) {
+  ParserImpl p{""};
+  auto deco = p.struct_member_decoration_decl();
+  ASSERT_FALSE(p.has_error());
+  EXPECT_EQ(deco.size(), 0);
+}
+
+TEST_F(ParserImplTest, StructMemberDecorationDecl_EmptyBlock) {
+  ParserImpl p{"[[]]"};
+  auto deco = p.struct_member_decoration_decl();
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(p.error(), "1:3: empty struct member decoration found");
+}
+
+TEST_F(ParserImplTest, StructMemberDecorationDecl_Single) {
+  ParserImpl p{"[[offset 4]]"};
+  auto deco = p.struct_member_decoration_decl();
+  ASSERT_FALSE(p.has_error());
+  ASSERT_EQ(deco.size(), 1);
+  EXPECT_TRUE(deco[0]->IsOffset());
+}
+
+TEST_F(ParserImplTest, StructMemberDecorationDecl_HandlesDuplicate) {
+  ParserImpl p{"[[offset 2, offset 4]]"};
+  auto deco = p.struct_member_decoration_decl();
+  ASSERT_TRUE(p.has_error()) << p.error();
+  EXPECT_EQ(p.error(), "1:21: duplicate offset decoration found");
+}
+
+TEST_F(ParserImplTest, StructMemberDecorationDecl_InvalidDecoration) {
+  ParserImpl p{"[[offset nan]]"};
+  auto deco = p.struct_member_decoration_decl();
+  ASSERT_TRUE(p.has_error()) << p.error();
+  EXPECT_EQ(p.error(), "1:10: invalid value for offset decoration");
+}
+
+TEST_F(ParserImplTest, StructMemberDecorationDecl_MissingClose) {
+  ParserImpl p{"[[offset 4"};
+  auto deco = p.struct_member_decoration_decl();
+  ASSERT_TRUE(p.has_error()) << p.error();
+  EXPECT_EQ(p.error(), "1:11: missing ]] for struct member decoration");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_struct_member_decoration_test.cc b/src/reader/wgsl/parser_impl_struct_member_decoration_test.cc
new file mode 100644
index 0000000..14a760c
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_struct_member_decoration_test.cc
@@ -0,0 +1,54 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/struct_member_offset_decoration.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, StructMemberDecoration_Offset) {
+  ParserImpl p{"offset 4"};
+  auto deco = p.struct_member_decoration();
+  ASSERT_NE(deco, nullptr);
+  ASSERT_FALSE(p.has_error());
+  ASSERT_TRUE(deco->IsOffset());
+
+  auto o = deco->AsOffset();
+  EXPECT_EQ(o->offset(), 4);
+}
+
+TEST_F(ParserImplTest, StructMemberDecoration_Offset_MissingValue) {
+  ParserImpl p{"offset"};
+  auto deco = p.struct_member_decoration();
+  ASSERT_EQ(deco, nullptr);
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(p.error(), "1:7: invalid value for offset decoration");
+}
+
+TEST_F(ParserImplTest, StructMemberDecoration_Offset_MissingInvalid) {
+  ParserImpl p{"offset nan"};
+  auto deco = p.struct_member_decoration();
+  ASSERT_EQ(deco, nullptr);
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(p.error(), "1:8: invalid value for offset decoration");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_struct_member_test.cc b/src/reader/wgsl/parser_impl_struct_member_test.cc
new file mode 100644
index 0000000..ee524b3
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_struct_member_test.cc
@@ -0,0 +1,87 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/struct_member_offset_decoration.h"
+#include "src/ast/type/i32_type.h"
+#include "src/reader/wgsl/parser_impl.h"
+#include "src/type_manager.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, StructMember_Parses) {
+  auto i32 =
+      TypeManager::Instance()->Get(std::make_unique<ast::type::I32Type>());
+
+  ParserImpl p{"a : i32;"};
+  auto m = p.struct_member();
+  ASSERT_FALSE(p.has_error());
+  ASSERT_NE(m, nullptr);
+
+  EXPECT_EQ(m->name(), "a");
+  EXPECT_EQ(m->type(), i32);
+  EXPECT_EQ(m->decorations().size(), 0);
+
+  TypeManager::Destroy();
+}
+
+TEST_F(ParserImplTest, StructMember_ParsesWithDecoration) {
+  auto i32 =
+      TypeManager::Instance()->Get(std::make_unique<ast::type::I32Type>());
+
+  ParserImpl p{"[[offset 2]] a : i32;"};
+  auto m = p.struct_member();
+  ASSERT_FALSE(p.has_error());
+  ASSERT_NE(m, nullptr);
+
+  EXPECT_EQ(m->name(), "a");
+  EXPECT_EQ(m->type(), i32);
+  EXPECT_EQ(m->decorations().size(), 1);
+  EXPECT_TRUE(m->decorations()[0]->IsOffset());
+  EXPECT_EQ(m->decorations()[0]->AsOffset()->offset(), 2);
+
+  TypeManager::Destroy();
+}
+
+TEST_F(ParserImplTest, StructMember_InvalidDecoration) {
+  ParserImpl p{"[[offset nan]] a : i32;"};
+  auto m = p.struct_member();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(m, nullptr);
+  EXPECT_EQ(p.error(), "1:10: invalid value for offset decoration");
+}
+
+TEST_F(ParserImplTest, StructMember_InvalidVariable) {
+  ParserImpl p{"[[offset 4]] a : B;"};
+  auto m = p.struct_member();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(m, nullptr);
+  EXPECT_EQ(p.error(), "1:18: unknown type alias 'B'");
+}
+
+TEST_F(ParserImplTest, StructMember_MissingSemicolon) {
+  ParserImpl p{"a : i32"};
+  auto m = p.struct_member();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(m, nullptr);
+  EXPECT_EQ(p.error(), "1:8: missing ; for struct member");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_switch_body_test.cc b/src/reader/wgsl/parser_impl_switch_body_test.cc
new file mode 100644
index 0000000..d155ac7
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_switch_body_test.cc
@@ -0,0 +1,129 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/case_statement.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, SwitchBody_Case) {
+  ParserImpl p{"case 1: { a = 4; }"};
+  auto e = p.switch_body();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsCase());
+  EXPECT_FALSE(e->IsDefault());
+  ASSERT_EQ(e->body().size(), 1);
+  EXPECT_TRUE(e->body()[0]->IsAssign());
+}
+
+TEST_F(ParserImplTest, SwitchBody_Case_InvalidConstLiteral) {
+  ParserImpl p{"case a == 4: { a = 4; }"};
+  auto e = p.switch_body();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:6: unable to parse case conditional");
+}
+
+TEST_F(ParserImplTest, SwitchBody_Case_MissingConstLiteral) {
+  ParserImpl p{"case: { a = 4; }"};
+  auto e = p.switch_body();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:5: unable to parse case conditional");
+}
+
+TEST_F(ParserImplTest, SwitchBody_Case_MissingColon) {
+  ParserImpl p{"case 1 { a = 4; }"};
+  auto e = p.switch_body();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:8: missing : for case statement");
+}
+
+TEST_F(ParserImplTest, SwitchBody_Case_MissingBracketLeft) {
+  ParserImpl p{"case 1: a = 4; }"};
+  auto e = p.switch_body();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:9: missing { for case statement");
+}
+
+TEST_F(ParserImplTest, SwitchBody_Case_MissingBracketRight) {
+  ParserImpl p{"case 1: { a = 4; "};
+  auto e = p.switch_body();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:18: missing } for case statement");
+}
+
+TEST_F(ParserImplTest, SwitchBody_Case_InvalidCaseBody) {
+  ParserImpl p{"case 1: { fn main() -> void {} }"};
+  auto e = p.switch_body();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:11: missing } for case statement");
+}
+
+TEST_F(ParserImplTest, SwitchBody_Default) {
+  ParserImpl p{"default: { a = 4; }"};
+  auto e = p.switch_body();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsCase());
+  EXPECT_TRUE(e->IsDefault());
+  ASSERT_EQ(e->body().size(), 1);
+  EXPECT_TRUE(e->body()[0]->IsAssign());
+}
+
+TEST_F(ParserImplTest, SwitchBody_Default_MissingColon) {
+  ParserImpl p{"default { a = 4; }"};
+  auto e = p.switch_body();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:9: missing : for case statement");
+}
+
+TEST_F(ParserImplTest, SwitchBody_Default_MissingBracketLeft) {
+  ParserImpl p{"default: a = 4; }"};
+  auto e = p.switch_body();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:10: missing { for case statement");
+}
+
+TEST_F(ParserImplTest, SwitchBody_Default_MissingBracketRight) {
+  ParserImpl p{"default: { a = 4; "};
+  auto e = p.switch_body();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:19: missing } for case statement");
+}
+
+TEST_F(ParserImplTest, SwitchBody_Default_InvalidCaseBody) {
+  ParserImpl p{"default: { fn main() -> void {} }"};
+  auto e = p.switch_body();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:12: missing } for case statement");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_switch_stmt_test.cc b/src/reader/wgsl/parser_impl_switch_stmt_test.cc
new file mode 100644
index 0000000..8016ec5
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_switch_stmt_test.cc
@@ -0,0 +1,110 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/case_statement.h"
+#include "src/ast/switch_statement.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, SwitchStmt_WithoutDefault) {
+  ParserImpl p{R"(switch(a) {
+  case 1: {}
+  case 2: {}
+})"};
+  auto e = p.switch_stmt();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsSwitch());
+  ASSERT_EQ(e->body().size(), 2);
+  EXPECT_FALSE(e->body()[0]->IsDefault());
+  EXPECT_FALSE(e->body()[1]->IsDefault());
+}
+
+TEST_F(ParserImplTest, SwitchStmt_Empty) {
+  ParserImpl p{"switch(a) { }"};
+  auto e = p.switch_stmt();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsSwitch());
+  ASSERT_EQ(e->body().size(), 0);
+}
+
+TEST_F(ParserImplTest, SwitchStmt_DefaultInMiddle) {
+  ParserImpl p{R"(switch(a) {
+  case 1: {}
+  default: {}
+  case 2: {}
+})"};
+  auto e = p.switch_stmt();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsSwitch());
+
+  ASSERT_EQ(e->body().size(), 3);
+  ASSERT_FALSE(e->body()[0]->IsDefault());
+  ASSERT_TRUE(e->body()[1]->IsDefault());
+  ASSERT_FALSE(e->body()[2]->IsDefault());
+}
+
+TEST_F(ParserImplTest, SwitchStmt_InvalidExpression) {
+  ParserImpl p{"switch(a=b) {}"};
+  auto e = p.switch_stmt();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:9: expected )");
+}
+
+TEST_F(ParserImplTest, SwitchStmt_MissingExpression) {
+  ParserImpl p{"switch {}"};
+  auto e = p.switch_stmt();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:8: expected (");
+}
+
+TEST_F(ParserImplTest, SwitchStmt_MissingBracketLeft) {
+  ParserImpl p{"switch(a) }"};
+  auto e = p.switch_stmt();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:11: missing { for switch statement");
+}
+
+TEST_F(ParserImplTest, SwitchStmt_MissingBracketRight) {
+  ParserImpl p{"switch(a) {"};
+  auto e = p.switch_stmt();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:12: missing } for switch statement");
+}
+
+TEST_F(ParserImplTest, SwitchStmt_InvalidBody) {
+  ParserImpl p{R"(switch(a) {
+  case: {}
+})"};
+  auto e = p.switch_stmt();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "2:7: unable to parse case conditional");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_test.cc b/src/reader/wgsl/parser_impl_test.cc
new file mode 100644
index 0000000..d59d5d4
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_test.cc
@@ -0,0 +1,80 @@
+// 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/reader/wgsl/parser_impl.h"
+
+#include "gtest/gtest.h"
+#include "src/ast/type/i32_type.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, Empty) {
+  ParserImpl p{""};
+  ASSERT_TRUE(p.Parse()) << p.error();
+}
+
+TEST_F(ParserImplTest, DISABLED_Parses) {
+  ParserImpl p{R"(
+import "GLSL.std.430" as glsl;
+
+[[location 0]] var<out> gl_FragColor : vec4<f32>;
+
+fn main() -> void {
+  gl_FragColor = vec4<f32>(.4, .2, .3, 1);
+}
+)"};
+  ASSERT_TRUE(p.Parse()) << p.error();
+
+  auto m = p.module();
+  ASSERT_EQ(1, m.imports().size());
+
+  // TODO(dsinclair) check rest of AST ...
+}
+
+TEST_F(ParserImplTest, DISABLED_HandlesError) {
+  ParserImpl p{R"(
+import "GLSL.std.430" as glsl;
+
+fn main() ->  {  # missing return type
+  return;
+})"};
+
+  ASSERT_FALSE(p.Parse());
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(p.error(), "4:15: missing return type for function");
+}
+
+TEST_F(ParserImplTest, GetRegisteredType) {
+  ParserImpl p{""};
+  ast::type::I32Type i32;
+  p.register_alias("my_alias", &i32);
+
+  auto alias = p.get_alias("my_alias");
+  ASSERT_NE(alias, nullptr);
+  ASSERT_EQ(alias, &i32);
+}
+
+TEST_F(ParserImplTest, GetUnregisteredType) {
+  ParserImpl p{""};
+  auto alias = p.get_alias("my_alias");
+  ASSERT_EQ(alias, nullptr);
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_type_alias_test.cc b/src/reader/wgsl/parser_impl_type_alias_test.cc
new file mode 100644
index 0000000..b4f814c
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_type_alias_test.cc
@@ -0,0 +1,96 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/type/alias_type.h"
+#include "src/ast/type/i32_type.h"
+#include "src/ast/type/struct_type.h"
+#include "src/reader/wgsl/parser_impl.h"
+#include "src/type_manager.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, TypeDecl_ParsesType) {
+  auto tm = TypeManager::Instance();
+  auto i32 = tm->Get(std::make_unique<ast::type::I32Type>());
+
+  ParserImpl p{"type a = i32"};
+  auto t = p.type_alias();
+  ASSERT_FALSE(p.has_error());
+  ASSERT_NE(t, nullptr);
+  ASSERT_TRUE(t->type()->IsI32());
+  ASSERT_EQ(t->type(), i32);
+
+  TypeManager::Destroy();
+}
+
+TEST_F(ParserImplTest, TypeDecl_ParsesStruct) {
+  ParserImpl p{"type a = struct { b: i32; c: f32;}"};
+  auto t = p.type_alias();
+  ASSERT_FALSE(p.has_error());
+  ASSERT_NE(t, nullptr);
+  EXPECT_EQ(t->name(), "a");
+  ASSERT_TRUE(t->type()->IsStruct());
+
+  auto s = t->type()->AsStruct();
+  EXPECT_EQ(s->impl()->members().size(), 2);
+}
+
+TEST_F(ParserImplTest, TypeDecl_MissingIdent) {
+  ParserImpl p{"type = i32"};
+  auto t = p.type_alias();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(t, nullptr);
+  EXPECT_EQ(p.error(), "1:6: missing identifier for type alias");
+}
+
+TEST_F(ParserImplTest, TypeDecl_InvalidIdent) {
+  ParserImpl p{"type 123 = i32"};
+  auto t = p.type_alias();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(t, nullptr);
+  EXPECT_EQ(p.error(), "1:6: missing identifier for type alias");
+}
+
+TEST_F(ParserImplTest, TypeDecl_MissingEqual) {
+  ParserImpl p{"type a i32"};
+  auto t = p.type_alias();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(t, nullptr);
+  EXPECT_EQ(p.error(), "1:8: missing = for type alias");
+}
+
+TEST_F(ParserImplTest, TypeDecl_InvalidType) {
+  ParserImpl p{"type a = B"};
+  auto t = p.type_alias();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(t, nullptr);
+  EXPECT_EQ(p.error(), "1:10: unknown type alias 'B'");
+}
+
+TEST_F(ParserImplTest, TypeDecl_InvalidStruct) {
+  ParserImpl p{"type a = [[block]] {}"};
+  auto t = p.type_alias();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(t, nullptr);
+  EXPECT_EQ(p.error(), "1:20: missing struct declaration");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_type_decl_test.cc b/src/reader/wgsl/parser_impl_type_decl_test.cc
new file mode 100644
index 0000000..fcf07d7
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_type_decl_test.cc
@@ -0,0 +1,506 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/type/alias_type.h"
+#include "src/ast/type/array_type.h"
+#include "src/ast/type/bool_type.h"
+#include "src/ast/type/f32_type.h"
+#include "src/ast/type/i32_type.h"
+#include "src/ast/type/matrix_type.h"
+#include "src/ast/type/pointer_type.h"
+#include "src/ast/type/struct_type.h"
+#include "src/ast/type/u32_type.h"
+#include "src/ast/type/vector_type.h"
+#include "src/reader/wgsl/parser_impl.h"
+#include "src/type_manager.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, TypeDecl_Invalid) {
+  ParserImpl p{"1234"};
+  auto t = p.type_decl();
+  EXPECT_EQ(t, nullptr);
+  EXPECT_FALSE(p.has_error());
+}
+
+TEST_F(ParserImplTest, TypeDecl_Identifier) {
+  ParserImpl p{"A"};
+
+  auto tm = TypeManager::Instance();
+  auto int_type = tm->Get(std::make_unique<ast::type::I32Type>());
+  // Pre-register to make sure that it's the same type.
+  auto alias_type =
+      tm->Get(std::make_unique<ast::type::AliasType>("A", int_type));
+
+  p.register_alias("A", alias_type);
+
+  auto t = p.type_decl();
+  ASSERT_NE(t, nullptr);
+  EXPECT_EQ(t, alias_type);
+  ASSERT_TRUE(t->IsAlias());
+
+  auto alias = t->AsAlias();
+  EXPECT_EQ(alias->name(), "A");
+  EXPECT_EQ(alias->type(), int_type);
+
+  TypeManager::Destroy();
+}
+
+TEST_F(ParserImplTest, TypeDecl_Identifier_NotFound) {
+  ParserImpl p{"B"};
+
+  auto t = p.type_decl();
+  ASSERT_EQ(t, nullptr);
+  EXPECT_TRUE(p.has_error());
+  EXPECT_EQ(p.error(), "1:1: unknown type alias 'B'");
+}
+
+TEST_F(ParserImplTest, TypeDecl_Bool) {
+  ParserImpl p{"bool"};
+
+  auto tm = TypeManager::Instance();
+  auto bool_type = tm->Get(std::make_unique<ast::type::BoolType>());
+
+  auto t = p.type_decl();
+  ASSERT_NE(t, nullptr);
+  EXPECT_EQ(t, bool_type);
+  ASSERT_TRUE(t->IsBool());
+
+  TypeManager::Destroy();
+}
+
+TEST_F(ParserImplTest, TypeDecl_F32) {
+  ParserImpl p{"f32"};
+
+  auto tm = TypeManager::Instance();
+  auto float_type = tm->Get(std::make_unique<ast::type::F32Type>());
+
+  auto t = p.type_decl();
+  ASSERT_NE(t, nullptr);
+  EXPECT_EQ(t, float_type);
+  ASSERT_TRUE(t->IsF32());
+
+  TypeManager::Destroy();
+}
+
+TEST_F(ParserImplTest, TypeDecl_I32) {
+  ParserImpl p{"i32"};
+
+  auto tm = TypeManager::Instance();
+  auto int_type = tm->Get(std::make_unique<ast::type::I32Type>());
+
+  auto t = p.type_decl();
+  ASSERT_NE(t, nullptr);
+  EXPECT_EQ(t, int_type);
+  ASSERT_TRUE(t->IsI32());
+
+  TypeManager::Destroy();
+}
+
+TEST_F(ParserImplTest, TypeDecl_U32) {
+  ParserImpl p{"u32"};
+
+  auto tm = TypeManager::Instance();
+  auto uint_type = tm->Get(std::make_unique<ast::type::U32Type>());
+
+  auto t = p.type_decl();
+  ASSERT_NE(t, nullptr);
+  EXPECT_EQ(t, uint_type);
+  ASSERT_TRUE(t->IsU32());
+
+  TypeManager::Destroy();
+}
+
+struct VecData {
+  const char* input;
+  size_t count;
+};
+inline std::ostream& operator<<(std::ostream& out, VecData data) {
+  out << std::string(data.input);
+  return out;
+}
+using VecTest = testing::TestWithParam<VecData>;
+TEST_P(VecTest, Parse) {
+  auto params = GetParam();
+  ParserImpl p{params.input};
+  auto t = p.type_decl();
+  ASSERT_NE(t, nullptr);
+  ASSERT_FALSE(p.has_error());
+  EXPECT_TRUE(t->IsVector());
+  EXPECT_EQ(t->AsVector()->size(), params.count);
+}
+INSTANTIATE_TEST_SUITE_P(ParserImplTest,
+                         VecTest,
+                         testing::Values(VecData{"vec2<f32>", 2},
+                                         VecData{"vec3<f32>", 3},
+                                         VecData{"vec4<f32>", 4}));
+
+using VecMissingGreaterThanTest = testing::TestWithParam<VecData>;
+TEST_P(VecMissingGreaterThanTest, Handles_Missing_GreaterThan) {
+  auto params = GetParam();
+  ParserImpl p{params.input};
+  auto t = p.type_decl();
+  ASSERT_EQ(t, nullptr);
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(p.error(), "1:9: missing > for vector");
+}
+INSTANTIATE_TEST_SUITE_P(ParserImplTest,
+                         VecMissingGreaterThanTest,
+                         testing::Values(VecData{"vec2<f32", 2},
+                                         VecData{"vec3<f32", 3},
+                                         VecData{"vec4<f32", 4}));
+
+using VecMissingLessThanTest = testing::TestWithParam<VecData>;
+TEST_P(VecMissingLessThanTest, Handles_Missing_GreaterThan) {
+  auto params = GetParam();
+  ParserImpl p{params.input};
+  auto t = p.type_decl();
+  ASSERT_EQ(t, nullptr);
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(p.error(), "1:5: missing < for vector");
+}
+INSTANTIATE_TEST_SUITE_P(ParserImplTest,
+                         VecMissingLessThanTest,
+                         testing::Values(VecData{"vec2", 2},
+                                         VecData{"vec3", 3},
+                                         VecData{"vec4", 4}));
+
+using VecBadType = testing::TestWithParam<VecData>;
+TEST_P(VecBadType, Handles_Unknown_Type) {
+  auto params = GetParam();
+  ParserImpl p{params.input};
+  auto t = p.type_decl();
+  ASSERT_EQ(t, nullptr);
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(p.error(), "1:6: unknown type alias 'unknown'");
+}
+INSTANTIATE_TEST_SUITE_P(ParserImplTest,
+                         VecBadType,
+                         testing::Values(VecData{"vec2<unknown", 2},
+                                         VecData{"vec3<unknown", 3},
+                                         VecData{"vec4<unknown", 4}));
+
+using VecMissingType = testing::TestWithParam<VecData>;
+TEST_P(VecMissingType, Handles_Missing_Type) {
+  auto params = GetParam();
+  ParserImpl p{params.input};
+  auto t = p.type_decl();
+  ASSERT_EQ(t, nullptr);
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(p.error(), "1:6: unable to determine subtype for vector");
+}
+INSTANTIATE_TEST_SUITE_P(ParserImplTest,
+                         VecMissingType,
+                         testing::Values(VecData{"vec2<>", 2},
+                                         VecData{"vec3<>", 3},
+                                         VecData{"vec4<>", 4}));
+
+TEST_F(ParserImplTest, TypeDecl_Ptr) {
+  ParserImpl p{"ptr<function, f32>"};
+  auto t = p.type_decl();
+  ASSERT_NE(t, nullptr) << p.error();
+  ASSERT_FALSE(p.has_error());
+  ASSERT_TRUE(t->IsPointer());
+
+  auto ptr = t->AsPointer();
+  ASSERT_TRUE(ptr->type()->IsF32());
+  ASSERT_EQ(ptr->storage_class(), ast::StorageClass::kFunction);
+}
+
+TEST_F(ParserImplTest, TypeDecl_Ptr_ToVec) {
+  ParserImpl p{"ptr<function, vec2<f32>>"};
+  auto t = p.type_decl();
+  ASSERT_NE(t, nullptr) << p.error();
+  ASSERT_FALSE(p.has_error());
+  ASSERT_TRUE(t->IsPointer());
+
+  auto ptr = t->AsPointer();
+  ASSERT_TRUE(ptr->type()->IsVector());
+  ASSERT_EQ(ptr->storage_class(), ast::StorageClass::kFunction);
+
+  auto vec = ptr->type()->AsVector();
+  ASSERT_EQ(vec->size(), 2);
+  ASSERT_TRUE(vec->type()->IsF32());
+}
+
+TEST_F(ParserImplTest, TypeDecl_Ptr_MissingLessThan) {
+  ParserImpl p{"ptr private, f32>"};
+  auto t = p.type_decl();
+  ASSERT_EQ(t, nullptr);
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(p.error(), "1:5: missing < for ptr declaration");
+}
+
+TEST_F(ParserImplTest, TypeDecl_Ptr_MissingGreaterThan) {
+  ParserImpl p{"ptr<function, f32"};
+  auto t = p.type_decl();
+  ASSERT_EQ(t, nullptr);
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(p.error(), "1:18: missing > for ptr declaration");
+}
+
+TEST_F(ParserImplTest, TypeDecl_Ptr_MissingComma) {
+  ParserImpl p{"ptr<function f32>"};
+  auto t = p.type_decl();
+  ASSERT_EQ(t, nullptr);
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(p.error(), "1:14: missing , for ptr declaration");
+}
+
+TEST_F(ParserImplTest, TypeDecl_Ptr_MissingStorageClass) {
+  ParserImpl p{"ptr<, f32>"};
+  auto t = p.type_decl();
+  ASSERT_EQ(t, nullptr);
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(p.error(), "1:5: missing storage class for ptr declaration");
+}
+
+TEST_F(ParserImplTest, TypeDecl_Ptr_MissingParams) {
+  ParserImpl p{"ptr<>"};
+  auto t = p.type_decl();
+  ASSERT_EQ(t, nullptr);
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(p.error(), "1:5: missing storage class for ptr declaration");
+}
+
+TEST_F(ParserImplTest, TypeDecl_Ptr_MissingType) {
+  ParserImpl p{"ptr<function,>"};
+  auto t = p.type_decl();
+  ASSERT_EQ(t, nullptr);
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(p.error(), "1:14: missing type for ptr declaration");
+}
+
+TEST_F(ParserImplTest, TypeDecl_Ptr_BadStorageClass) {
+  ParserImpl p{"ptr<unknown, f32>"};
+  auto t = p.type_decl();
+  ASSERT_EQ(t, nullptr);
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(p.error(), "1:5: missing storage class for ptr declaration");
+}
+
+TEST_F(ParserImplTest, TypeDecl_Ptr_BadType) {
+  ParserImpl p{"ptr<function, unknown>"};
+  auto t = p.type_decl();
+  ASSERT_EQ(t, nullptr);
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(p.error(), "1:15: unknown type alias 'unknown'");
+}
+
+TEST_F(ParserImplTest, TypeDecl_Array) {
+  ParserImpl p{"array<f32, 5>"};
+  auto t = p.type_decl();
+  ASSERT_NE(t, nullptr);
+  ASSERT_FALSE(p.has_error());
+  ASSERT_TRUE(t->IsArray());
+
+  auto a = t->AsArray();
+  ASSERT_FALSE(a->IsRuntimeArray());
+  ASSERT_EQ(a->size(), 5);
+  ASSERT_TRUE(a->type()->IsF32());
+}
+
+TEST_F(ParserImplTest, TypeDecl_Array_Runtime) {
+  ParserImpl p{"array<u32>"};
+  auto t = p.type_decl();
+  ASSERT_NE(t, nullptr);
+  ASSERT_FALSE(p.has_error());
+  ASSERT_TRUE(t->IsArray());
+
+  auto a = t->AsArray();
+  ASSERT_TRUE(a->IsRuntimeArray());
+  ASSERT_TRUE(a->type()->IsU32());
+}
+
+TEST_F(ParserImplTest, TypeDecl_Array_BadType) {
+  ParserImpl p{"array<unknown, 3>"};
+  auto t = p.type_decl();
+  ASSERT_EQ(t, nullptr);
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(p.error(), "1:7: unknown type alias 'unknown'");
+}
+
+TEST_F(ParserImplTest, TypeDecl_Array_ZeroSize) {
+  ParserImpl p{"array<f32, 0>"};
+  auto t = p.type_decl();
+  ASSERT_EQ(t, nullptr);
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(p.error(), "1:12: invalid size for array declaration");
+}
+
+TEST_F(ParserImplTest, TypeDecl_Array_NegativeSize) {
+  ParserImpl p{"array<f32, -1>"};
+  auto t = p.type_decl();
+  ASSERT_EQ(t, nullptr);
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(p.error(), "1:12: invalid size for array declaration");
+}
+
+TEST_F(ParserImplTest, TypeDecl_Array_BadSize) {
+  ParserImpl p{"array<f32, invalid>"};
+  auto t = p.type_decl();
+  ASSERT_EQ(t, nullptr);
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(p.error(), "1:12: missing size of array declaration");
+}
+
+TEST_F(ParserImplTest, TypeDecl_Array_MissingLessThan) {
+  ParserImpl p{"array f32>"};
+  auto t = p.type_decl();
+  ASSERT_EQ(t, nullptr);
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(p.error(), "1:7: missing < for array declaration");
+}
+
+TEST_F(ParserImplTest, TypeDecl_Array_MissingGreaterThan) {
+  ParserImpl p{"array<f32"};
+  auto t = p.type_decl();
+  ASSERT_EQ(t, nullptr);
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(p.error(), "1:10: missing > for array declaration");
+}
+
+TEST_F(ParserImplTest, TypeDecl_Array_MissingComma) {
+  ParserImpl p{"array<f32 3>"};
+  auto t = p.type_decl();
+  ASSERT_EQ(t, nullptr);
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(p.error(), "1:11: missing > for array declaration");
+}
+
+struct MatrixData {
+  const char* input;
+  size_t rows;
+  size_t columns;
+};
+inline std::ostream& operator<<(std::ostream& out, MatrixData data) {
+  out << std::string(data.input);
+  return out;
+}
+using MatrixTest = testing::TestWithParam<MatrixData>;
+TEST_P(MatrixTest, Parse) {
+  auto params = GetParam();
+  ParserImpl p{params.input};
+  auto t = p.type_decl();
+  ASSERT_NE(t, nullptr);
+  ASSERT_FALSE(p.has_error());
+  EXPECT_TRUE(t->IsMatrix());
+  auto mat = t->AsMatrix();
+  EXPECT_EQ(mat->rows(), params.rows);
+  EXPECT_EQ(mat->columns(), params.columns);
+}
+INSTANTIATE_TEST_SUITE_P(ParserImplTest,
+                         MatrixTest,
+                         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}));
+
+using MatrixMissingGreaterThanTest = testing::TestWithParam<MatrixData>;
+TEST_P(MatrixMissingGreaterThanTest, Handles_Missing_GreaterThan) {
+  auto params = GetParam();
+  ParserImpl p{params.input};
+  auto t = p.type_decl();
+  ASSERT_EQ(t, nullptr);
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(p.error(), "1:11: missing > for matrix");
+}
+INSTANTIATE_TEST_SUITE_P(ParserImplTest,
+                         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}));
+
+using MatrixMissingLessThanTest = testing::TestWithParam<MatrixData>;
+TEST_P(MatrixMissingLessThanTest, Handles_Missing_GreaterThan) {
+  auto params = GetParam();
+  ParserImpl p{params.input};
+  auto t = p.type_decl();
+  ASSERT_EQ(t, nullptr);
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(p.error(), "1:8: missing < for matrix");
+}
+INSTANTIATE_TEST_SUITE_P(ParserImplTest,
+                         MatrixMissingLessThanTest,
+                         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}));
+
+using MatrixBadType = testing::TestWithParam<MatrixData>;
+TEST_P(MatrixBadType, Handles_Unknown_Type) {
+  auto params = GetParam();
+  ParserImpl p{params.input};
+  auto t = p.type_decl();
+  ASSERT_EQ(t, nullptr);
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(p.error(), "1:8: unknown type alias 'unknown'");
+}
+INSTANTIATE_TEST_SUITE_P(ParserImplTest,
+                         MatrixBadType,
+                         testing::Values(MatrixData{"mat2x2<unknown>", 2, 2},
+                                         MatrixData{"mat2x3<unknown>", 2, 3},
+                                         MatrixData{"mat2x4<unknown>", 2, 4},
+                                         MatrixData{"mat3x2<unknown>", 3, 2},
+                                         MatrixData{"mat3x3<unknown>", 3, 3},
+                                         MatrixData{"mat3x4<unknown>", 3, 4},
+                                         MatrixData{"mat4x2<unknown>", 4, 2},
+                                         MatrixData{"mat4x3<unknown>", 4, 3},
+                                         MatrixData{"mat4x4<unknown>", 4, 4}));
+
+using MatrixMissingType = testing::TestWithParam<MatrixData>;
+TEST_P(MatrixMissingType, Handles_Missing_Type) {
+  auto params = GetParam();
+  ParserImpl p{params.input};
+  auto t = p.type_decl();
+  ASSERT_EQ(t, nullptr);
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(p.error(), "1:8: unable to determine subtype for matrix");
+}
+INSTANTIATE_TEST_SUITE_P(ParserImplTest,
+                         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}));
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_unary_expression_test.cc b/src/reader/wgsl/parser_impl_unary_expression_test.cc
new file mode 100644
index 0000000..48acd55
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_unary_expression_test.cc
@@ -0,0 +1,846 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/array_accessor_expression.h"
+#include "src/ast/const_initializer_expression.h"
+#include "src/ast/identifier_expression.h"
+#include "src/ast/int_literal.h"
+#include "src/ast/unary_derivative_expression.h"
+#include "src/ast/unary_method_expression.h"
+#include "src/ast/unary_op_expression.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, UnaryExpression_Postix) {
+  ParserImpl p{"a[2]"};
+  auto e = p.unary_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+
+  ASSERT_TRUE(e->IsArrayAccessor());
+  auto ary = e->AsArrayAccessor();
+  ASSERT_TRUE(ary->array()->IsIdentifier());
+  auto ident = ary->array()->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 1);
+  EXPECT_EQ(ident->name()[0], "a");
+
+  ASSERT_TRUE(ary->idx_expr()->IsInitializer());
+  ASSERT_TRUE(ary->idx_expr()->AsInitializer()->IsConstInitializer());
+  auto init = ary->idx_expr()->AsInitializer()->AsConstInitializer();
+  ASSERT_TRUE(init->literal()->IsInt());
+  ASSERT_EQ(init->literal()->AsInt()->value(), 2);
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Minus) {
+  ParserImpl p{"- 1"};
+  auto e = p.unary_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsUnaryOp());
+
+  auto u = e->AsUnaryOp();
+  ASSERT_EQ(u->op(), ast::UnaryOp::kNegation);
+
+  ASSERT_TRUE(u->expr()->IsInitializer());
+  ASSERT_TRUE(u->expr()->AsInitializer()->IsConstInitializer());
+
+  auto init = u->expr()->AsInitializer()->AsConstInitializer();
+  ASSERT_TRUE(init->literal()->IsInt());
+  EXPECT_EQ(init->literal()->AsInt()->value(), 1);
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Minus_InvalidRHS) {
+  ParserImpl p{"-if(a) {}"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:2: unable to parse right side of - expression");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Bang) {
+  ParserImpl p{"!1"};
+  auto e = p.unary_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsUnaryOp());
+
+  auto u = e->AsUnaryOp();
+  ASSERT_EQ(u->op(), ast::UnaryOp::kNot);
+
+  ASSERT_TRUE(u->expr()->IsInitializer());
+  ASSERT_TRUE(u->expr()->AsInitializer()->IsConstInitializer());
+
+  auto init = u->expr()->AsInitializer()->AsConstInitializer();
+  ASSERT_TRUE(init->literal()->IsInt());
+  EXPECT_EQ(init->literal()->AsInt()->value(), 1);
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Bang_InvalidRHS) {
+  ParserImpl p{"!if (a) {}"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:2: unable to parse right side of ! expression");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Any) {
+  ParserImpl p{"any(a)"};
+  auto e = p.unary_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsUnaryMethod());
+
+  auto u = e->AsUnaryMethod();
+  ASSERT_EQ(u->op(), ast::UnaryMethod::kAny);
+  ASSERT_EQ(u->params().size(), 1);
+  ASSERT_TRUE(u->params()[0]->IsIdentifier());
+  auto ident = u->params()[0]->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 1);
+  EXPECT_EQ(ident->name()[0], "a");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Any_MissingParenLeft) {
+  ParserImpl p{"any a)"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:5: missing ( for method call");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Any_MissingParenRight) {
+  ParserImpl p{"any(a"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:6: missing ) for method call");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Any_MissingIdentifier) {
+  ParserImpl p{"any()"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:5: missing identifier for method call");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Any_InvalidIdentifier) {
+  ParserImpl p{"any(123)"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:5: missing identifier for method call");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_All) {
+  ParserImpl p{"all(a)"};
+  auto e = p.unary_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsUnaryMethod());
+
+  auto u = e->AsUnaryMethod();
+  ASSERT_EQ(u->op(), ast::UnaryMethod::kAll);
+  ASSERT_EQ(u->params().size(), 1);
+  ASSERT_TRUE(u->params()[0]->IsIdentifier());
+  auto ident = u->params()[0]->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 1);
+  EXPECT_EQ(ident->name()[0], "a");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_All_MissingParenLeft) {
+  ParserImpl p{"all a)"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:5: missing ( for method call");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_All_MissingParenRight) {
+  ParserImpl p{"all(a"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:6: missing ) for method call");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_All_MissingIdentifier) {
+  ParserImpl p{"all()"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:5: missing identifier for method call");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_All_InvalidIdentifier) {
+  ParserImpl p{"all(123)"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:5: missing identifier for method call");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_IsNan) {
+  ParserImpl p{"is_nan(a)"};
+  auto e = p.unary_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsUnaryMethod());
+
+  auto u = e->AsUnaryMethod();
+  ASSERT_EQ(u->op(), ast::UnaryMethod::kIsNan);
+  ASSERT_EQ(u->params().size(), 1);
+  ASSERT_TRUE(u->params()[0]->IsIdentifier());
+  auto ident = u->params()[0]->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 1);
+  EXPECT_EQ(ident->name()[0], "a");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_IsNan_MissingParenLeft) {
+  ParserImpl p{"is_nan a)"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:8: missing ( for method call");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_IsNan_MissingParenRight) {
+  ParserImpl p{"is_nan(a"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:9: missing ) for method call");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_IsNan_MissingIdentifier) {
+  ParserImpl p{"is_nan()"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:8: missing identifier for method call");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_IsNan_InvalidIdentifier) {
+  ParserImpl p{"is_nan(123)"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:8: missing identifier for method call");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_IsInf) {
+  ParserImpl p{"is_inf(a)"};
+  auto e = p.unary_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsUnaryMethod());
+
+  auto u = e->AsUnaryMethod();
+  ASSERT_EQ(u->op(), ast::UnaryMethod::kIsInf);
+  ASSERT_EQ(u->params().size(), 1);
+  ASSERT_TRUE(u->params()[0]->IsIdentifier());
+  auto ident = u->params()[0]->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 1);
+  EXPECT_EQ(ident->name()[0], "a");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_IsInf_MissingParenLeft) {
+  ParserImpl p{"is_inf a)"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:8: missing ( for method call");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_IsInf_MissingParenRight) {
+  ParserImpl p{"is_inf(a"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:9: missing ) for method call");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_IsInf_MissingIdentifier) {
+  ParserImpl p{"is_inf()"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:8: missing identifier for method call");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_IsInf_InvalidIdentifier) {
+  ParserImpl p{"is_inf(123)"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:8: missing identifier for method call");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_IsFinite) {
+  ParserImpl p{"is_finite(a)"};
+  auto e = p.unary_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsUnaryMethod());
+
+  auto u = e->AsUnaryMethod();
+  ASSERT_EQ(u->op(), ast::UnaryMethod::kIsFinite);
+  ASSERT_EQ(u->params().size(), 1);
+  ASSERT_TRUE(u->params()[0]->IsIdentifier());
+  auto ident = u->params()[0]->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 1);
+  EXPECT_EQ(ident->name()[0], "a");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_IsFinite_MissingParenLeft) {
+  ParserImpl p{"is_finite a)"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:11: missing ( for method call");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_IsFinite_MissingParenRight) {
+  ParserImpl p{"is_finite(a"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:12: missing ) for method call");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_IsFinite_MissingIdentifier) {
+  ParserImpl p{"is_finite()"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:11: missing identifier for method call");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_IsFinite_InvalidIdentifier) {
+  ParserImpl p{"is_finite(123)"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:11: missing identifier for method call");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_IsNormal) {
+  ParserImpl p{"is_normal(a)"};
+  auto e = p.unary_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsUnaryMethod());
+
+  auto u = e->AsUnaryMethod();
+  ASSERT_EQ(u->op(), ast::UnaryMethod::kIsNormal);
+  ASSERT_EQ(u->params().size(), 1);
+  ASSERT_TRUE(u->params()[0]->IsIdentifier());
+  auto ident = u->params()[0]->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 1);
+  EXPECT_EQ(ident->name()[0], "a");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_IsNormal_MissingParenLeft) {
+  ParserImpl p{"is_normal a)"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:11: missing ( for method call");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_IsNormal_MissingParenRight) {
+  ParserImpl p{"is_normal(a"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:12: missing ) for method call");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_IsNormal_MissingIdentifier) {
+  ParserImpl p{"is_normal()"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:11: missing identifier for method call");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_IsNormal_InvalidIdentifier) {
+  ParserImpl p{"is_normal(123)"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:11: missing identifier for method call");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Dot) {
+  ParserImpl p{"dot(a, b)"};
+  auto e = p.unary_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsUnaryMethod());
+
+  auto u = e->AsUnaryMethod();
+  ASSERT_EQ(u->op(), ast::UnaryMethod::kDot);
+  ASSERT_EQ(u->params().size(), 2);
+  ASSERT_TRUE(u->params()[0]->IsIdentifier());
+  auto ident = u->params()[0]->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 1);
+  EXPECT_EQ(ident->name()[0], "a");
+
+  ASSERT_TRUE(u->params()[1]->IsIdentifier());
+  ident = u->params()[1]->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 1);
+  EXPECT_EQ(ident->name()[0], "b");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Dot_MissingParenLeft) {
+  ParserImpl p{"dot a, b)"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:5: missing ( for method call");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Dot_MissingParenRight) {
+  ParserImpl p{"dot(a, b"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:9: missing ) for method call");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Dot_MissingFirstIdentifier) {
+  ParserImpl p{"dot(, a)"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:5: missing identifier for method call");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Dot_MissingSecondIdentifier) {
+  ParserImpl p{"dot(a, )"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:8: missing identifier for method call");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Dot_MissingComma) {
+  ParserImpl p{"dot(a b)"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:7: missing , for method call");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Dot_InvalidFirstIdentifier) {
+  ParserImpl p{"dot(123, b)"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:5: missing identifier for method call");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Dot_InvalidSecondIdentifier) {
+  ParserImpl p{"dot(a, 123)"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:8: missing identifier for method call");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_OuterProduct) {
+  ParserImpl p{"outer_product(a, b)"};
+  auto e = p.unary_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsUnaryMethod());
+
+  auto u = e->AsUnaryMethod();
+  ASSERT_EQ(u->op(), ast::UnaryMethod::kOuterProduct);
+  ASSERT_EQ(u->params().size(), 2);
+  ASSERT_TRUE(u->params()[0]->IsIdentifier());
+  auto ident = u->params()[0]->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 1);
+  EXPECT_EQ(ident->name()[0], "a");
+
+  ASSERT_TRUE(u->params()[1]->IsIdentifier());
+  ident = u->params()[1]->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 1);
+  EXPECT_EQ(ident->name()[0], "b");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_OuterProduct_MissingParenLeft) {
+  ParserImpl p{"outer_product a, b)"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:15: missing ( for method call");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_OuterProduct_MissingParenRight) {
+  ParserImpl p{"outer_product(a, b"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:19: missing ) for method call");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_OuterProduct_MissingFirstIdentifier) {
+  ParserImpl p{"outer_product(, b)"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:15: missing identifier for method call");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_OuterProduct_MissingSecondIdentifier) {
+  ParserImpl p{"outer_product(a, )"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:18: missing identifier for method call");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_OuterProduct_MissingComma) {
+  ParserImpl p{"outer_product(a b)"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:17: missing , for method call");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_OuterProduct_InvalidFirstIdentifier) {
+  ParserImpl p{"outer_product(123, b)"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:15: missing identifier for method call");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_OuterProduct_InvalidSecondIdentifier) {
+  ParserImpl p{"outer_product(a, 123)"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:18: missing identifier for method call");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Dpdx_NoModifier) {
+  ParserImpl p{"dpdx(a)"};
+  auto e = p.unary_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsUnaryDerivative());
+
+  auto deriv = e->AsUnaryDerivative();
+  EXPECT_EQ(deriv->op(), ast::UnaryDerivative::kDpdx);
+  EXPECT_EQ(deriv->modifier(), ast::DerivativeModifier::kNone);
+
+  ASSERT_NE(deriv->param(), nullptr);
+  ASSERT_TRUE(deriv->param()->IsIdentifier());
+  auto ident = deriv->param()->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 1);
+  EXPECT_EQ(ident->name()[0], "a");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Dpdx_WithModifier) {
+  ParserImpl p{"dpdx<coarse>(a)"};
+  auto e = p.unary_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsUnaryDerivative());
+
+  auto deriv = e->AsUnaryDerivative();
+  EXPECT_EQ(deriv->op(), ast::UnaryDerivative::kDpdx);
+  EXPECT_EQ(deriv->modifier(), ast::DerivativeModifier::kCoarse);
+
+  ASSERT_NE(deriv->param(), nullptr);
+  ASSERT_TRUE(deriv->param()->IsIdentifier());
+  auto ident = deriv->param()->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 1);
+  EXPECT_EQ(ident->name()[0], "a");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Dpdx_MissingLessThan) {
+  ParserImpl p{"dpdx coarse>(a)"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:6: missing ( for derivative method");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Dpdx_InvalidModifier) {
+  ParserImpl p{"dpdx<invalid>(a)"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:6: unable to parse derivative modifier");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Dpdx_EmptyModifer) {
+  ParserImpl p{"dpdx coarse>(a)"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:6: missing ( for derivative method");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Dpdx_MissingGreaterThan) {
+  ParserImpl p{"dpdx<coarse (a)"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:13: missing > for derivative modifier");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Dpdx_MisisngLeftParen) {
+  ParserImpl p{"dpdx<coarse>a)"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:13: missing ( for derivative method");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Dpdx_MissingRightParen) {
+  ParserImpl p{"dpdx<coarse>(a"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:15: missing ) for derivative method");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Dpdx_MissingIdentifier) {
+  ParserImpl p{"dpdx<coarse>()"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:14: missing identifier for derivative method");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Dpdx_InvalidIdentifeir) {
+  ParserImpl p{"dpdx<coarse>(12345)"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:14: missing identifier for derivative method");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Dpdy_NoModifier) {
+  ParserImpl p{"dpdy(a)"};
+  auto e = p.unary_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsUnaryDerivative());
+
+  auto deriv = e->AsUnaryDerivative();
+  EXPECT_EQ(deriv->op(), ast::UnaryDerivative::kDpdy);
+  EXPECT_EQ(deriv->modifier(), ast::DerivativeModifier::kNone);
+
+  ASSERT_NE(deriv->param(), nullptr);
+  ASSERT_TRUE(deriv->param()->IsIdentifier());
+  auto ident = deriv->param()->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 1);
+  EXPECT_EQ(ident->name()[0], "a");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Dpdy_WithModifier) {
+  ParserImpl p{"dpdy<fine>(a)"};
+  auto e = p.unary_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsUnaryDerivative());
+
+  auto deriv = e->AsUnaryDerivative();
+  EXPECT_EQ(deriv->op(), ast::UnaryDerivative::kDpdy);
+  EXPECT_EQ(deriv->modifier(), ast::DerivativeModifier::kFine);
+
+  ASSERT_NE(deriv->param(), nullptr);
+  ASSERT_TRUE(deriv->param()->IsIdentifier());
+  auto ident = deriv->param()->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 1);
+  EXPECT_EQ(ident->name()[0], "a");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Dpdy_MissingLessThan) {
+  ParserImpl p{"dpdy coarse>(a)"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:6: missing ( for derivative method");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Dpdy_InvalidModifier) {
+  ParserImpl p{"dpdy<invalid>(a)"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:6: unable to parse derivative modifier");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Dpdy_EmptyModifer) {
+  ParserImpl p{"dpdy coarse>(a)"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:6: missing ( for derivative method");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Dpdy_MissingGreaterThan) {
+  ParserImpl p{"dpdy<coarse (a)"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:13: missing > for derivative modifier");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Dpdy_MisisngLeftParen) {
+  ParserImpl p{"dpdy<coarse>a)"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:13: missing ( for derivative method");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Dpdy_MissingRightParen) {
+  ParserImpl p{"dpdy<coarse>(a"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:15: missing ) for derivative method");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Dpdy_MissingIdentifier) {
+  ParserImpl p{"dpdy<coarse>()"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:14: missing identifier for derivative method");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Dpdy_InvalidIdentifeir) {
+  ParserImpl p{"dpdy<coarse>(12345)"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:14: missing identifier for derivative method");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Fwidth_NoModifier) {
+  ParserImpl p{"fwidth(a)"};
+  auto e = p.unary_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsUnaryDerivative());
+
+  auto deriv = e->AsUnaryDerivative();
+  EXPECT_EQ(deriv->op(), ast::UnaryDerivative::kFwidth);
+  EXPECT_EQ(deriv->modifier(), ast::DerivativeModifier::kNone);
+
+  ASSERT_NE(deriv->param(), nullptr);
+  ASSERT_TRUE(deriv->param()->IsIdentifier());
+  auto ident = deriv->param()->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 1);
+  EXPECT_EQ(ident->name()[0], "a");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Fwidth_WithModifier) {
+  ParserImpl p{"fwidth<coarse>(a)"};
+  auto e = p.unary_expression();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsUnaryDerivative());
+
+  auto deriv = e->AsUnaryDerivative();
+  EXPECT_EQ(deriv->op(), ast::UnaryDerivative::kFwidth);
+  EXPECT_EQ(deriv->modifier(), ast::DerivativeModifier::kCoarse);
+
+  ASSERT_NE(deriv->param(), nullptr);
+  ASSERT_TRUE(deriv->param()->IsIdentifier());
+  auto ident = deriv->param()->AsIdentifier();
+  ASSERT_EQ(ident->name().size(), 1);
+  EXPECT_EQ(ident->name()[0], "a");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Fwidth_MissingLessThan) {
+  ParserImpl p{"fwidth coarse>(a)"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:8: missing ( for derivative method");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Fwidth_InvalidModifier) {
+  ParserImpl p{"fwidth<invalid>(a)"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:8: unable to parse derivative modifier");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Fwidth_EmptyModifer) {
+  ParserImpl p{"fwidth coarse>(a)"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:8: missing ( for derivative method");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Fwidth_MissingGreaterThan) {
+  ParserImpl p{"fwidth<coarse (a)"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:15: missing > for derivative modifier");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Fwidth_MisisngLeftParen) {
+  ParserImpl p{"fwidth<coarse>a)"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:15: missing ( for derivative method");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Fwidth_MissingRightParen) {
+  ParserImpl p{"fwidth<coarse>(a"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:17: missing ) for derivative method");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Fwidth_MissingIdentifier) {
+  ParserImpl p{"fwidth<coarse>()"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:16: missing identifier for derivative method");
+}
+
+TEST_F(ParserImplTest, UnaryExpression_Fwidht_InvalidIdentifeir) {
+  ParserImpl p{"fwidth<coarse>(12345)"};
+  auto e = p.unary_expression();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:16: missing identifier for derivative method");
+}
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_unless_stmt_test.cc b/src/reader/wgsl/parser_impl_unless_stmt_test.cc
new file mode 100644
index 0000000..f34d7d8
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_unless_stmt_test.cc
@@ -0,0 +1,62 @@
+// 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 "gtest/gtest.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, UnlessStmt) {
+  ParserImpl p{"unless (a) { kill; }"};
+  auto e = p.unless_stmt();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsUnless());
+  ASSERT_NE(e->condition(), nullptr);
+  EXPECT_TRUE(e->condition()->IsIdentifier());
+  ASSERT_EQ(e->body().size(), 1);
+  EXPECT_TRUE(e->body()[0]->IsKill());
+}
+
+TEST_F(ParserImplTest, UnlessStmt_InvalidCondition) {
+  ParserImpl p{"unless(if(a){}) {}"};
+  auto e = p.unless_stmt();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:8: unable to parse expression");
+}
+
+TEST_F(ParserImplTest, UnlessStmt_EmptyCondition) {
+  ParserImpl p{"unless() {}"};
+  auto e = p.unless_stmt();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:8: unable to parse expression");
+}
+
+TEST_F(ParserImplTest, UnlessStmt_InvalidBody) {
+  ParserImpl p{"unless(a + 2 - 5 == true) { kill }"};
+  auto e = p.unless_stmt();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:34: missing ;");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_variable_decl_test.cc b/src/reader/wgsl/parser_impl_variable_decl_test.cc
new file mode 100644
index 0000000..73d7c17
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_variable_decl_test.cc
@@ -0,0 +1,75 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/variable.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, VariableDecl_Parses) {
+  ParserImpl p{"var my_var : f32"};
+  auto var = p.variable_decl();
+  ASSERT_FALSE(p.has_error());
+  ASSERT_NE(var, nullptr);
+  ASSERT_EQ(var->name(), "my_var");
+  ASSERT_NE(var->type(), nullptr);
+  ASSERT_EQ(var->source().line, 1);
+  ASSERT_EQ(var->source().column, 1);
+  ASSERT_TRUE(var->type()->IsF32());
+}
+
+TEST_F(ParserImplTest, VariableDecl_MissingVar) {
+  ParserImpl p{"my_var : f32"};
+  auto v = p.variable_decl();
+  ASSERT_EQ(v, nullptr);
+  ASSERT_FALSE(p.has_error());
+
+  auto t = p.next();
+  ASSERT_TRUE(t.IsIdentifier());
+}
+
+TEST_F(ParserImplTest, VariableDecl_InvalidIdentDecl) {
+  ParserImpl p{"var my_var f32"};
+  auto v = p.variable_decl();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(v, nullptr);
+  ASSERT_EQ(p.error(), "1:12: missing : for identifier declaration");
+}
+
+TEST_F(ParserImplTest, VariableDecl_WithStorageClass) {
+  ParserImpl p{"var<private> my_var : f32"};
+  auto v = p.variable_decl();
+  ASSERT_FALSE(p.has_error());
+  ASSERT_NE(v, nullptr);
+  EXPECT_EQ(v->name(), "my_var");
+  EXPECT_TRUE(v->type()->IsF32());
+  EXPECT_EQ(v->storage_class(), ast::StorageClass::kPrivate);
+}
+
+TEST_F(ParserImplTest, VariableDecl_InvalidStorageClass) {
+  ParserImpl p{"var<unknown> my_var : f32"};
+  auto v = p.variable_decl();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(v, nullptr);
+  EXPECT_EQ(p.error(), "1:5: invalid storage class for variable decoration");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_variable_decoration_list_test.cc b/src/reader/wgsl/parser_impl_variable_decoration_list_test.cc
new file mode 100644
index 0000000..19029e0
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_variable_decoration_list_test.cc
@@ -0,0 +1,81 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/builtin_decoration.h"
+#include "src/ast/location_decoration.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, VariableDecorationList_Parses) {
+  ParserImpl p{R"([[location 4, builtin position]])"};
+  auto decos = p.variable_decoration_list();
+  ASSERT_FALSE(p.has_error());
+  ASSERT_EQ(decos.size(), 2);
+  ASSERT_TRUE(decos[0]->IsLocation());
+  EXPECT_EQ(decos[0]->AsLocation()->value(), 4);
+  ASSERT_TRUE(decos[1]->IsBuiltin());
+  EXPECT_EQ(decos[1]->AsBuiltin()->value(), ast::Builtin::kPosition);
+}
+
+TEST_F(ParserImplTest, VariableDecorationList_Empty) {
+  ParserImpl p{R"([[]])"};
+  auto decos = p.variable_decoration_list();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(p.error(), "1:3: empty variable decoration list");
+}
+
+TEST_F(ParserImplTest, VariableDecorationList_Invalid) {
+  ParserImpl p{R"([[invalid]])"};
+  auto decos = p.variable_decoration_list();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(p.error(), "1:3: missing variable decoration for decoration list");
+}
+
+TEST_F(ParserImplTest, VariableDecorationList_ExtraComma) {
+  ParserImpl p{R"([[builtin position, ]])"};
+  auto decos = p.variable_decoration_list();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(p.error(), "1:21: missing variable decoration after comma");
+}
+
+TEST_F(ParserImplTest, VariableDecorationList_MissingComma) {
+  ParserImpl p{R"([[binding 4 location 5]])"};
+  auto decos = p.variable_decoration_list();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(p.error(), "1:13: missing comma in variable decoration list");
+}
+
+TEST_F(ParserImplTest, VariableDecorationList_BadDecoration) {
+  ParserImpl p{R"([[location bad]])"};
+  auto decos = p.variable_decoration_list();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(p.error(), "1:12: invalid value for location decoration");
+}
+
+TEST_F(ParserImplTest, VariableDecorationList_InvalidBuiltin) {
+  ParserImpl p{"[[builtin invalid]]"};
+  auto decos = p.variable_decoration_list();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(p.error(), "1:11: invalid value for builtin decoration");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_variable_decoration_test.cc b/src/reader/wgsl/parser_impl_variable_decoration_test.cc
new file mode 100644
index 0000000..311dd5a
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_variable_decoration_test.cc
@@ -0,0 +1,138 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/binding_decoration.h"
+#include "src/ast/builtin_decoration.h"
+#include "src/ast/location_decoration.h"
+#include "src/ast/set_decoration.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, VariableDecoration_Location) {
+  ParserImpl p{"location 4"};
+  auto deco = p.variable_decoration();
+  ASSERT_NE(deco, nullptr);
+  ASSERT_FALSE(p.has_error());
+  ASSERT_TRUE(deco->IsLocation());
+
+  auto loc = deco->AsLocation();
+  EXPECT_EQ(loc->value(), 4);
+}
+
+TEST_F(ParserImplTest, VariableDecoration_Location_MissingValue) {
+  ParserImpl p{"location"};
+  auto deco = p.variable_decoration();
+  ASSERT_EQ(deco, nullptr);
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(p.error(), "1:9: invalid value for location decoration");
+}
+
+TEST_F(ParserImplTest, VariableDecoration_Location_MissingInvalid) {
+  ParserImpl p{"location nan"};
+  auto deco = p.variable_decoration();
+  ASSERT_EQ(deco, nullptr);
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(p.error(), "1:10: invalid value for location decoration");
+}
+
+TEST_F(ParserImplTest, VariableDecoration_Builtin) {
+  ParserImpl p{"builtin frag_depth"};
+  auto deco = p.variable_decoration();
+  ASSERT_FALSE(p.has_error());
+  ASSERT_NE(deco, nullptr);
+  ASSERT_TRUE(deco->IsBuiltin());
+
+  auto builtin = deco->AsBuiltin();
+  EXPECT_EQ(builtin->value(), ast::Builtin::kFragDepth);
+}
+
+TEST_F(ParserImplTest, VariableDecoration_Builtin_MissingValue) {
+  ParserImpl p{"builtin"};
+  auto deco = p.variable_decoration();
+  ASSERT_EQ(deco, nullptr);
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(p.error(), "1:8: invalid value for builtin decoration");
+}
+
+TEST_F(ParserImplTest, VariableDecoration_Builtin_MissingInvalid) {
+  ParserImpl p{"builtin 3"};
+  auto deco = p.variable_decoration();
+  ASSERT_EQ(deco, nullptr);
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(p.error(), "1:9: invalid value for builtin decoration");
+}
+
+TEST_F(ParserImplTest, VariableDecoration_Binding) {
+  ParserImpl p{"binding 4"};
+  auto deco = p.variable_decoration();
+  ASSERT_NE(deco, nullptr);
+  ASSERT_FALSE(p.has_error());
+  ASSERT_TRUE(deco->IsBinding());
+
+  auto binding = deco->AsBinding();
+  EXPECT_EQ(binding->value(), 4);
+}
+
+TEST_F(ParserImplTest, VariableDecoration_Binding_MissingValue) {
+  ParserImpl p{"binding"};
+  auto deco = p.variable_decoration();
+  ASSERT_EQ(deco, nullptr);
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(p.error(), "1:8: invalid value for binding decoration");
+}
+
+TEST_F(ParserImplTest, VariableDecoration_Binding_MissingInvalid) {
+  ParserImpl p{"binding nan"};
+  auto deco = p.variable_decoration();
+  ASSERT_EQ(deco, nullptr);
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(p.error(), "1:9: invalid value for binding decoration");
+}
+
+TEST_F(ParserImplTest, VariableDecoration_set) {
+  ParserImpl p{"set 4"};
+  auto deco = p.variable_decoration();
+  ASSERT_FALSE(p.has_error());
+  ASSERT_NE(deco.get(), nullptr);
+  ASSERT_TRUE(deco->IsSet());
+
+  auto set = deco->AsSet();
+  EXPECT_EQ(set->value(), 4);
+}
+
+TEST_F(ParserImplTest, VariableDecoration_Set_MissingValue) {
+  ParserImpl p{"set"};
+  auto deco = p.variable_decoration();
+  ASSERT_EQ(deco, nullptr);
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(p.error(), "1:4: invalid value for set decoration");
+}
+
+TEST_F(ParserImplTest, VariableDecoration_Set_MissingInvalid) {
+  ParserImpl p{"set nan"};
+  auto deco = p.variable_decoration();
+  ASSERT_EQ(deco, nullptr);
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(p.error(), "1:5: invalid value for set decoration");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_variable_ident_decl_test.cc b/src/reader/wgsl/parser_impl_variable_ident_decl_test.cc
new file mode 100644
index 0000000..393884b
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_variable_ident_decl_test.cc
@@ -0,0 +1,84 @@
+// 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 "gtest/gtest.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, VariableIdentDecl_Parses) {
+  ParserImpl p{"my_var : f32"};
+  std::string name;
+  ast::type::Type* type;
+  std::tie(name, type) = p.variable_ident_decl();
+  ASSERT_FALSE(p.has_error());
+  ASSERT_EQ(name, "my_var");
+  ASSERT_NE(type, nullptr);
+  ASSERT_TRUE(type->IsF32());
+}
+
+TEST_F(ParserImplTest, VariableIdentDecl_MissingIdent) {
+  ParserImpl p{": f32"};
+  std::string name;
+  ast::type::Type* type;
+  std::tie(name, type) = p.variable_ident_decl();
+  ASSERT_FALSE(p.has_error());
+  ASSERT_EQ(name, "");
+  ASSERT_EQ(type, nullptr);
+
+  auto t = p.next();
+  ASSERT_TRUE(t.IsColon());
+}
+
+TEST_F(ParserImplTest, VariableIdentDecl_MissingColon) {
+  ParserImpl p{"my_var f32"};
+  auto r = p.variable_ident_decl();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(p.error(), "1:8: missing : for identifier declaration");
+}
+
+TEST_F(ParserImplTest, VariableIdentDecl_MissingType) {
+  ParserImpl p{"my_var :"};
+  auto r = p.variable_ident_decl();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(p.error(), "1:9: invalid type for identifier declaration");
+}
+
+TEST_F(ParserImplTest, VariableIdentDecl_InvalidIdent) {
+  ParserImpl p{"123 : f32"};
+  std::string name;
+  ast::type::Type* type;
+  std::tie(name, type) = p.variable_ident_decl();
+  ASSERT_FALSE(p.has_error());
+  ASSERT_EQ(name, "");
+  ASSERT_EQ(type, nullptr);
+
+  auto t = p.next();
+  ASSERT_TRUE(t.IsIntLiteral());
+}
+
+TEST_F(ParserImplTest, VariableIdentDecl_InvalidType) {
+  ParserImpl p{"my_var : invalid"};
+  auto r = p.variable_ident_decl();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(p.error(), "1:10: unknown type alias 'invalid'");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_variable_stmt_test.cc b/src/reader/wgsl/parser_impl_variable_stmt_test.cc
new file mode 100644
index 0000000..bbbfdc6
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_variable_stmt_test.cc
@@ -0,0 +1,109 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/statement.h"
+#include "src/ast/variable_statement.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+TEST_F(ParserImplTest, VariableStmt_VariableDecl) {
+  ParserImpl p{"var a : i32;"};
+  auto e = p.variable_stmt();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsVariable());
+  ASSERT_NE(e->variable(), nullptr);
+  EXPECT_EQ(e->variable()->name(), "a");
+
+  EXPECT_EQ(e->variable()->initializer(), nullptr);
+}
+
+TEST_F(ParserImplTest, VariableStmt_VariableDecl_WithInit) {
+  ParserImpl p{"var a : i32 = 1;"};
+  auto e = p.variable_stmt();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsVariable());
+  ASSERT_NE(e->variable(), nullptr);
+  EXPECT_EQ(e->variable()->name(), "a");
+
+  ASSERT_NE(e->variable()->initializer(), nullptr);
+  EXPECT_TRUE(e->variable()->initializer()->IsInitializer());
+}
+
+TEST_F(ParserImplTest, VariableStmt_VariableDecl_Invalid) {
+  ParserImpl p{"var a : invalid;"};
+  auto e = p.variable_stmt();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:9: unknown type alias 'invalid'");
+}
+
+TEST_F(ParserImplTest, VariableStmt_VariableDecl_InitializerInvalid) {
+  ParserImpl p{"var a : i32 = if(a) {}"};
+  auto e = p.variable_stmt();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:15: missing initializer for variable declaration");
+}
+
+TEST_F(ParserImplTest, VariableStmt_Const) {
+  ParserImpl p{"const a : i32 = 1"};
+  auto e = p.variable_stmt();
+  ASSERT_FALSE(p.has_error()) << p.error();
+  ASSERT_NE(e, nullptr);
+  ASSERT_TRUE(e->IsVariable());
+}
+
+TEST_F(ParserImplTest, VariableStmt_Const_InvalidVarIdent) {
+  ParserImpl p{"const a : invalid = 1"};
+  auto e = p.variable_stmt();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:11: unknown type alias 'invalid'");
+}
+
+TEST_F(ParserImplTest, VariableStmt_Const_MissingEqual) {
+  ParserImpl p{"const a : i32 1"};
+  auto e = p.variable_stmt();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:15: missing = for constant declaration");
+}
+
+TEST_F(ParserImplTest, VariableStmt_Const_MissingInitializer) {
+  ParserImpl p{"const a : i32 ="};
+  auto e = p.variable_stmt();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:16: missing initializer for const declaration");
+}
+
+TEST_F(ParserImplTest, VariableStmt_Const_InvalidInitializer) {
+  ParserImpl p{"const a : i32 = if (a) {}"};
+  auto e = p.variable_stmt();
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(e, nullptr);
+  EXPECT_EQ(p.error(), "1:17: missing initializer for const declaration");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_variable_storage_decoration_test.cc b/src/reader/wgsl/parser_impl_variable_storage_decoration_test.cc
new file mode 100644
index 0000000..04963d7
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_variable_storage_decoration_test.cc
@@ -0,0 +1,98 @@
+// 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 "gtest/gtest.h"
+#include "src/ast/storage_class.h"
+#include "src/reader/wgsl/parser_impl.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserImplTest = testing::Test;
+
+struct VariableStorageData {
+  const char* input;
+  ast::StorageClass result;
+};
+inline std::ostream& operator<<(std::ostream& out, VariableStorageData data) {
+  out << std::string(data.input);
+  return out;
+}
+using VariableStorageTest = testing::TestWithParam<VariableStorageData>;
+TEST_P(VariableStorageTest, Parses) {
+  auto params = GetParam();
+  ParserImpl p{std::string("<") + params.input + ">"};
+
+  auto sc = p.variable_storage_decoration();
+  ASSERT_FALSE(p.has_error());
+  EXPECT_EQ(sc, params.result);
+
+  auto t = p.next();
+  EXPECT_TRUE(t.IsEof());
+}
+INSTANTIATE_TEST_SUITE_P(
+    ParserImplTest,
+    VariableStorageTest,
+    testing::Values(
+        VariableStorageData{"in", ast::StorageClass::kInput},
+        VariableStorageData{"out", ast::StorageClass::kOutput},
+        VariableStorageData{"uniform", ast::StorageClass::kUniform},
+        VariableStorageData{"workgroup", ast::StorageClass::kWorkgroup},
+        VariableStorageData{"uniform_constant",
+                            ast::StorageClass::kUniformConstant},
+        VariableStorageData{"storage_buffer",
+                            ast::StorageClass::kStorageBuffer},
+        VariableStorageData{"image", ast::StorageClass::kImage},
+        VariableStorageData{"push_constant", ast::StorageClass::kPushConstant},
+        VariableStorageData{"private", ast::StorageClass::kPrivate},
+        VariableStorageData{"function", ast::StorageClass::kFunction}));
+
+TEST_F(ParserImplTest, VariableStorageDecoration_NoMatch) {
+  ParserImpl p{"<not-a-storage-class>"};
+  auto sc = p.variable_storage_decoration();
+  ASSERT_EQ(sc, ast::StorageClass::kNone);
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(p.error(), "1:2: invalid storage class for variable decoration");
+}
+
+TEST_F(ParserImplTest, VariableStorageDecoration_Empty) {
+  ParserImpl p{"<>"};
+  auto sc = p.variable_storage_decoration();
+  ASSERT_EQ(sc, ast::StorageClass::kNone);
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(p.error(), "1:2: invalid storage class for variable decoration");
+}
+
+TEST_F(ParserImplTest, VariableStorageDecoration_MissingLessThan) {
+  ParserImpl p{"in>"};
+  auto sc = p.variable_storage_decoration();
+  ASSERT_EQ(sc, ast::StorageClass::kNone);
+  ASSERT_FALSE(p.has_error());
+
+  auto t = p.next();
+  ASSERT_TRUE(t.IsIn());
+}
+
+TEST_F(ParserImplTest, VariableStorageDecoration_MissingGreaterThan) {
+  ParserImpl p{"<in"};
+  auto sc = p.variable_storage_decoration();
+  ASSERT_EQ(sc, ast::StorageClass::kNone);
+  ASSERT_TRUE(p.has_error());
+  ASSERT_EQ(p.error(), "1:4: missing > for variable decoration");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_test.cc b/src/reader/wgsl/parser_test.cc
new file mode 100644
index 0000000..6ef3016
--- /dev/null
+++ b/src/reader/wgsl/parser_test.cc
@@ -0,0 +1,63 @@
+// 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/reader/wgsl/parser.h"
+
+#include "gtest/gtest.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using ParserTest = testing::Test;
+
+TEST_F(ParserTest, Empty) {
+  Parser p{""};
+  ASSERT_TRUE(p.Parse()) << p.error();
+}
+
+TEST_F(ParserTest, DISABLED_Parses) {
+  Parser p{R"(
+import "GLSL.std.430" as glsl;
+
+[[location 0]] var<out> gl_FragColor : vec4<f32>;
+
+fn main() -> void {
+  gl_FragColor = vec4<f32>(.4, .2, .3, 1);
+}
+)"};
+  ASSERT_TRUE(p.Parse()) << p.error();
+
+  auto m = p.module();
+  ASSERT_EQ(1, m.imports().size());
+
+  // TODO(dsinclair) check rest of AST ...
+}
+
+TEST_F(ParserTest, DISABLED_HandlesError) {
+  Parser p{R"(
+import "GLSL.std.430" as glsl;
+
+fn main() ->  {  # missing return type
+  return;
+})"};
+
+  ASSERT_FALSE(p.Parse());
+  ASSERT_TRUE(p.has_error());
+  EXPECT_EQ(p.error(), "4:15: missing return type for function");
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/token.cc b/src/reader/wgsl/token.cc
new file mode 100644
index 0000000..fbbc1aa
--- /dev/null
+++ b/src/reader/wgsl/token.cc
@@ -0,0 +1,344 @@
+// 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/reader/wgsl/token.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+// static
+std::string Token::TypeToName(Type type) {
+  switch (type) {
+    case Token::Type::kError:
+      return "kError";
+    case Token::Type::kReservedKeyword:
+      return "kReservedKeyword";
+    case Token::Type::kEOF:
+      return "kEOF";
+    case Token::Type::kIdentifier:
+      return "kIdentifier";
+    case Token::Type::kStringLiteral:
+      return "kStringLiteral";
+    case Token::Type::kFloatLiteral:
+      return "kFloatLiteral";
+    case Token::Type::kIntLiteral:
+      return "kIntLiteral";
+    case Token::Type::kUintLiteral:
+      return "kUintLiteral";
+    case Token::Type::kUninitialized:
+      return "kUninitialized";
+
+    case Token::Type::kAnd:
+      return "&";
+    case Token::Type::kAndAnd:
+      return "&&";
+    case Token::Type::kArrow:
+      return "->";
+    case Token::Type::kAttrLeft:
+      return "[[";
+    case Token::Type::kAttrRight:
+      return "]]";
+    case Token::Type::kForwardSlash:
+      return "/";
+    case Token::Type::kBang:
+      return "!";
+    case Token::Type::kBraceLeft:
+      return "[";
+    case Token::Type::kBraceRight:
+      return "]";
+    case Token::Type::kBracketLeft:
+      return "{";
+    case Token::Type::kBracketRight:
+      return "}";
+    case Token::Type::kColon:
+      return ":";
+    case Token::Type::kComma:
+      return ",";
+    case Token::Type::kEqual:
+      return "=";
+    case Token::Type::kEqualEqual:
+      return "==";
+    case Token::Type::kGreaterThan:
+      return ">";
+    case Token::Type::kGreaterThanEqual:
+      return ">=";
+    case Token::Type::kLessThan:
+      return "<";
+    case Token::Type::kLessThanEqual:
+      return "<=";
+    case Token::Type::kMod:
+      return "%";
+    case Token::Type::kNotEqual:
+      return "!=";
+    case Token::Type::kMinus:
+      return "-";
+    case Token::Type::kNamespace:
+      return "::";
+    case Token::Type::kPeriod:
+      return ".";
+    case Token::Type::kPlus:
+      return "+";
+    case Token::Type::kOr:
+      return "|";
+    case Token::Type::kOrOr:
+      return "||";
+    case Token::Type::kParenLeft:
+      return "(";
+    case Token::Type::kParenRight:
+      return ")";
+    case Token::Type::kSemicolon:
+      return ";";
+    case Token::Type::kStar:
+      return "*";
+    case Token::Type::kXor:
+      return "^";
+
+    case Token::Type::kAll:
+      return "all";
+    case Token::Type::kAny:
+      return "any";
+    case Token::Type::kArray:
+      return "array";
+    case Token::Type::kAs:
+      return "as";
+    case Token::Type::kBinding:
+      return "binding";
+    case Token::Type::kBlock:
+      return "block";
+    case Token::Type::kBool:
+      return "bool";
+    case Token::Type::kBreak:
+      return "break";
+    case Token::Type::kBuiltin:
+      return "builtin";
+    case Token::Type::kCase:
+      return "case";
+    case Token::Type::kCast:
+      return "cast";
+    case Token::Type::kCompute:
+      return "compute";
+    case Token::Type::kConst:
+      return "const";
+    case Token::Type::kContinue:
+      return "continue";
+    case Token::Type::kContinuing:
+      return "continuing";
+    case Token::Type::kCoarse:
+      return "coarse";
+    case Token::Type::kDefault:
+      return "default";
+    case Token::Type::kDot:
+      return "dot";
+    case Token::Type::kDpdx:
+      return "dpdx";
+    case Token::Type::kDpdy:
+      return "dpdy";
+    case Token::Type::kElse:
+      return "else";
+    case Token::Type::kElseIf:
+      return "elseif";
+    case Token::Type::kEntryPoint:
+      return "entry_point";
+    case Token::Type::kF32:
+      return "f32";
+    case Token::Type::kFallthrough:
+      return "fallthrough";
+    case Token::Type::kFalse:
+      return "false";
+    case Token::Type::kFine:
+      return "fine";
+    case Token::Type::kFn:
+      return "fn";
+    case Token::Type::kFragCoord:
+      return "frag_coord";
+    case Token::Type::kFragDepth:
+      return "frag_depth";
+    case Token::Type::kFragment:
+      return "fragment";
+    case Token::Type::kFrontFacing:
+      return "front_facing";
+    case Token::Type::kFunction:
+      return "function";
+    case Token::Type::kFwidth:
+      return "fwidth";
+    case Token::Type::kGlobalInvocationId:
+      return "global_invocation_id";
+    case Token::Type::kI32:
+      return "i32";
+    case Token::Type::kIf:
+      return "if";
+    case Token::Type::kImage:
+      return "image";
+    case Token::Type::kImport:
+      return "import";
+    case Token::Type::kIn:
+      return "in";
+    case Token::Type::kInstanceIdx:
+      return "instance_idx";
+    case Token::Type::kIsNan:
+      return "is_nan";
+    case Token::Type::kIsInf:
+      return "is_inf";
+    case Token::Type::kIsFinite:
+      return "is_finite";
+    case Token::Type::kIsNormal:
+      return "is_normal";
+    case Token::Type::kKill:
+      return "kill";
+    case Token::Type::kLocalInvocationId:
+      return "local_invocation_id";
+    case Token::Type::kLocalInvocationIdx:
+      return "local_invocation_idx";
+    case Token::Type::kLocation:
+      return "location";
+    case Token::Type::kLoop:
+      return "loop";
+    case Token::Type::kMat2x2:
+      return "mat2x2";
+    case Token::Type::kMat2x3:
+      return "mat2x3";
+    case Token::Type::kMat2x4:
+      return "mat2x4";
+    case Token::Type::kMat3x2:
+      return "mat3x2";
+    case Token::Type::kMat3x3:
+      return "mat3x3";
+    case Token::Type::kMat3x4:
+      return "mat3x4";
+    case Token::Type::kMat4x2:
+      return "mat4x2";
+    case Token::Type::kMat4x3:
+      return "mat4x3";
+    case Token::Type::kMat4x4:
+      return "mat4x4";
+    case Token::Type::kNop:
+      return "nop";
+    case Token::Type::kNumWorkgroups:
+      return "num_workgroups";
+    case Token::Type::kOffset:
+      return "offset";
+    case Token::Type::kOut:
+      return "out";
+    case Token::Type::kOuterProduct:
+      return "outer_product";
+    case Token::Type::kPosition:
+      return "position";
+    case Token::Type::kPremerge:
+      return "premerge";
+    case Token::Type::kPrivate:
+      return "private";
+    case Token::Type::kPtr:
+      return "ptr";
+    case Token::Type::kPushConstant:
+      return "push_constant";
+    case Token::Type::kRegardless:
+      return "regardless";
+    case Token::Type::kReturn:
+      return "return";
+    case Token::Type::kSet:
+      return "set";
+    case Token::Type::kStorageBuffer:
+      return "storage_buffer";
+    case Token::Type::kStruct:
+      return "struct";
+    case Token::Type::kSwitch:
+      return "switch";
+    case Token::Type::kTrue:
+      return "true";
+    case Token::Type::kType:
+      return "type";
+    case Token::Type::kU32:
+      return "u32";
+    case Token::Type::kUniform:
+      return "uniform";
+    case Token::Type::kUniformConstant:
+      return "uniform_constant";
+    case Token::Type::kUnless:
+      return "unless";
+    case Token::Type::kVar:
+      return "var";
+    case Token::Type::kVec2:
+      return "vec2";
+    case Token::Type::kVec3:
+      return "vec3";
+    case Token::Type::kVec4:
+      return "vec4";
+    case Token::Type::kVertex:
+      return "vertex";
+    case Token::Type::kVertexIdx:
+      return "vertex_idx";
+    case Token::Type::kVoid:
+      return "void";
+    case Token::Type::kWorkgroup:
+      return "workgroup";
+    case Token::Type::kWorkgroupSize:
+      return "workgroup_size";
+  }
+
+  return "<unknown>";
+}
+
+Token::Token() : type_(Type::kUninitialized) {}
+
+Token::Token(Type type, const Source& source, const std::string& val)
+    : type_(type), source_(source), val_str_(val) {}
+
+Token::Token(const Source& source, uint32_t val)
+    : type_(Type::kUintLiteral), source_(source), val_uint_(val) {}
+
+Token::Token(const Source& source, int32_t val)
+    : type_(Type::kIntLiteral), source_(source), val_int_(val) {}
+
+Token::Token(const Source& source, float val)
+    : type_(Type::kFloatLiteral), source_(source), val_float_(val) {}
+
+Token::Token(Type type, const Source& source) : Token(type, source, "") {}
+
+Token::Token(Token&&) = default;
+
+Token::Token(const Token&) = default;
+
+Token::~Token() = default;
+
+Token& Token::operator=(const Token&) = default;
+
+std::string Token::to_str() const {
+  if (type_ == Type::kFloatLiteral) {
+    return std::to_string(val_float_);
+  }
+  if (type_ == Type::kIntLiteral) {
+    return std::to_string(val_int_);
+  }
+  if (type_ == Type::kUintLiteral) {
+    return std::to_string(val_uint_);
+  }
+  return val_str_;
+}
+
+float Token::to_f32() const {
+  return val_float_;
+}
+
+uint32_t Token::to_u32() const {
+  return val_uint_;
+}
+
+int32_t Token::to_i32() const {
+  return val_int_;
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/token.h b/src/reader/wgsl/token.h
new file mode 100644
index 0000000..ad77a80
--- /dev/null
+++ b/src/reader/wgsl/token.h
@@ -0,0 +1,667 @@
+// 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.
+
+#ifndef SRC_READER_WGSL_TOKEN_H_
+#define SRC_READER_WGSL_TOKEN_H_
+
+#include <stddef.h>
+
+#include <ostream>
+#include <string>
+
+#include "src/source.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+/// Stores tokens generated by the Lexer
+class Token {
+ public:
+  /// The type of the parsed token
+  enum class Type {
+    /// Error result
+    kError = -2,
+    /// Reserved keyword
+    kReservedKeyword = -1,
+    /// Uninitialized token
+    kUninitialized = 0,
+    /// End of input string reached
+    kEOF,
+
+    /// An identifier
+    kIdentifier,
+    /// A string value
+    kStringLiteral,
+    /// A float value
+    kFloatLiteral,
+    /// An int value
+    kIntLiteral,
+    /// A uint value
+    kUintLiteral,
+
+    /// A '&'
+    kAnd,
+    /// A '&&'
+    kAndAnd,
+    /// A '->'
+    kArrow,
+    /// A '[['
+    kAttrLeft,
+    /// A ']]'
+    kAttrRight,
+    /// A '/'
+    kForwardSlash,
+    /// A '!'
+    kBang,
+    /// A '['
+    kBraceLeft,
+    /// A ']'
+    kBraceRight,
+    /// A '{'
+    kBracketLeft,
+    /// A '}'
+    kBracketRight,
+    /// A ':'
+    kColon,
+    /// A ','
+    kComma,
+    /// A '='
+    kEqual,
+    /// A '=='
+    kEqualEqual,
+    /// A '>'
+    kGreaterThan,
+    /// A '>='
+    kGreaterThanEqual,
+    /// A '<'
+    kLessThan,
+    /// A '<='
+    kLessThanEqual,
+    /// A '%'
+    kMod,
+    /// A '-'
+    kMinus,
+    /// A '::'
+    kNamespace,
+    /// A '!='
+    kNotEqual,
+    /// A '.'
+    kPeriod,
+    /// A '+'
+    kPlus,
+    /// A '|'
+    kOr,
+    /// A '||'
+    kOrOr,
+    /// A '('
+    kParenLeft,
+    /// A ')'
+    kParenRight,
+    /// A ';'
+    kSemicolon,
+    /// A '*'
+    kStar,
+    /// A '^'
+    kXor,
+
+    /// A 'all'
+    kAll,
+    /// A 'any'
+    kAny,
+    /// A 'array'
+    kArray,
+    /// A 'as'
+    kAs,
+    /// A 'binding'
+    kBinding,
+    /// A 'bool'
+    kBool,
+    /// A 'block'
+    kBlock,
+    /// A 'break'
+    kBreak,
+    /// A 'builtin'
+    kBuiltin,
+    /// A 'case'
+    kCase,
+    /// A 'cast'
+    kCast,
+    /// A 'compute'
+    kCompute,
+    /// A 'const'
+    kConst,
+    /// A 'continue'
+    kContinue,
+    /// A 'continuing'
+    kContinuing,
+    /// A 'coarse'
+    kCoarse,
+    /// A 'default'
+    kDefault,
+    /// A 'dot'
+    kDot,
+    /// A 'dpdx'
+    kDpdx,
+    /// A 'dpdy'
+    kDpdy,
+    /// A 'else'
+    kElse,
+    /// A 'elseif'
+    kElseIf,
+    /// A 'entry_point'
+    kEntryPoint,
+    /// A 'f32'
+    kF32,
+    /// A 'fallthrough'
+    kFallthrough,
+    /// A 'false'
+    kFalse,
+    /// A 'fine'
+    kFine,
+    /// A 'fn'
+    kFn,
+    /// A 'frag_coord'
+    kFragCoord,
+    // A 'frag_depth'
+    kFragDepth,
+    /// A 'fragment'
+    kFragment,
+    /// A 'front_facing'
+    kFrontFacing,
+    /// A 'function'
+    kFunction,
+    /// A 'fwidth'
+    kFwidth,
+    /// A 'global_invocation_id'
+    kGlobalInvocationId,
+    /// A 'i32'
+    kI32,
+    /// A 'if'
+    kIf,
+    /// A 'image'
+    kImage,
+    /// A 'import'
+    kImport,
+    /// A 'in'
+    kIn,
+    /// A 'instance_idx'
+    kInstanceIdx,
+    /// A 'is_nan'
+    kIsNan,
+    /// A 'is_inf'
+    kIsInf,
+    /// A 'is_finite'
+    kIsFinite,
+    /// A 'is_normal'
+    kIsNormal,
+    /// A 'kill'
+    kKill,
+    /// A 'local_invocation_id'
+    kLocalInvocationId,
+    /// A 'local_invocation_idx'
+    kLocalInvocationIdx,
+    /// A 'location'
+    kLocation,
+    /// A 'loop'
+    kLoop,
+    /// A 'mat2x2'
+    kMat2x2,
+    /// A 'mat2x3'
+    kMat2x3,
+    /// A 'mat2x4'
+    kMat2x4,
+    /// A 'mat3x2'
+    kMat3x2,
+    /// A 'mat3x3'
+    kMat3x3,
+    /// A 'mat3x4'
+    kMat3x4,
+    /// A 'mat4x2'
+    kMat4x2,
+    /// A 'mat4x3'
+    kMat4x3,
+    /// A 'mat4x4'
+    kMat4x4,
+    /// A 'nop'
+    kNop,
+    /// A 'num_workgroups'
+    kNumWorkgroups,
+    /// A 'offset'
+    kOffset,
+    /// A 'out'
+    kOut,
+    /// A 'outer_product'
+    kOuterProduct,
+    /// A 'position'
+    kPosition,
+    /// A 'premerge'
+    kPremerge,
+    /// A 'private'
+    kPrivate,
+    /// A 'ptr'
+    kPtr,
+    /// A 'push_constant'
+    kPushConstant,
+    /// A 'regardless'
+    kRegardless,
+    /// A 'return'
+    kReturn,
+    /// A 'set'
+    kSet,
+    /// A 'storage_buffer'
+    kStorageBuffer,
+    /// A 'struct'
+    kStruct,
+    /// A 'switch'
+    kSwitch,
+    /// A 'true'
+    kTrue,
+    /// A 'type'
+    kType,
+    /// A 'u32'
+    kU32,
+    /// A 'uniform'
+    kUniform,
+    /// A 'uniform_constant'
+    kUniformConstant,
+    /// A 'unless'
+    kUnless,
+    /// A 'var'
+    kVar,
+    /// A 'vec2'
+    kVec2,
+    /// A 'vec3'
+    kVec3,
+    /// A 'vec4'
+    kVec4,
+    /// A 'vertex'
+    kVertex,
+    /// A 'vertex_idx'
+    kVertexIdx,
+    /// A 'void'
+    kVoid,
+    /// A 'workgroup'
+    kWorkgroup,
+    /// A 'workgroup_size'
+    kWorkgroupSize
+  };
+
+  /// Converts a token type to a name
+  /// @param type the type to convert
+  /// @returns the token type as as string
+  static std::string TypeToName(Type type);
+
+  /// Creates an uninitialized token
+  Token();
+  /// Create a Token
+  /// @param type the Token::Type of the token
+  /// @param source the source of the token
+  Token(Type type, const Source& source);
+
+  /// Create a string Token
+  /// @param type the Token::Type of the token
+  /// @param source the source of the token
+  /// @param val the source string for the token
+  Token(Type type, const Source& source, const std::string& val);
+  /// Create a unsigned integer Token
+  /// @param source the source of the token
+  /// @param val the source unsigned for the token
+  Token(const Source& source, uint32_t val);
+  /// Create a signed integer Token
+  /// @param source the source of the token
+  /// @param val the source integer for the token
+  Token(const Source& source, int32_t val);
+  /// Create a float Token
+  /// @param source the source of the token
+  /// @param val the source float for the token
+  Token(const Source& source, float 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);
+
+  /// Returns true if the token is of the given type
+  /// @param t the type to check against.
+  /// @returns true if the token is of type |t|
+  bool Is(Type t) const { return type_ == t; }
+
+  /// @returns true if the token is uninitialized
+  bool IsUninitialized() const { return type_ == Type::kUninitialized; }
+  /// @returns true if the token is reserved
+  bool IsReservedKeyword() const { return type_ == Type::kReservedKeyword; }
+  /// @returns true if the token is an error
+  bool IsError() const { return type_ == Type::kError; }
+  /// @returns true if the token is EOF
+  bool IsEof() const { return type_ == Type::kEOF; }
+  /// @returns true if the token is an identifier
+  bool IsIdentifier() const { return type_ == Type::kIdentifier; }
+  /// @returns true if the token is a string
+  bool IsStringLiteral() const { return type_ == Type::kStringLiteral; }
+  /// @returns true if the token is a float
+  bool IsFloatLiteral() const { return type_ == Type::kFloatLiteral; }
+  /// @returns true if the token is an int
+  bool IsIntLiteral() const { return type_ == Type::kIntLiteral; }
+  /// @returns true if the token is a unsigned int
+  bool IsUintLiteral() const { return type_ == Type::kUintLiteral; }
+
+  /// @returns true if token is a '&'
+  bool IsAnd() const { return type_ == Type::kAnd; }
+  /// @returns true if token is a '&&'
+  bool IsAndAnd() const { return type_ == Type::kAndAnd; }
+  /// @returns true if token is a '->'
+  bool IsArrow() const { return type_ == Type::kArrow; }
+  /// @returns true if token is a '[['
+  bool IsAttrLeft() const { return type_ == Type::kAttrLeft; }
+  /// @returns true if token is a ']]'
+  bool IsAttrRight() const { return type_ == Type::kAttrRight; }
+  /// @returns true if token is a '/'
+  bool IsForwardSlash() const { return type_ == Type::kForwardSlash; }
+  /// @returns true if token is a '!'
+  bool IsBang() const { return type_ == Type::kBang; }
+  /// @returns true if token is a '['
+  bool IsBraceLeft() const { return type_ == Type::kBraceLeft; }
+  /// @returns true if token is a ']'
+  bool IsBraceRight() const { return type_ == Type::kBraceRight; }
+  /// @returns true if token is a '{'
+  bool IsBracketLeft() const { return type_ == Type::kBracketLeft; }
+  /// @returns true if token is a '}'
+  bool IsBracketRight() const { return type_ == Type::kBracketRight; }
+  /// @returns true if token is a ':'
+  bool IsColon() const { return type_ == Type::kColon; }
+  /// @returns true if token is a ','
+  bool IsComma() const { return type_ == Type::kComma; }
+  /// @returns true if token is a '='
+  bool IsEqual() const { return type_ == Type::kEqual; }
+  /// @returns true if token is a '=='
+  bool IsEqualEqual() const { return type_ == Type::kEqualEqual; }
+  /// @returns true if token is a '>'
+  bool IsGreaterThan() const { return type_ == Type::kGreaterThan; }
+  /// @returns true if token is a '>='
+  bool IsGreaterThanEqual() const { return type_ == Type::kGreaterThanEqual; }
+  /// @returns true if token is a '<'
+  bool IsLessThan() const { return type_ == Type::kLessThan; }
+  /// @returns true if token is a '<='
+  bool IsLessThanEqual() const { return type_ == Type::kLessThanEqual; }
+  /// @returns true if token is a '%'
+  bool IsMod() const { return type_ == Type::kMod; }
+  /// @returns true if token is a '-'
+  bool IsMinus() const { return type_ == Type::kMinus; }
+  /// @returns true if token is a '::'
+  bool IsNamespace() const { return type_ == Type::kNamespace; }
+  /// @returns true if token is a '!='
+  bool IsNotEqual() const { return type_ == Type::kNotEqual; }
+  /// @returns true if token is a '.'
+  bool IsPeriod() const { return type_ == Type::kPeriod; }
+  /// @returns true if token is a '+'
+  bool IsPlus() const { return type_ == Type::kPlus; }
+  /// @returns true if token is a '|'
+  bool IsOr() const { return type_ == Type::kOr; }
+  /// @returns true if token is a '||'
+  bool IsOrOr() const { return type_ == Type::kOrOr; }
+  /// @returns true if token is a '('
+  bool IsParenLeft() const { return type_ == Type::kParenLeft; }
+  /// @returns true if token is a ')'
+  bool IsParenRight() const { return type_ == Type::kParenRight; }
+  /// @returns true if token is a ';'
+  bool IsSemicolon() const { return type_ == Type::kSemicolon; }
+  /// @returns true if token is a '*'
+  bool IsStar() const { return type_ == Type::kStar; }
+  /// @returns true if token is a '^'
+  bool IsXor() const { return type_ == Type::kXor; }
+
+  /// @returns true if token is a 'all'
+  bool IsAll() const { return type_ == Type::kAll; }
+  /// @returns true if token is a 'any'
+  bool IsAny() const { return type_ == Type::kAny; }
+  /// @returns true if token is a 'array'
+  bool IsArray() const { return type_ == Type::kArray; }
+  /// @returns true if token is a 'as'
+  bool IsAs() const { return type_ == Type::kAs; }
+  /// @returns true if token is a 'binding'
+  bool IsBinding() const { return type_ == Type::kBinding; }
+  /// @returns true if token is a 'block'
+  bool IsBlock() const { return type_ == Type::kBlock; }
+  /// @returns true if token is a 'bool'
+  bool IsBool() const { return type_ == Type::kBool; }
+  /// @returns true if token is a 'break'
+  bool IsBreak() const { return type_ == Type::kBreak; }
+  /// @returns true if token is a 'builtin'
+  bool IsBuiltin() const { return type_ == Type::kBuiltin; }
+  /// @returns true if token is a 'case'
+  bool IsCase() const { return type_ == Type::kCase; }
+  /// @returns true if token is a 'cast'
+  bool IsCast() const { return type_ == Type::kCast; }
+  /// @returns true if token is 'coarse'
+  bool IsCoarse() const { return type_ == Type::kCoarse; }
+  /// @returns true if token is a 'compute'
+  bool IsCompute() const { return type_ == Type::kCompute; }
+  /// @returns true if token is a 'const'
+  bool IsConst() const { return type_ == Type::kConst; }
+  /// @returns true if token is a 'continue'
+  bool IsContinue() const { return type_ == Type::kContinue; }
+  /// @returns true if token is a 'continuing'
+  bool IsContinuing() const { return type_ == Type::kContinuing; }
+  /// @returns true if token is a 'default'
+  bool IsDefault() const { return type_ == Type::kDefault; }
+  /// @returns true if token is a 'dot'
+  bool IsDot() const { return type_ == Type::kDot; }
+  /// @returns true if token is a 'dpdx'
+  bool IsDpdx() const { return type_ == Type::kDpdx; }
+  /// @returns true if token is a 'dpdy'
+  bool IsDpdy() const { return type_ == Type::kDpdy; }
+  /// @returns true if token is a 'else'
+  bool IsElse() const { return type_ == Type::kElse; }
+  /// @returns true if token is a 'elseif'
+  bool IsElseIf() const { return type_ == Type::kElseIf; }
+  /// @returns true if token is a 'entry_point'
+  bool IsEntryPoint() const { return type_ == Type::kEntryPoint; }
+  /// @returns true if token is a 'f32'
+  bool IsF32() const { return type_ == Type::kF32; }
+  /// @returns true if token is a 'fallthrough'
+  bool IsFallthrough() const { return type_ == Type::kFallthrough; }
+  /// @returns true if token is a 'false'
+  bool IsFalse() const { return type_ == Type::kFalse; }
+  /// @returns true if token is a 'fine'
+  bool IsFine() const { return type_ == Type::kFine; }
+  /// @returns true if token is a 'fn'
+  bool IsFn() const { return type_ == Type::kFn; }
+  /// @returns true if token is a 'frag_coord'
+  bool IsFragCoord() const { return type_ == Type::kFragCoord; }
+  /// @returns true if token is a 'frag_depth'
+  bool IsFragDepth() const { return type_ == Type::kFragDepth; }
+  /// @returns true if token is a 'fragment'
+  bool IsFragment() const { return type_ == Type::kFragment; }
+  /// @returns true if token is a 'front_facing'
+  bool IsFrontFacing() const { return type_ == Type::kFrontFacing; }
+  /// @returns true if token is a 'function'
+  bool IsFunction() const { return type_ == Type::kFunction; }
+  /// @returns true if token is a 'fwidth'
+  bool IsFwidth() const { return type_ == Type::kFwidth; }
+  /// @returns true if token is a 'global_invocation_id'
+  bool IsGlobalInvocationId() const {
+    return type_ == Type::kGlobalInvocationId;
+  }
+  /// @returns true if token is a 'i32'
+  bool IsI32() const { return type_ == Type::kI32; }
+  /// @returns true if token is a 'if'
+  bool IsIf() const { return type_ == Type::kIf; }
+  /// @returns true if token is a 'image'
+  bool IsImage() const { return type_ == Type::kImage; }
+  /// @returns true if token is a 'import'
+  bool IsImport() const { return type_ == Type::kImport; }
+  /// @returns true if token is a 'in'
+  bool IsIn() const { return type_ == Type::kIn; }
+  /// @returns true if token is a 'instance_idx'
+  bool IsInstanceIdx() const { return type_ == Type::kInstanceIdx; }
+  /// @returns true if token is a 'is_nan'
+  bool IsIsNan() const { return type_ == Type::kIsNan; }
+  /// @returns true if token is a 'is_inf'
+  bool IsIsInf() const { return type_ == Type::kIsInf; }
+  /// @returns true if token is a 'is_finite'
+  bool IsIsFinite() const { return type_ == Type::kIsFinite; }
+  /// @returns true if token is a 'is_normal'
+  bool IsIsNormal() const { return type_ == Type::kIsNormal; }
+  /// @returns true if token is a 'kill'
+  bool IsKill() const { return type_ == Type::kKill; }
+  /// @returns true if token is a 'local_invocation_id'
+  bool IsLocalInvocationId() const { return type_ == Type::kLocalInvocationId; }
+  /// @returns true if token is a 'local_invocation_idx'
+  bool IsLocalInvocationIdx() const {
+    return type_ == Type::kLocalInvocationIdx;
+  }
+  /// @returns true if token is a 'location'
+  bool IsLocation() const { return type_ == Type::kLocation; }
+  /// @returns true if token is a 'loop'
+  bool IsLoop() const { return type_ == Type::kLoop; }
+  /// @returns true if token is a 'mat2x2'
+  bool IsMat2x2() const { return type_ == Type::kMat2x2; }
+  /// @returns true if token is a 'mat2x3'
+  bool IsMat2x3() const { return type_ == Type::kMat2x3; }
+  /// @returns true if token is a 'mat2x4'
+  bool IsMat2x4() const { return type_ == Type::kMat2x4; }
+  /// @returns true if token is a 'mat3x2'
+  bool IsMat3x2() const { return type_ == Type::kMat3x2; }
+  /// @returns true if token is a 'mat3x3'
+  bool IsMat3x3() const { return type_ == Type::kMat3x3; }
+  /// @returns true if token is a 'mat3x4'
+  bool IsMat3x4() const { return type_ == Type::kMat3x4; }
+  /// @returns true if token is a 'mat4x2'
+  bool IsMat4x2() const { return type_ == Type::kMat4x2; }
+  /// @returns true if token is a 'mat4x3'
+  bool IsMat4x3() const { return type_ == Type::kMat4x3; }
+  /// @returns true if token is a 'mat4x4'
+  bool IsMat4x4() const { return type_ == Type::kMat4x4; }
+  /// @returns true if token is a 'nop'
+  bool IsNop() const { return type_ == Type::kNop; }
+  /// @returns true if token is a 'num_workgroups'
+  bool IsNumWorkgroups() const { return type_ == Type::kNumWorkgroups; }
+  /// @returns true if token is a 'offset'
+  bool IsOffset() const { return type_ == Type::kOffset; }
+  /// @returns true if token is a 'out'
+  bool IsOut() const { return type_ == Type::kOut; }
+  /// @returns true if token is a 'outer_product'
+  bool IsOuterProduct() const { return type_ == Type::kOuterProduct; }
+  /// @returns true if token is a 'position'
+  bool IsPosition() const { return type_ == Type::kPosition; }
+  /// @returns true if token is a 'premerge'
+  bool IsPremerge() const { return type_ == Type::kPremerge; }
+  /// @returns true if token is a 'private'
+  bool IsPrivate() const { return type_ == Type::kPrivate; }
+  /// @returns true if token is a 'ptr'
+  bool IsPtr() const { return type_ == Type::kPtr; }
+  /// @returns true if token is a 'push_constant'
+  bool IsPushConstant() const { return type_ == Type::kPushConstant; }
+  /// @returns true if token is a 'regardless'
+  bool IsRegardless() const { return type_ == Type::kRegardless; }
+  /// @returns true if token is a 'return'
+  bool IsReturn() const { return type_ == Type::kReturn; }
+  /// @returns true if token is a 'set'
+  bool IsSet() const { return type_ == Type::kSet; }
+  /// @returns true if token is a 'storage_buffer'
+  bool IsStorageBuffer() const { return type_ == Type::kStorageBuffer; }
+  /// @returns true if token is a 'struct'
+  bool IsStruct() const { return type_ == Type::kStruct; }
+  /// @returns true if token is a 'switch'
+  bool IsSwitch() const { return type_ == Type::kSwitch; }
+  /// @returns true if token is a 'true'
+  bool IsTrue() const { return type_ == Type::kTrue; }
+  /// @returns true if token is a 'type'
+  bool IsType() const { return type_ == Type::kType; }
+  /// @returns true if token is a 'u32'
+  bool IsU32() const { return type_ == Type::kU32; }
+  /// @returns true if token is a 'uniform'
+  bool IsUniform() const { return type_ == Type::kUniform; }
+  /// @returns true if token is a 'uniform_constant'
+  bool IsUniformConstant() const { return type_ == Type::kUniformConstant; }
+  /// @returns true if token is a 'unless'
+  bool IsUnless() const { return type_ == Type::kUnless; }
+  /// @returns true if token is a 'var'
+  bool IsVar() const { return type_ == Type::kVar; }
+  /// @returns true if token is a 'vec2'
+  bool IsVec2() const { return type_ == Type::kVec2; }
+  /// @returns true if token is a 'vec3'
+  bool IsVec3() const { return type_ == Type::kVec3; }
+  /// @returns true if token is a 'vec4'
+  bool IsVec4() const { return type_ == Type::kVec4; }
+  /// @returns true if token is a 'vertex'
+  bool IsVertex() const { return type_ == Type::kVertex; }
+  /// @returns true if token is a 'vertex_idx'
+  bool IsVertexIdx() const { return type_ == Type::kVertexIdx; }
+  /// @returns true if token is a 'void'
+  bool IsVoid() const { return type_ == Type::kVoid; }
+  /// @returns true if token is a 'workgroup'
+  bool IsWorkgroup() const { return type_ == Type::kWorkgroup; }
+  /// @returns true if token is a 'workgroup_size'
+  bool IsWorkgroupSize() const { return type_ == Type::kWorkgroupSize; }
+
+  /// @returns the source line of the token
+  size_t line() const { return source_.line; }
+  /// @returns the source column of the token
+  size_t column() const { return source_.column; }
+  /// @returns the source information for this token
+  Source source() const { return source_; }
+
+  /// Returns the string value of the token
+  /// @return const std::string&
+  std::string to_str() const;
+  /// Returns the float value of the token. 0 is returned if the token does not
+  /// contain a float value.
+  /// @return float
+  float to_f32() const;
+  /// Returns the uint32 value of the token. 0 is returned if the token does not
+  /// contain a unsigned integer value.
+  /// @return uint32_t
+  uint32_t to_u32() const;
+  /// Returns the int32 value of the token. 0 is returned if the token does not
+  /// contain a signed integer value.
+  /// @return int32_t
+  int32_t to_i32() const;
+
+  /// @returns the token type as string
+  std::string to_name() const { return Token::TypeToName(type_); }
+
+ private:
+  /// The Token::Type of the token
+  Type type_ = Type::kError;
+  /// The source where the token appeared
+  Source source_;
+  /// The string represented by the token
+  std::string val_str_;
+  /// The signed integer represented by the token
+  int32_t val_int_ = 0;
+  /// The unsigned integer represented by the token
+  uint32_t val_uint_ = 0;
+  /// The float value represented by the token
+  float val_float_ = 0.0;
+};
+
+#ifndef NDEBUG
+inline std::ostream& operator<<(std::ostream& out, Token::Type type) {
+  out << Token::TypeToName(type);
+  return out;
+}
+#endif  // NDEBUG
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
+
+#endif  // SRC_READER_WGSL_TOKEN_H_
diff --git a/src/reader/wgsl/token_test.cc b/src/reader/wgsl/token_test.cc
new file mode 100644
index 0000000..d4fbc42
--- /dev/null
+++ b/src/reader/wgsl/token_test.cc
@@ -0,0 +1,76 @@
+// 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/reader/wgsl/token.h"
+
+#include <limits>
+
+#include "gtest/gtest.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+
+using TokenTest = testing::Test;
+
+TEST_F(TokenTest, ReturnsStr) {
+  Token t(Token::Type::kStringLiteral, Source{1, 1}, "test string");
+  EXPECT_EQ(t.to_str(), "test string");
+}
+
+TEST_F(TokenTest, ReturnsF32) {
+  Token t1(Source{1, 1}, -2.345f);
+  EXPECT_EQ(t1.to_f32(), -2.345f);
+
+  Token t2(Source{1, 1}, 2.345f);
+  EXPECT_EQ(t2.to_f32(), 2.345f);
+}
+
+TEST_F(TokenTest, ReturnsI32) {
+  Token t1(Source{1, 1}, -2345);
+  EXPECT_EQ(t1.to_i32(), -2345);
+
+  Token t2(Source{1, 1}, 2345);
+  EXPECT_EQ(t2.to_i32(), 2345);
+}
+
+TEST_F(TokenTest, HandlesMaxI32) {
+  Token t1(Source{1, 1}, std::numeric_limits<int32_t>::max());
+  EXPECT_EQ(t1.to_i32(), std::numeric_limits<int32_t>::max());
+}
+
+TEST_F(TokenTest, HandlesMinI32) {
+  Token t1(Source{1, 1}, std::numeric_limits<int32_t>::min());
+  EXPECT_EQ(t1.to_i32(), std::numeric_limits<int32_t>::min());
+}
+
+TEST_F(TokenTest, ReturnsU32) {
+  Token t2(Source{1, 1}, 2345u);
+  EXPECT_EQ(t2.to_u32(), 2345);
+}
+
+TEST_F(TokenTest, ReturnsMaxU32) {
+  Token t1(Source{1, 1}, std::numeric_limits<uint32_t>::max());
+  EXPECT_EQ(t1.to_u32(), std::numeric_limits<uint32_t>::max());
+}
+
+TEST_F(TokenTest, Source) {
+  Token t(Token::Type::kUintLiteral, Source{3, 9});
+  EXPECT_EQ(t.line(), 3);
+  EXPECT_EQ(t.column(), 9);
+}
+
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint