Initial commit
diff --git a/.clang-format b/.clang-format
new file mode 100644
index 0000000..2fb833a
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1,2 @@
+# http://clang.llvm.org/docs/ClangFormatStyleOptions.html
+BasedOnStyle: Chromium
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ed1481c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+.vscode
+.DS_Store
+build
+out
+third_party/cpplint
+third_party/googletest
+third_party/spirv-headers
+third_party/spirv-tools
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..8c20be8
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,140 @@
+# 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.
+
+cmake_minimum_required(VERSION 3.10.2)
+
+project(tint)
+enable_testing()
+set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR})
+set(CMAKE_POSITION_INDEPENDENT_CODE ON)
+set(CMAKE_CXX_STANDARD 14)
+set(CMAKE_DEBUG_POSTFIX "")
+
+if ("${CMAKE_BUILD_TYPE}" STREQUAL "")
+  message(STATUS "No build type selected, default to Debug")
+  set(CMAKE_BUILD_TYPE "Debug")
+endif()
+
+option(TINT_BUILD_DOCS "Build documentation" ON)
+option(TINT_BUILD_SPV_PARSER "Build the SPIR-V input parser" OFF)
+option(TINT_BUILD_FUZZERS "Build fuzzers" OFF)
+
+option(TINT_ENABLE_MSAN "Enable memory sanitizer" OFF)
+option(TINT_ENABLE_ASAN "Enable address sanitizer" OFF)
+option(TINT_ENABLE_UBSAN "Enable undefined behaviour sanitizer" OFF)
+
+message(STATUS "Tint build docs: ${TINT_BUILD_DOCS}")
+message(STATUS "Tint build SPIR-V parser: ${TINT_BUILD_SPV_PARSER}")
+message(STATUS "Tint build fuzzers: ${TINT_BUILD_FUZZERS}")
+message(STATUS "Tint build with ASAN: ${TINT_ENABLE_ASAN}")
+message(STATUS "Tint build with MSAN: ${TINT_ENABLE_MSAN}")
+message(STATUS "Tint build with UBSAN: ${TINT_ENABLE_UBSAN}")
+
+message(STATUS "Using python3")
+find_package(PythonInterp 3 REQUIRED)
+
+if (${TINT_BUILD_SPV_PARSER})
+  include_directories("${PROJECT_SOURCE_DIR}/third_party/spirv-tools/include")
+endif()
+
+if(("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") OR
+    ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "AppleClang") OR
+    (("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") AND
+     (NOT CMAKE_CXX_SIMULATE_ID STREQUAL "MSVC")))
+  set(COMPILER_IS_LIKE_GNU TRUE)
+endif()
+
+find_package(Doxygen)
+if(DOXYGEN_FOUND)
+  add_custom_target(tint_docs ALL
+      COMMAND ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/Doxyfile
+      WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
+      COMMENT "Generating API documentation"
+      VERBATIM)
+else()
+  message("Doxygen not found. Skipping documentation")
+endif()
+
+function(tint_default_compile_options TARGET)
+  include_directories("${PROJECT_SOURCE_DIR}")
+
+  if (${COMPILER_IS_LIKE_GNU})
+    target_compile_options(${TARGET} PRIVATE
+      -std=c++14
+      -fno-exceptions
+      -fno-rtti
+      -fvisibility=hidden
+      -Wall
+      -Werror
+      -Wextra
+      -Wno-documentation-unknown-command
+      -Wno-padded
+      -Wno-switch-enum
+      -Wno-unknown-pragmas
+      -pedantic-errors
+    )
+
+    if (("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") OR
+        ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "AppleClang"))
+      target_compile_options(${TARGET} PRIVATE
+        -Wno-c++98-compat
+        -Wno-c++98-compat-pedantic
+        -Wno-format-pedantic
+        -Wno-return-std-move-in-c++11
+        -Wno-unknown-warning-option
+        -Weverything
+      )
+    endif()
+
+    if (${TINT_ENABLE_MSAN})
+      target_compile_options(${TARGET} PRIVATE -fsanitize=memory)
+      target_link_options(${TARGET} PRIVATE -fsanitize=memory)
+    elseif (${TINT_ENABLE_ASAN})
+      target_compile_options(${TARGET} PRIVATE -fsanitize=address)
+      target_link_options(${TARGET} PRIVATE -fsanitize=address)
+    elseif (${TINT_ENABLE_UBSAN})
+      target_compile_options(${TARGET} PRIVATE -fsanitize=undefined)
+      target_link_options(${TARGET} PRIVATE -fsanitize=undefined)
+    endif()
+  endif()
+
+  if (MSVC)
+    # Specify /EHs for exception handling.
+    target_compile_options(${TARGET} PRIVATE
+      /bigobj
+      /EHsc
+      /W3
+      /WX
+      /wd4068
+      /wd4514
+      /wd4571
+      /wd4625
+      /wd4626
+      /wd4710
+      /wd4774
+      /wd4820
+      /wd5026
+      /wd5027
+    )
+  endif()
+endfunction()
+
+add_subdirectory(third_party)
+add_subdirectory(src)
+add_subdirectory(samples)
+
+if (${TINT_BUILD_FUZZERS})
+  add_subdirectory(fuzz)
+endif()
+
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..12921d9
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,93 @@
+# Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to making participation in our project and
+our community a harassment-free experience for everyone, regardless of age, body
+size, disability, ethnicity, gender identity and expression, level of
+experience, education, socio-economic status, nationality, personal appearance,
+race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+*   Using welcoming and inclusive language
+*   Being respectful of differing viewpoints and experiences
+*   Gracefully accepting constructive criticism
+*   Focusing on what is best for the community
+*   Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+*   The use of sexualized language or imagery and unwelcome sexual attention or
+    advances
+*   Trolling, insulting/derogatory comments, and personal or political attacks
+*   Public or private harassment
+*   Publishing others' private information, such as a physical or electronic
+    address, without explicit permission
+*   Other conduct which could reasonably be considered inappropriate in a
+    professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable
+behavior and are expected to take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, or to ban temporarily or permanently any
+contributor for other behaviors that they deem inappropriate, threatening,
+offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community. Examples of
+representing a project or community include using an official project e-mail
+address, posting via an official social media account, or acting as an appointed
+representative at an online or offline event. Representation of a project may be
+further defined and clarified by project maintainers.
+
+This Code of Conduct also applies outside the project spaces when the Project
+Steward has a reasonable belief that an individual's behavior may have a
+negative impact on the project or its community.
+
+## Conflict Resolution
+
+We do not believe that all conflict is bad; healthy debate and disagreement
+often yield positive results. However, it is never okay to be disrespectful or
+to engage in behavior that violates the project’s code of conduct.
+
+If you see someone violating the code of conduct, you are encouraged to address
+the behavior directly with those involved. Many issues can be resolved quickly
+and easily, and this gives people more control over the outcome of their
+dispute. If you are unable to resolve the matter for any reason, or if the
+behavior is threatening or harassing, report it. We are dedicated to providing
+an environment where participants feel welcome and safe.
+
+Reports should be directed to David Neto <dneto@google.com>, the
+Project Steward(s) for Tint. It is the Project Steward’s duty to
+receive and address reported violations of the code of conduct. They will then
+work with a committee consisting of representatives from the Open Source
+Programs Office and the Google Open Source Strategy team. If for any reason you
+are uncomfortable reaching out the Project Steward, please email
+opensource@google.com.
+
+We will investigate every complaint, but you may not receive a direct response.
+We will use our discretion in determining when and how to follow up on reported
+incidents, which may range from not taking action to permanent expulsion from
+the project and project-sponsored spaces. We will notify the accused of the
+report and provide them an opportunity to discuss it before any action is taken.
+The identity of the reporter will be omitted from the details of the report
+supplied to the accused. In potentially harmful situations, such as ongoing
+harassment or threats to anyone's safety, we may take action without notice.
+
+## Attribution
+
+This Code of Conduct is adapted from the Contributor Covenant, version 1.4,
+available at
+https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..db177d4
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,28 @@
+# How to Contribute
+
+We'd love to accept your patches and contributions to this project. There are
+just a few small guidelines you need to follow.
+
+## Contributor License Agreement
+
+Contributions to this project must be accompanied by a Contributor License
+Agreement. You (or your employer) retain the copyright to your contribution;
+this simply gives us permission to use and redistribute your contributions as
+part of the project. Head over to <https://cla.developers.google.com/> to see
+your current agreements on file or to sign a new one.
+
+You generally only need to submit a CLA once, so if you've already submitted one
+(even if it was for a different project), you probably don't need to do it
+again.
+
+## Code reviews
+
+All submissions, including submissions by project members, require review. We
+use GitHub pull requests for this purpose. Consult
+[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
+information on using pull requests.
+
+## Community Guidelines
+
+This project follows
+[Google's Open Source Community Guidelines](https://opensource.google.com/conduct/).
diff --git a/CPPLINT.cfg b/CPPLINT.cfg
new file mode 100644
index 0000000..48172ca
--- /dev/null
+++ b/CPPLINT.cfg
@@ -0,0 +1,2 @@
+set noparent
+headers=h,hpp
diff --git a/DEPS b/DEPS
new file mode 100644
index 0000000..c2beef6
--- /dev/null
+++ b/DEPS
@@ -0,0 +1,25 @@
+use_relative_paths = True
+
+vars = {
+  'google_git':  'https://github.com/google',
+  'khronos_git': 'https://github.com/KhronosGroup',
+
+  'cpplint_revision': '26470f9ccb354ff2f6d098f831271a1833701b28',
+  'googletest_revision': '41b5f149ab306e96b5b2faf523505d75acffd98a',
+  'spirv_headers_revision': '5dbc1c32182e17b8ab8e8158a802ecabaf35aad3',
+  'spirv_tools_revision': 'fe10239f92f4539e9050da375dab095328fec196',
+}
+
+deps = {
+  'third_party/cpplint': Var('google_git') + '/styleguide.git@' +
+      Var('cpplint_revision'),
+
+  'third_party/googletest': Var('google_git') + '/googletest.git@' +
+      Var('googletest_revision'),
+
+  'third_party/spirv-headers': Var('khronos_git') + '/SPIRV-Headers.git@' +
+      Var('spirv_headers_revision'),
+
+  'third_party/spirv-tools': Var('khronos_git') + '/SPIRV-Tools.git@' +
+      Var('spirv_tools_revision'),
+}
diff --git a/Doxyfile b/Doxyfile
new file mode 100644
index 0000000..a3d64f6
--- /dev/null
+++ b/Doxyfile
@@ -0,0 +1,2485 @@
+# Doxyfile 1.8.14
+
+# This file describes the settings to be used by the documentation system
+# doxygen (www.doxygen.org) for a project.
+#
+# All text after a double hash (##) is considered a comment and is placed in
+# front of the TAG it is preceding.
+#
+# All text after a single hash (#) is considered a comment and will be ignored.
+# The format is:
+# TAG = value [value, ...]
+# For lists, items can also be appended using:
+# TAG += value [value, ...]
+# Values that contain spaces should be placed between quotes (\" \").
+
+#---------------------------------------------------------------------------
+# Project related configuration options
+#---------------------------------------------------------------------------
+
+# This tag specifies the encoding used for all characters in the config file
+# that follow. The default is UTF-8 which is also the encoding used for all text
+# before the first occurrence of this tag. Doxygen uses libiconv (or the iconv
+# built into libc) for the transcoding. See
+# https://www.gnu.org/software/libiconv/ for the list of possible encodings.
+# The default value is: UTF-8.
+
+DOXYFILE_ENCODING      = UTF-8
+
+# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by
+# double-quotes, unless you are using Doxywizard) that should identify the
+# project for which the documentation is generated. This name is used in the
+# title of most generated pages and in a few other places.
+# The default value is: My Project.
+
+PROJECT_NAME           = "Tint"
+
+# The PROJECT_NUMBER tag can be used to enter a project or revision number. This
+# could be handy for archiving the generated documentation or if some version
+# control system is used.
+
+PROJECT_NUMBER         =
+
+# Using the PROJECT_BRIEF tag one can provide an optional one line description
+# for a project that appears at the top of each page and should give viewer a
+# quick idea about the purpose of the project. Keep the description short.
+
+PROJECT_BRIEF          = Tint
+
+# With the PROJECT_LOGO tag one can specify a logo or an icon that is included
+# in the documentation. The maximum height of the logo should not exceed 55
+# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy
+# the logo to the output directory.
+
+PROJECT_LOGO           =
+
+# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path
+# into which the generated documentation will be written. If a relative path is
+# entered, it will be relative to the location where doxygen was started. If
+# left blank the current directory will be used.
+
+OUTPUT_DIRECTORY       = out/docs
+
+# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub-
+# directories (in 2 levels) under the output directory of each output format and
+# will distribute the generated files over these directories. Enabling this
+# option can be useful when feeding doxygen a huge amount of source files, where
+# putting all generated files in the same directory would otherwise causes
+# performance problems for the file system.
+# The default value is: NO.
+
+CREATE_SUBDIRS         = NO
+
+# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII
+# characters to appear in the names of generated files. If set to NO, non-ASCII
+# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode
+# U+3044.
+# The default value is: NO.
+
+ALLOW_UNICODE_NAMES    = NO
+
+# The OUTPUT_LANGUAGE tag is used to specify the language in which all
+# documentation generated by doxygen is written. Doxygen will use this
+# information to generate all constant output in the proper language.
+# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese,
+# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States),
+# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian,
+# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages),
+# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian,
+# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian,
+# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish,
+# Ukrainian and Vietnamese.
+# The default value is: English.
+
+OUTPUT_LANGUAGE        = English
+
+# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member
+# descriptions after the members that are listed in the file and class
+# documentation (similar to Javadoc). Set to NO to disable this.
+# The default value is: YES.
+
+BRIEF_MEMBER_DESC      = YES
+
+# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief
+# description of a member or function before the detailed description
+#
+# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the
+# brief descriptions will be completely suppressed.
+# The default value is: YES.
+
+REPEAT_BRIEF           = YES
+
+# This tag implements a quasi-intelligent brief description abbreviator that is
+# used to form the text in various listings. Each string in this list, if found
+# as the leading text of the brief description, will be stripped from the text
+# and the result, after processing the whole list, is used as the annotated
+# text. Otherwise, the brief description is used as-is. If left blank, the
+# following values are used ($name is automatically replaced with the name of
+# the entity):The $name class, The $name widget, The $name file, is, provides,
+# specifies, contains, represents, a, an and the.
+
+ABBREVIATE_BRIEF       = "The $name class" \
+                         "The $name widget" \
+                         "The $name file" \
+                         is \
+                         provides \
+                         specifies \
+                         contains \
+                         represents \
+                         a \
+                         an \
+                         the
+
+# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then
+# doxygen will generate a detailed section even if there is only a brief
+# description.
+# The default value is: NO.
+
+ALWAYS_DETAILED_SEC    = NO
+
+# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all
+# inherited members of a class in the documentation of that class as if those
+# members were ordinary class members. Constructors, destructors and assignment
+# operators of the base classes will not be shown.
+# The default value is: NO.
+
+INLINE_INHERITED_MEMB  = NO
+
+# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path
+# before files name in the file list and in the header files. If set to NO the
+# shortest path that makes the file name unique will be used
+# The default value is: YES.
+
+FULL_PATH_NAMES        = YES
+
+# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path.
+# Stripping is only done if one of the specified strings matches the left-hand
+# part of the path. The tag can be used to show relative paths in the file list.
+# If left blank the directory from which doxygen is run is used as the path to
+# strip.
+#
+# Note that you can specify absolute paths here, but also relative paths, which
+# will be relative from the directory where doxygen is started.
+# This tag requires that the tag FULL_PATH_NAMES is set to YES.
+
+STRIP_FROM_PATH        =
+
+# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the
+# path mentioned in the documentation of a class, which tells the reader which
+# header file to include in order to use a class. If left blank only the name of
+# the header file containing the class definition is used. Otherwise one should
+# specify the list of include paths that are normally passed to the compiler
+# using the -I flag.
+
+STRIP_FROM_INC_PATH    =
+
+# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but
+# less readable) file names. This can be useful is your file systems doesn't
+# support long names like on DOS, Mac, or CD-ROM.
+# The default value is: NO.
+
+SHORT_NAMES            = NO
+
+# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the
+# first line (until the first dot) of a Javadoc-style comment as the brief
+# description. If set to NO, the Javadoc-style will behave just like regular Qt-
+# style comments (thus requiring an explicit @brief command for a brief
+# description.)
+# The default value is: NO.
+
+JAVADOC_AUTOBRIEF      = NO
+
+# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first
+# line (until the first dot) of a Qt-style comment as the brief description. If
+# set to NO, the Qt-style will behave just like regular Qt-style comments (thus
+# requiring an explicit \brief command for a brief description.)
+# The default value is: NO.
+
+QT_AUTOBRIEF           = NO
+
+# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a
+# multi-line C++ special comment block (i.e. a block of //! or /// comments) as
+# a brief description. This used to be the default behavior. The new default is
+# to treat a multi-line C++ comment block as a detailed description. Set this
+# tag to YES if you prefer the old behavior instead.
+#
+# Note that setting this tag to YES also means that rational rose comments are
+# not recognized any more.
+# The default value is: NO.
+
+MULTILINE_CPP_IS_BRIEF = NO
+
+# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the
+# documentation from any documented member that it re-implements.
+# The default value is: YES.
+
+INHERIT_DOCS           = YES
+
+# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new
+# page for each member. If set to NO, the documentation of a member will be part
+# of the file/class/namespace that contains it.
+# The default value is: NO.
+
+SEPARATE_MEMBER_PAGES  = NO
+
+# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen
+# uses this value to replace tabs by spaces in code fragments.
+# Minimum value: 1, maximum value: 16, default value: 4.
+
+TAB_SIZE               = 2
+
+# This tag can be used to specify a number of aliases that act as commands in
+# the documentation. An alias has the form:
+# name=value
+# For example adding
+# "sideeffect=@par Side Effects:\n"
+# will allow you to put the command \sideeffect (or @sideeffect) in the
+# documentation, which will result in a user-defined paragraph with heading
+# "Side Effects:". You can put \n's in the value part of an alias to insert
+# newlines (in the resulting output). You can put ^^ in the value part of an
+# alias to insert a newline as if a physical newline was in the original file.
+
+ALIASES                =
+
+# This tag can be used to specify a number of word-keyword mappings (TCL only).
+# A mapping has the form "name=value". For example adding "class=itcl::class"
+# will allow you to use the command class in the itcl::class meaning.
+
+TCL_SUBST              =
+
+# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources
+# only. Doxygen will then generate output that is more tailored for C. For
+# instance, some of the names that are used will be different. The list of all
+# members will be omitted, etc.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_FOR_C  = YES
+
+# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or
+# Python sources only. Doxygen will then generate output that is more tailored
+# for that language. For instance, namespaces will be presented as packages,
+# qualified scopes will look different, etc.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_JAVA   = NO
+
+# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran
+# sources. Doxygen will then generate output that is tailored for Fortran.
+# The default value is: NO.
+
+OPTIMIZE_FOR_FORTRAN   = NO
+
+# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL
+# sources. Doxygen will then generate output that is tailored for VHDL.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_VHDL   = NO
+
+# Doxygen selects the parser to use depending on the extension of the files it
+# parses. With this tag you can assign which parser to use for a given
+# extension. Doxygen has a built-in mapping, but you can override or extend it
+# using this tag. The format is ext=language, where ext is a file extension, and
+# language is one of the parsers supported by doxygen: IDL, Java, Javascript,
+# C#, C, C++, D, PHP, Objective-C, Python, Fortran (fixed format Fortran:
+# FortranFixed, free formatted Fortran: FortranFree, unknown formatted Fortran:
+# Fortran. In the later case the parser tries to guess whether the code is fixed
+# or free formatted code, this is the default for Fortran type files), VHDL. For
+# instance to make doxygen treat .inc files as Fortran files (default is PHP),
+# and .f files as C (default is Fortran), use: inc=Fortran f=C.
+#
+# Note: For files without extension you can use no_extension as a placeholder.
+#
+# Note that for custom extensions you also need to set FILE_PATTERNS otherwise
+# the files are not read by doxygen.
+
+EXTENSION_MAPPING      =
+
+# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments
+# according to the Markdown format, which allows for more readable
+# documentation. See http://daringfireball.net/projects/markdown/ for details.
+# The output of markdown processing is further processed by doxygen, so you can
+# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in
+# case of backward compatibilities issues.
+# The default value is: YES.
+
+MARKDOWN_SUPPORT       = YES
+
+# When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up
+# to that level are automatically included in the table of contents, even if
+# they do not have an id attribute.
+# Note: This feature currently applies only to Markdown headings.
+# Minimum value: 0, maximum value: 99, default value: 0.
+# This tag requires that the tag MARKDOWN_SUPPORT is set to YES.
+
+TOC_INCLUDE_HEADINGS   = 0
+
+# When enabled doxygen tries to link words that correspond to documented
+# classes, or namespaces to their corresponding documentation. Such a link can
+# be prevented in individual cases by putting a % sign in front of the word or
+# globally by setting AUTOLINK_SUPPORT to NO.
+# The default value is: YES.
+
+AUTOLINK_SUPPORT       = YES
+
+# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want
+# to include (a tag file for) the STL sources as input, then you should set this
+# tag to YES in order to let doxygen match functions declarations and
+# definitions whose arguments contain STL classes (e.g. func(std::string);
+# versus func(std::string) {}). This also make the inheritance and collaboration
+# diagrams that involve STL classes more complete and accurate.
+# The default value is: NO.
+
+BUILTIN_STL_SUPPORT    = NO
+
+# If you use Microsoft's C++/CLI language, you should set this option to YES to
+# enable parsing support.
+# The default value is: NO.
+
+CPP_CLI_SUPPORT        = NO
+
+# Set the SIP_SUPPORT tag to YES if your project consists of sip (see:
+# https://www.riverbankcomputing.com/software/sip/intro) sources only. Doxygen
+# will parse them like normal C++ but will assume all classes use public instead
+# of private inheritance when no explicit protection keyword is present.
+# The default value is: NO.
+
+SIP_SUPPORT            = NO
+
+# For Microsoft's IDL there are propget and propput attributes to indicate
+# getter and setter methods for a property. Setting this option to YES will make
+# doxygen to replace the get and set methods by a property in the documentation.
+# This will only work if the methods are indeed getting or setting a simple
+# type. If this is not the case, or you want to show the methods anyway, you
+# should set this option to NO.
+# The default value is: YES.
+
+IDL_PROPERTY_SUPPORT   = YES
+
+# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC
+# tag is set to YES then doxygen will reuse the documentation of the first
+# member in the group (if any) for the other members of the group. By default
+# all members of a group must be documented explicitly.
+# The default value is: NO.
+
+DISTRIBUTE_GROUP_DOC   = NO
+
+# If one adds a struct or class to a group and this option is enabled, then also
+# any nested class or struct is added to the same group. By default this option
+# is disabled and one has to add nested compounds explicitly via \ingroup.
+# The default value is: NO.
+
+GROUP_NESTED_COMPOUNDS = NO
+
+# Set the SUBGROUPING tag to YES to allow class member groups of the same type
+# (for instance a group of public functions) to be put as a subgroup of that
+# type (e.g. under the Public Functions section). Set it to NO to prevent
+# subgrouping. Alternatively, this can be done per class using the
+# \nosubgrouping command.
+# The default value is: YES.
+
+SUBGROUPING            = YES
+
+# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions
+# are shown inside the group in which they are included (e.g. using \ingroup)
+# instead of on a separate page (for HTML and Man pages) or section (for LaTeX
+# and RTF).
+#
+# Note that this feature does not work in combination with
+# SEPARATE_MEMBER_PAGES.
+# The default value is: NO.
+
+INLINE_GROUPED_CLASSES = NO
+
+# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions
+# with only public data fields or simple typedef fields will be shown inline in
+# the documentation of the scope in which they are defined (i.e. file,
+# namespace, or group documentation), provided this scope is documented. If set
+# to NO, structs, classes, and unions are shown on a separate page (for HTML and
+# Man pages) or section (for LaTeX and RTF).
+# The default value is: NO.
+
+INLINE_SIMPLE_STRUCTS  = NO
+
+# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or
+# enum is documented as struct, union, or enum with the name of the typedef. So
+# typedef struct TypeS {} TypeT, will appear in the documentation as a struct
+# with name TypeT. When disabled the typedef will appear as a member of a file,
+# namespace, or class. And the struct will be named TypeS. This can typically be
+# useful for C code in case the coding convention dictates that all compound
+# types are typedef'ed and only the typedef is referenced, never the tag name.
+# The default value is: NO.
+
+TYPEDEF_HIDES_STRUCT   = NO
+
+# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This
+# cache is used to resolve symbols given their name and scope. Since this can be
+# an expensive process and often the same symbol appears multiple times in the
+# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small
+# doxygen will become slower. If the cache is too large, memory is wasted. The
+# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range
+# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536
+# symbols. At the end of a run doxygen will report the cache usage and suggest
+# the optimal cache size from a speed point of view.
+# Minimum value: 0, maximum value: 9, default value: 0.
+
+LOOKUP_CACHE_SIZE      = 0
+
+#---------------------------------------------------------------------------
+# Build related configuration options
+#---------------------------------------------------------------------------
+
+# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in
+# documentation are documented, even if no documentation was available. Private
+# class members and static file members will be hidden unless the
+# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES.
+# Note: This will also disable the warnings about undocumented members that are
+# normally produced when WARNINGS is set to YES.
+# The default value is: NO.
+
+EXTRACT_ALL            = NO
+
+# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will
+# be included in the documentation.
+# The default value is: NO.
+
+EXTRACT_PRIVATE        = NO
+
+# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal
+# scope will be included in the documentation.
+# The default value is: NO.
+
+EXTRACT_PACKAGE        = yes
+
+# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be
+# included in the documentation.
+# The default value is: NO.
+
+EXTRACT_STATIC         = yes
+
+# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined
+# locally in source files will be included in the documentation. If set to NO,
+# only classes defined in header files are included. Does not have any effect
+# for Java sources.
+# The default value is: YES.
+
+EXTRACT_LOCAL_CLASSES  = YES
+
+# This flag is only useful for Objective-C code. If set to YES, local methods,
+# which are defined in the implementation section but not in the interface are
+# included in the documentation. If set to NO, only methods in the interface are
+# included.
+# The default value is: NO.
+
+EXTRACT_LOCAL_METHODS  = NO
+
+# If this flag is set to YES, the members of anonymous namespaces will be
+# extracted and appear in the documentation as a namespace called
+# 'anonymous_namespace{file}', where file will be replaced with the base name of
+# the file that contains the anonymous namespace. By default anonymous namespace
+# are hidden.
+# The default value is: NO.
+
+EXTRACT_ANON_NSPACES   = NO
+
+# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all
+# undocumented members inside documented classes or files. If set to NO these
+# members will be included in the various overviews, but no documentation
+# section is generated. This option has no effect if EXTRACT_ALL is enabled.
+# The default value is: NO.
+
+HIDE_UNDOC_MEMBERS     = NO
+
+# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all
+# undocumented classes that are normally visible in the class hierarchy. If set
+# to NO, these classes will be included in the various overviews. This option
+# has no effect if EXTRACT_ALL is enabled.
+# The default value is: NO.
+
+HIDE_UNDOC_CLASSES     = NO
+
+# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend
+# (class|struct|union) declarations. If set to NO, these declarations will be
+# included in the documentation.
+# The default value is: NO.
+
+HIDE_FRIEND_COMPOUNDS  = NO
+
+# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any
+# documentation blocks found inside the body of a function. If set to NO, these
+# blocks will be appended to the function's detailed documentation block.
+# The default value is: NO.
+
+HIDE_IN_BODY_DOCS      = NO
+
+# The INTERNAL_DOCS tag determines if documentation that is typed after a
+# \internal command is included. If the tag is set to NO then the documentation
+# will be excluded. Set it to YES to include the internal documentation.
+# The default value is: NO.
+
+INTERNAL_DOCS          = NO
+
+# If the CASE_SENSE_NAMES tag is set to NO then doxygen will only generate file
+# names in lower-case letters. If set to YES, upper-case letters are also
+# allowed. This is useful if you have classes or files whose names only differ
+# in case and if your file system supports case sensitive file names. Windows
+# and Mac users are advised to set this option to NO.
+# The default value is: system dependent.
+
+CASE_SENSE_NAMES       = NO
+
+# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with
+# their full class and namespace scopes in the documentation. If set to YES, the
+# scope will be hidden.
+# The default value is: NO.
+
+HIDE_SCOPE_NAMES       = NO
+
+# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will
+# append additional text to a page's title, such as Class Reference. If set to
+# YES the compound reference will be hidden.
+# The default value is: NO.
+
+HIDE_COMPOUND_REFERENCE= NO
+
+# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of
+# the files that are included by a file in the documentation of that file.
+# The default value is: YES.
+
+SHOW_INCLUDE_FILES     = YES
+
+# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each
+# grouped member an include statement to the documentation, telling the reader
+# which file to include in order to use the member.
+# The default value is: NO.
+
+SHOW_GROUPED_MEMB_INC  = NO
+
+# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include
+# files with double quotes in the documentation rather than with sharp brackets.
+# The default value is: NO.
+
+FORCE_LOCAL_INCLUDES   = NO
+
+# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the
+# documentation for inline members.
+# The default value is: YES.
+
+INLINE_INFO            = YES
+
+# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the
+# (detailed) documentation of file and class members alphabetically by member
+# name. If set to NO, the members will appear in declaration order.
+# The default value is: YES.
+
+SORT_MEMBER_DOCS       = YES
+
+# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief
+# descriptions of file, namespace and class members alphabetically by member
+# name. If set to NO, the members will appear in declaration order. Note that
+# this will also influence the order of the classes in the class list.
+# The default value is: NO.
+
+SORT_BRIEF_DOCS        = YES
+
+# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the
+# (brief and detailed) documentation of class members so that constructors and
+# destructors are listed first. If set to NO the constructors will appear in the
+# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS.
+# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief
+# member documentation.
+# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting
+# detailed member documentation.
+# The default value is: NO.
+
+SORT_MEMBERS_CTORS_1ST = YES
+
+# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy
+# of group names into alphabetical order. If set to NO the group names will
+# appear in their defined order.
+# The default value is: NO.
+
+SORT_GROUP_NAMES       = YES
+
+# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by
+# fully-qualified names, including namespaces. If set to NO, the class list will
+# be sorted only by class name, not including the namespace part.
+# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES.
+# Note: This option applies only to the class list, not to the alphabetical
+# list.
+# The default value is: NO.
+
+SORT_BY_SCOPE_NAME     = YES
+
+# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper
+# type resolution of all parameters of a function it will reject a match between
+# the prototype and the implementation of a member function even if there is
+# only one candidate or it is obvious which candidate to choose by doing a
+# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still
+# accept a match between prototype and implementation in such cases.
+# The default value is: NO.
+
+STRICT_PROTO_MATCHING  = NO
+
+# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo
+# list. This list is created by putting \todo commands in the documentation.
+# The default value is: YES.
+
+GENERATE_TODOLIST      = YES
+
+# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test
+# list. This list is created by putting \test commands in the documentation.
+# The default value is: YES.
+
+GENERATE_TESTLIST      = YES
+
+# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug
+# list. This list is created by putting \bug commands in the documentation.
+# The default value is: YES.
+
+GENERATE_BUGLIST       = YES
+
+# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO)
+# the deprecated list. This list is created by putting \deprecated commands in
+# the documentation.
+# The default value is: YES.
+
+GENERATE_DEPRECATEDLIST= YES
+
+# The ENABLED_SECTIONS tag can be used to enable conditional documentation
+# sections, marked by \if <section_label> ... \endif and \cond <section_label>
+# ... \endcond blocks.
+
+ENABLED_SECTIONS       =
+
+# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the
+# initial value of a variable or macro / define can have for it to appear in the
+# documentation. If the initializer consists of more lines than specified here
+# it will be hidden. Use a value of 0 to hide initializers completely. The
+# appearance of the value of individual variables and macros / defines can be
+# controlled using \showinitializer or \hideinitializer command in the
+# documentation regardless of this setting.
+# Minimum value: 0, maximum value: 10000, default value: 30.
+
+MAX_INITIALIZER_LINES  = 30
+
+# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at
+# the bottom of the documentation of classes and structs. If set to YES, the
+# list will mention the files that were used to generate the documentation.
+# The default value is: YES.
+
+SHOW_USED_FILES        = YES
+
+# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This
+# will remove the Files entry from the Quick Index and from the Folder Tree View
+# (if specified).
+# The default value is: YES.
+
+SHOW_FILES             = YES
+
+# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces
+# page. This will remove the Namespaces entry from the Quick Index and from the
+# Folder Tree View (if specified).
+# The default value is: YES.
+
+SHOW_NAMESPACES        = YES
+
+# The FILE_VERSION_FILTER tag can be used to specify a program or script that
+# doxygen should invoke to get the current version for each file (typically from
+# the version control system). Doxygen will invoke the program by executing (via
+# popen()) the command command input-file, where command is the value of the
+# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided
+# by doxygen. Whatever the program writes to standard output is used as the file
+# version. For an example see the documentation.
+
+FILE_VERSION_FILTER    =
+
+# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed
+# by doxygen. The layout file controls the global structure of the generated
+# output files in an output format independent way. To create the layout file
+# that represents doxygen's defaults, run doxygen with the -l option. You can
+# optionally specify a file name after the option, if omitted DoxygenLayout.xml
+# will be used as the name of the layout file.
+#
+# Note that if you run doxygen from a directory containing a file called
+# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE
+# tag is left empty.
+
+LAYOUT_FILE            =
+
+# The CITE_BIB_FILES tag can be used to specify one or more bib files containing
+# the reference definitions. This must be a list of .bib files. The .bib
+# extension is automatically appended if omitted. This requires the bibtex tool
+# to be installed. See also https://en.wikipedia.org/wiki/BibTeX for more info.
+# For LaTeX the style of the bibliography can be controlled using
+# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the
+# search path. See also \cite for info how to create references.
+
+CITE_BIB_FILES         =
+
+#---------------------------------------------------------------------------
+# Configuration options related to warning and progress messages
+#---------------------------------------------------------------------------
+
+# The QUIET tag can be used to turn on/off the messages that are generated to
+# standard output by doxygen. If QUIET is set to YES this implies that the
+# messages are off.
+# The default value is: NO.
+
+QUIET                  = YES
+
+# The WARNINGS tag can be used to turn on/off the warning messages that are
+# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES
+# this implies that the warnings are on.
+#
+# Tip: Turn warnings on while writing the documentation.
+# The default value is: YES.
+
+WARNINGS               = YES
+
+# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate
+# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag
+# will automatically be disabled.
+# The default value is: YES.
+
+WARN_IF_UNDOCUMENTED   = YES
+
+# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for
+# potential errors in the documentation, such as not documenting some parameters
+# in a documented function, or documenting parameters that don't exist or using
+# markup commands wrongly.
+# The default value is: YES.
+
+WARN_IF_DOC_ERROR      = YES
+
+# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that
+# are documented, but have no documentation for their parameters or return
+# value. If set to NO, doxygen will only warn about wrong or incomplete
+# parameter documentation, but not about the absence of documentation.
+# The default value is: NO.
+
+WARN_NO_PARAMDOC       = YES
+
+# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when
+# a warning is encountered.
+# The default value is: NO.
+
+WARN_AS_ERROR          = NO
+
+# The WARN_FORMAT tag determines the format of the warning messages that doxygen
+# can produce. The string should contain the $file, $line, and $text tags, which
+# will be replaced by the file and line number from which the warning originated
+# and the warning text. Optionally the format may contain $version, which will
+# be replaced by the version of the file (if it could be obtained via
+# FILE_VERSION_FILTER)
+# The default value is: $file:$line: $text.
+
+WARN_FORMAT            = "$file:$line: $text"
+
+# The WARN_LOGFILE tag can be used to specify a file to which warning and error
+# messages should be written. If left blank the output is written to standard
+# error (stderr).
+
+WARN_LOGFILE           =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the input files
+#---------------------------------------------------------------------------
+
+# The INPUT tag is used to specify the files and/or directories that contain
+# documented source files. You may enter file names like myfile.cpp or
+# directories like /usr/src/myproject. Separate the files or directories with
+# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING
+# Note: If this tag is empty the current directory is searched.
+
+INPUT                  = README.md  \
+                         CODE_OF_CONDUCT.md \
+                         CONTRIBUTING.md \
+                         src
+
+# This tag can be used to specify the character encoding of the source files
+# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses
+# libiconv (or the iconv built into libc) for the transcoding. See the libiconv
+# documentation (see: https://www.gnu.org/software/libiconv/) for the list of
+# possible encodings.
+# The default value is: UTF-8.
+
+INPUT_ENCODING         = UTF-8
+
+# If the value of the INPUT tag contains directories, you can use the
+# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and
+# *.h) to filter out the source-files in the directories.
+#
+# Note that for custom extensions or not directly supported extensions you also
+# need to set EXTENSION_MAPPING for the extension otherwise the files are not
+# read by doxygen.
+#
+# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp,
+# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h,
+# *.hh, *.hxx, *.hpp, *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc,
+# *.m, *.markdown, *.md, *.mm, *.dox, *.py, *.pyw, *.f90, *.f95, *.f03, *.f08,
+# *.f, *.for, *.tcl, *.vhd, *.vhdl, *.ucf and *.qsf.
+
+FILE_PATTERNS          = *.c \
+                         *.cc \
+                         *.cxx \
+                         *.cpp \
+                         *.c++ \
+                         *.java \
+                         *.ii \
+                         *.ixx \
+                         *.ipp \
+                         *.i++ \
+                         *.inl \
+                         *.idl \
+                         *.ddl \
+                         *.odl \
+                         *.h \
+                         *.hh \
+                         *.hxx \
+                         *.hpp \
+                         *.h++ \
+                         *.cs \
+                         *.d \
+                         *.php \
+                         *.php4 \
+                         *.php5 \
+                         *.phtml \
+                         *.inc \
+                         *.m \
+                         *.markdown \
+                         *.md \
+                         *.mm \
+                         *.dox \
+                         *.py \
+                         *.pyw \
+                         *.f90 \
+                         *.f95 \
+                         *.f03 \
+                         *.f08 \
+                         *.f \
+                         *.for \
+                         *.tcl \
+                         *.vhd \
+                         *.vhdl \
+                         *.ucf \
+                         *.qsf
+
+# The RECURSIVE tag can be used to specify whether or not subdirectories should
+# be searched for input files as well.
+# The default value is: NO.
+
+RECURSIVE              = YES
+
+# The EXCLUDE tag can be used to specify files and/or directories that should be
+# excluded from the INPUT source files. This way you can easily exclude a
+# subdirectory from a directory tree whose root is specified with the INPUT tag.
+#
+# Note that relative paths are relative to the directory from which doxygen is
+# run.
+
+EXCLUDE                =
+
+# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or
+# directories that are symbolic links (a Unix file system feature) are excluded
+# from the input.
+# The default value is: NO.
+
+EXCLUDE_SYMLINKS       = NO
+
+# If the value of the INPUT tag contains directories, you can use the
+# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude
+# certain files from those directories.
+#
+# Note that the wildcards are matched against the file with absolute path, so to
+# exclude all test directories for example use the pattern */test/*
+
+EXCLUDE_PATTERNS       = *_test.cc
+
+# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names
+# (namespaces, classes, functions, etc.) that should be excluded from the
+# output. The symbol name can be a fully qualified name, a word, or if the
+# wildcard * is used, a substring. Examples: ANamespace, AClass,
+# AClass::ANamespace, ANamespace::*Test
+#
+# Note that the wildcards are matched against the file with absolute path, so to
+# exclude all test directories use the pattern */test/*
+
+EXCLUDE_SYMBOLS        =
+
+# The EXAMPLE_PATH tag can be used to specify one or more files or directories
+# that contain example code fragments that are included (see the \include
+# command).
+
+EXAMPLE_PATH           =
+
+# If the value of the EXAMPLE_PATH tag contains directories, you can use the
+# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and
+# *.h) to filter out the source-files in the directories. If left blank all
+# files are included.
+
+EXAMPLE_PATTERNS       = *
+
+# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be
+# searched for input files to be used with the \include or \dontinclude commands
+# irrespective of the value of the RECURSIVE tag.
+# The default value is: NO.
+
+EXAMPLE_RECURSIVE      = NO
+
+# The IMAGE_PATH tag can be used to specify one or more files or directories
+# that contain images that are to be included in the documentation (see the
+# \image command).
+
+IMAGE_PATH             =
+
+# The INPUT_FILTER tag can be used to specify a program that doxygen should
+# invoke to filter for each input file. Doxygen will invoke the filter program
+# by executing (via popen()) the command:
+#
+# <filter> <input-file>
+#
+# where <filter> is the value of the INPUT_FILTER tag, and <input-file> is the
+# name of an input file. Doxygen will then use the output that the filter
+# program writes to standard output. If FILTER_PATTERNS is specified, this tag
+# will be ignored.
+#
+# Note that the filter must not add or remove lines; it is applied before the
+# code is scanned, but not when the output code is generated. If lines are added
+# or removed, the anchors will not be placed correctly.
+#
+# Note that for custom extensions or not directly supported extensions you also
+# need to set EXTENSION_MAPPING for the extension otherwise the files are not
+# properly processed by doxygen.
+
+INPUT_FILTER           =
+
+# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern
+# basis. Doxygen will compare the file name with each pattern and apply the
+# filter if there is a match. The filters are a list of the form: pattern=filter
+# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how
+# filters are used. If the FILTER_PATTERNS tag is empty or if none of the
+# patterns match the file name, INPUT_FILTER is applied.
+#
+# Note that for custom extensions or not directly supported extensions you also
+# need to set EXTENSION_MAPPING for the extension otherwise the files are not
+# properly processed by doxygen.
+
+FILTER_PATTERNS        =
+
+# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using
+# INPUT_FILTER) will also be used to filter the input files that are used for
+# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES).
+# The default value is: NO.
+
+FILTER_SOURCE_FILES    = NO
+
+# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file
+# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and
+# it is also possible to disable source filtering for a specific pattern using
+# *.ext= (so without naming a filter).
+# This tag requires that the tag FILTER_SOURCE_FILES is set to YES.
+
+FILTER_SOURCE_PATTERNS =
+
+# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that
+# is part of the input, its contents will be placed on the main page
+# (index.html). This can be useful if you have a project on for instance GitHub
+# and want to reuse the introduction page also for the doxygen output.
+
+USE_MDFILE_AS_MAINPAGE = README.md
+
+#---------------------------------------------------------------------------
+# Configuration options related to source browsing
+#---------------------------------------------------------------------------
+
+# If the SOURCE_BROWSER tag is set to YES then a list of source files will be
+# generated. Documented entities will be cross-referenced with these sources.
+#
+# Note: To get rid of all source code in the generated output, make sure that
+# also VERBATIM_HEADERS is set to NO.
+# The default value is: NO.
+
+SOURCE_BROWSER         = NO
+
+# Setting the INLINE_SOURCES tag to YES will include the body of functions,
+# classes and enums directly into the documentation.
+# The default value is: NO.
+
+INLINE_SOURCES         = NO
+
+# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any
+# special comment blocks from generated source code fragments. Normal C, C++ and
+# Fortran comments will always remain visible.
+# The default value is: YES.
+
+STRIP_CODE_COMMENTS    = YES
+
+# If the REFERENCED_BY_RELATION tag is set to YES then for each documented
+# function all documented functions referencing it will be listed.
+# The default value is: NO.
+
+REFERENCED_BY_RELATION = NO
+
+# If the REFERENCES_RELATION tag is set to YES then for each documented function
+# all documented entities called/used by that function will be listed.
+# The default value is: NO.
+
+REFERENCES_RELATION    = NO
+
+# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set
+# to YES then the hyperlinks from functions in REFERENCES_RELATION and
+# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will
+# link to the documentation.
+# The default value is: YES.
+
+REFERENCES_LINK_SOURCE = YES
+
+# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the
+# source code will show a tooltip with additional information such as prototype,
+# brief description and links to the definition and documentation. Since this
+# will make the HTML file larger and loading of large files a bit slower, you
+# can opt to disable this feature.
+# The default value is: YES.
+# This tag requires that the tag SOURCE_BROWSER is set to YES.
+
+SOURCE_TOOLTIPS        = YES
+
+# If the USE_HTAGS tag is set to YES then the references to source code will
+# point to the HTML generated by the htags(1) tool instead of doxygen built-in
+# source browser. The htags tool is part of GNU's global source tagging system
+# (see https://www.gnu.org/software/global/global.html). You will need version
+# 4.8.6 or higher.
+#
+# To use it do the following:
+# - Install the latest version of global
+# - Enable SOURCE_BROWSER and USE_HTAGS in the config file
+# - Make sure the INPUT points to the root of the source tree
+# - Run doxygen as normal
+#
+# Doxygen will invoke htags (and that will in turn invoke gtags), so these
+# tools must be available from the command line (i.e. in the search path).
+#
+# The result: instead of the source browser generated by doxygen, the links to
+# source code will now point to the output of htags.
+# The default value is: NO.
+# This tag requires that the tag SOURCE_BROWSER is set to YES.
+
+USE_HTAGS              = NO
+
+# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a
+# verbatim copy of the header file for each class for which an include is
+# specified. Set to NO to disable this.
+# See also: Section \class.
+# The default value is: YES.
+
+VERBATIM_HEADERS       = YES
+
+#---------------------------------------------------------------------------
+# Configuration options related to the alphabetical class index
+#---------------------------------------------------------------------------
+
+# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all
+# compounds will be generated. Enable this if the project contains a lot of
+# classes, structs, unions or interfaces.
+# The default value is: YES.
+
+ALPHABETICAL_INDEX     = YES
+
+# The COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns in
+# which the alphabetical index list will be split.
+# Minimum value: 1, maximum value: 20, default value: 5.
+# This tag requires that the tag ALPHABETICAL_INDEX is set to YES.
+
+COLS_IN_ALPHA_INDEX    = 5
+
+# In case all classes in a project start with a common prefix, all classes will
+# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag
+# can be used to specify a prefix (or a list of prefixes) that should be ignored
+# while generating the index headers.
+# This tag requires that the tag ALPHABETICAL_INDEX is set to YES.
+
+IGNORE_PREFIX          =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the HTML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output
+# The default value is: YES.
+
+GENERATE_HTML          = YES
+
+# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: html.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_OUTPUT            = html
+
+# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each
+# generated HTML page (for example: .htm, .php, .asp).
+# The default value is: .html.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_FILE_EXTENSION    = .html
+
+# The HTML_HEADER tag can be used to specify a user-defined HTML header file for
+# each generated HTML page. If the tag is left blank doxygen will generate a
+# standard header.
+#
+# To get valid HTML the header file that includes any scripts and style sheets
+# that doxygen needs, which is dependent on the configuration options used (e.g.
+# the setting GENERATE_TREEVIEW). It is highly recommended to start with a
+# default header using
+# doxygen -w html new_header.html new_footer.html new_stylesheet.css
+# YourConfigFile
+# and then modify the file new_header.html. See also section "Doxygen usage"
+# for information on how to generate the default header that doxygen normally
+# uses.
+# Note: The header is subject to change so you typically have to regenerate the
+# default header when upgrading to a newer version of doxygen. For a description
+# of the possible markers and block names see the documentation.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_HEADER            =
+
+# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each
+# generated HTML page. If the tag is left blank doxygen will generate a standard
+# footer. See HTML_HEADER for more information on how to generate a default
+# footer and what special commands can be used inside the footer. See also
+# section "Doxygen usage" for information on how to generate the default footer
+# that doxygen normally uses.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_FOOTER            =
+
+# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style
+# sheet that is used by each HTML page. It can be used to fine-tune the look of
+# the HTML output. If left blank doxygen will generate a default style sheet.
+# See also section "Doxygen usage" for information on how to generate the style
+# sheet that doxygen normally uses.
+# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as
+# it is more robust and this tag (HTML_STYLESHEET) will in the future become
+# obsolete.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_STYLESHEET        =
+
+# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined
+# cascading style sheets that are included after the standard style sheets
+# created by doxygen. Using this option one can overrule certain style aspects.
+# This is preferred over using HTML_STYLESHEET since it does not replace the
+# standard style sheet and is therefore more robust against future updates.
+# Doxygen will copy the style sheet files to the output directory.
+# Note: The order of the extra style sheet files is of importance (e.g. the last
+# style sheet in the list overrules the setting of the previous ones in the
+# list). For an example see the documentation.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_EXTRA_STYLESHEET  =
+
+# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or
+# other source files which should be copied to the HTML output directory. Note
+# that these files will be copied to the base HTML output directory. Use the
+# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these
+# files. In the HTML_STYLESHEET file, use the file name only. Also note that the
+# files will be copied as-is; there are no commands or markers available.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_EXTRA_FILES       =
+
+# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen
+# will adjust the colors in the style sheet and background images according to
+# this color. Hue is specified as an angle on a colorwheel, see
+# https://en.wikipedia.org/wiki/Hue for more information. For instance the value
+# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300
+# purple, and 360 is red again.
+# Minimum value: 0, maximum value: 359, default value: 220.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_HUE    = 220
+
+# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors
+# in the HTML output. For a value of 0 the output will use grayscales only. A
+# value of 255 will produce the most vivid colors.
+# Minimum value: 0, maximum value: 255, default value: 100.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_SAT    = 100
+
+# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the
+# luminance component of the colors in the HTML output. Values below 100
+# gradually make the output lighter, whereas values above 100 make the output
+# darker. The value divided by 100 is the actual gamma applied, so 80 represents
+# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not
+# change the gamma.
+# Minimum value: 40, maximum value: 240, default value: 80.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_GAMMA  = 80
+
+# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML
+# page will contain the date and time when the page was generated. Setting this
+# to YES can help to show when doxygen was last run and thus if the
+# documentation is up to date.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_TIMESTAMP         = NO
+
+# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML
+# documentation will contain a main index with vertical navigation menus that
+# are dynamically created via Javascript. If disabled, the navigation index will
+# consists of multiple levels of tabs that are statically embedded in every HTML
+# page. Disable this option to support browsers that do not have Javascript,
+# like the Qt help browser.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+#HTML_DYNAMIC_MENUS     = YES
+
+# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML
+# documentation will contain sections that can be hidden and shown after the
+# page has loaded.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_DYNAMIC_SECTIONS  = NO
+
+# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries
+# shown in the various tree structured indices initially; the user can expand
+# and collapse entries dynamically later on. Doxygen will expand the tree to
+# such a level that at most the specified number of entries are visible (unless
+# a fully collapsed tree already exceeds this amount). So setting the number of
+# entries 1 will produce a full collapsed tree by default. 0 is a special value
+# representing an infinite number of entries and will result in a full expanded
+# tree by default.
+# Minimum value: 0, maximum value: 9999, default value: 100.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_INDEX_NUM_ENTRIES = 100
+
+# If the GENERATE_DOCSET tag is set to YES, additional index files will be
+# generated that can be used as input for Apple's Xcode 3 integrated development
+# environment (see: https://developer.apple.com/tools/xcode/), introduced with
+# OSX 10.5 (Leopard). To create a documentation set, doxygen will generate a
+# Makefile in the HTML output directory. Running make will produce the docset in
+# that directory and running make install will install the docset in
+# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at
+# startup. See https://developer.apple.com/tools/creatingdocsetswithdoxygen.html
+# for more information.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_DOCSET        = NO
+
+# This tag determines the name of the docset feed. A documentation feed provides
+# an umbrella under which multiple documentation sets from a single provider
+# (such as a company or product suite) can be grouped.
+# The default value is: Doxygen generated docs.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_FEEDNAME        = "Doxygen generated docs"
+
+# This tag specifies a string that should uniquely identify the documentation
+# set bundle. This should be a reverse domain-name style string, e.g.
+# com.mycompany.MyDocSet. Doxygen will append .docset to the name.
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_BUNDLE_ID       = org.doxygen.Project
+
+# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify
+# the documentation publisher. This should be a reverse domain-name style
+# string, e.g. com.mycompany.MyDocSet.documentation.
+# The default value is: org.doxygen.Publisher.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_PUBLISHER_ID    = org.doxygen.Publisher
+
+# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher.
+# The default value is: Publisher.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_PUBLISHER_NAME  = Publisher
+
+# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three
+# additional HTML index files: index.hhp, index.hhc, and index.hhk. The
+# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop
+# (see: http://www.microsoft.com/en-us/download/details.aspx?id=21138) on
+# Windows.
+#
+# The HTML Help Workshop contains a compiler that can convert all HTML output
+# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML
+# files are now used as the Windows 98 help format, and will replace the old
+# Windows help format (.hlp) on all Windows platforms in the future. Compressed
+# HTML files also contain an index, a table of contents, and you can search for
+# words in the documentation. The HTML workshop also contains a viewer for
+# compressed HTML files.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_HTMLHELP      = NO
+
+# The CHM_FILE tag can be used to specify the file name of the resulting .chm
+# file. You can add a path in front of the file if the result should not be
+# written to the html output directory.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+CHM_FILE               =
+
+# The HHC_LOCATION tag can be used to specify the location (absolute path
+# including file name) of the HTML help compiler (hhc.exe). If non-empty,
+# doxygen will try to run the HTML help compiler on the generated index.hhp.
+# The file has to be specified with full path.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+HHC_LOCATION           =
+
+# The GENERATE_CHI flag controls if a separate .chi index file is generated
+# (YES) or that it should be included in the master .chm file (NO).
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+GENERATE_CHI           = NO
+
+# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc)
+# and project file content.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+CHM_INDEX_ENCODING     =
+
+# The BINARY_TOC flag controls whether a binary table of contents is generated
+# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it
+# enables the Previous and Next buttons.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+BINARY_TOC             = NO
+
+# The TOC_EXPAND flag can be set to YES to add extra items for group members to
+# the table of contents of the HTML help documentation and to the tree view.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+TOC_EXPAND             = NO
+
+# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and
+# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that
+# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help
+# (.qch) of the generated HTML documentation.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_QHP           = NO
+
+# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify
+# the file name of the resulting .qch file. The path specified is relative to
+# the HTML output folder.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QCH_FILE               =
+
+# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help
+# Project output. For more information please see Qt Help Project / Namespace
+# (see: http://doc.qt.io/qt-4.8/qthelpproject.html#namespace).
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_NAMESPACE          = org.doxygen.Project
+
+# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt
+# Help Project output. For more information please see Qt Help Project / Virtual
+# Folders (see: http://doc.qt.io/qt-4.8/qthelpproject.html#virtual-folders).
+# The default value is: doc.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_VIRTUAL_FOLDER     = doc
+
+# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom
+# filter to add. For more information please see Qt Help Project / Custom
+# Filters (see: http://doc.qt.io/qt-4.8/qthelpproject.html#custom-filters).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_CUST_FILTER_NAME   =
+
+# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the
+# custom filter to add. For more information please see Qt Help Project / Custom
+# Filters (see: http://doc.qt.io/qt-4.8/qthelpproject.html#custom-filters).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_CUST_FILTER_ATTRS  =
+
+# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this
+# project's filter section matches. Qt Help Project / Filter Attributes (see:
+# http://doc.qt.io/qt-4.8/qthelpproject.html#filter-attributes).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_SECT_FILTER_ATTRS  =
+
+# The QHG_LOCATION tag can be used to specify the location of Qt's
+# qhelpgenerator. If non-empty doxygen will try to run qhelpgenerator on the
+# generated .qhp file.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHG_LOCATION           =
+
+# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be
+# generated, together with the HTML files, they form an Eclipse help plugin. To
+# install this plugin and make it available under the help contents menu in
+# Eclipse, the contents of the directory containing the HTML and XML files needs
+# to be copied into the plugins directory of eclipse. The name of the directory
+# within the plugins directory should be the same as the ECLIPSE_DOC_ID value.
+# After copying Eclipse needs to be restarted before the help appears.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_ECLIPSEHELP   = NO
+
+# A unique identifier for the Eclipse help plugin. When installing the plugin
+# the directory name containing the HTML and XML files should also have this
+# name. Each documentation set should have its own identifier.
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES.
+
+ECLIPSE_DOC_ID         = org.doxygen.Project
+
+# If you want full control over the layout of the generated HTML pages it might
+# be necessary to disable the index and replace it with your own. The
+# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top
+# of each HTML page. A value of NO enables the index and the value YES disables
+# it. Since the tabs in the index contain the same information as the navigation
+# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+DISABLE_INDEX          = NO
+
+# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index
+# structure should be generated to display hierarchical information. If the tag
+# value is set to YES, a side panel will be generated containing a tree-like
+# index structure (just like the one that is generated for HTML Help). For this
+# to work a browser that supports JavaScript, DHTML, CSS and frames is required
+# (i.e. any modern browser). Windows users are probably better off using the
+# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can
+# further fine-tune the look of the index. As an example, the default style
+# sheet generated by doxygen has an example that shows how to put an image at
+# the root of the tree instead of the PROJECT_NAME. Since the tree basically has
+# the same information as the tab index, you could consider setting
+# DISABLE_INDEX to YES when enabling this option.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_TREEVIEW      = NO
+
+# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that
+# doxygen will group on one line in the generated HTML documentation.
+#
+# Note that a value of 0 will completely suppress the enum values from appearing
+# in the overview section.
+# Minimum value: 0, maximum value: 20, default value: 4.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+ENUM_VALUES_PER_LINE   = 4
+
+# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used
+# to set the initial width (in pixels) of the frame in which the tree is shown.
+# Minimum value: 0, maximum value: 1500, default value: 250.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+TREEVIEW_WIDTH         = 250
+
+# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to
+# external symbols imported via tag files in a separate window.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+EXT_LINKS_IN_WINDOW    = NO
+
+# Use this tag to change the font size of LaTeX formulas included as images in
+# the HTML documentation. When you change the font size after a successful
+# doxygen run you need to manually remove any form_*.png images from the HTML
+# output directory to force them to be regenerated.
+# Minimum value: 8, maximum value: 50, default value: 10.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+FORMULA_FONTSIZE       = 10
+
+# Use the FORMULA_TRANSPARENT tag to determine whether or not the images
+# generated for formulas are transparent PNGs. Transparent PNGs are not
+# supported properly for IE 6.0, but are supported on all modern browsers.
+#
+# Note that when changing this option you need to delete any form_*.png files in
+# the HTML output directory before the changes have effect.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+FORMULA_TRANSPARENT    = YES
+
+# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see
+# https://www.mathjax.org) which uses client side Javascript for the rendering
+# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX
+# installed or if you want to formulas look prettier in the HTML output. When
+# enabled you may also need to install MathJax separately and configure the path
+# to it using the MATHJAX_RELPATH option.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+USE_MATHJAX            = NO
+
+# When MathJax is enabled you can set the default output format to be used for
+# the MathJax output. See the MathJax site (see:
+# http://docs.mathjax.org/en/latest/output.html) for more details.
+# Possible values are: HTML-CSS (which is slower, but has the best
+# compatibility), NativeMML (i.e. MathML) and SVG.
+# The default value is: HTML-CSS.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_FORMAT         = HTML-CSS
+
+# When MathJax is enabled you need to specify the location relative to the HTML
+# output directory using the MATHJAX_RELPATH option. The destination directory
+# should contain the MathJax.js script. For instance, if the mathjax directory
+# is located at the same level as the HTML output directory, then
+# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax
+# Content Delivery Network so you can quickly see the result without installing
+# MathJax. However, it is strongly recommended to install a local copy of
+# MathJax from https://www.mathjax.org before deployment.
+# The default value is: https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.2/.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_RELPATH        = https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.2/
+
+# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax
+# extension names that should be enabled during MathJax rendering. For example
+# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_EXTENSIONS     =
+
+# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces
+# of code that will be used on startup of the MathJax code. See the MathJax site
+# (see: http://docs.mathjax.org/en/latest/output.html) for more details. For an
+# example see the documentation.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_CODEFILE       =
+
+# When the SEARCHENGINE tag is enabled doxygen will generate a search box for
+# the HTML output. The underlying search engine uses javascript and DHTML and
+# should work on any modern browser. Note that when using HTML help
+# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET)
+# there is already a search function so this one should typically be disabled.
+# For large projects the javascript based search engine can be slow, then
+# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to
+# search using the keyboard; to jump to the search box use <access key> + S
+# (what the <access key> is depends on the OS and browser, but it is typically
+# <CTRL>, <ALT>/<option>, or both). Inside the search box use the <cursor down
+# key> to jump into the search results window, the results can be navigated
+# using the <cursor keys>. Press <Enter> to select an item or <escape> to cancel
+# the search. The filter options can be selected when the cursor is inside the
+# search box by pressing <Shift>+<cursor down>. Also here use the <cursor keys>
+# to select a filter and <Enter> or <escape> to activate or cancel the filter
+# option.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+SEARCHENGINE           = YES
+
+# When the SERVER_BASED_SEARCH tag is enabled the search engine will be
+# implemented using a web server instead of a web client using Javascript. There
+# are two flavors of web server based searching depending on the EXTERNAL_SEARCH
+# setting. When disabled, doxygen will generate a PHP script for searching and
+# an index file used by the script. When EXTERNAL_SEARCH is enabled the indexing
+# and searching needs to be provided by external tools. See the section
+# "External Indexing and Searching" for details.
+# The default value is: NO.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+SERVER_BASED_SEARCH    = NO
+
+# When EXTERNAL_SEARCH tag is enabled doxygen will no longer generate the PHP
+# script for searching. Instead the search results are written to an XML file
+# which needs to be processed by an external indexer. Doxygen will invoke an
+# external search engine pointed to by the SEARCHENGINE_URL option to obtain the
+# search results.
+#
+# Doxygen ships with an example indexer (doxyindexer) and search engine
+# (doxysearch.cgi) which are based on the open source search engine library
+# Xapian (see: https://xapian.org/).
+#
+# See the section "External Indexing and Searching" for details.
+# The default value is: NO.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTERNAL_SEARCH        = NO
+
+# The SEARCHENGINE_URL should point to a search engine hosted by a web server
+# which will return the search results when EXTERNAL_SEARCH is enabled.
+#
+# Doxygen ships with an example indexer (doxyindexer) and search engine
+# (doxysearch.cgi) which are based on the open source search engine library
+# Xapian (see: https://xapian.org/). See the section "External Indexing and
+# Searching" for details.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+SEARCHENGINE_URL       =
+
+# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the unindexed
+# search data is written to a file for indexing by an external tool. With the
+# SEARCHDATA_FILE tag the name of this file can be specified.
+# The default file is: searchdata.xml.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+SEARCHDATA_FILE        = searchdata.xml
+
+# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the
+# EXTERNAL_SEARCH_ID tag can be used as an identifier for the project. This is
+# useful in combination with EXTRA_SEARCH_MAPPINGS to search through multiple
+# projects and redirect the results back to the right project.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTERNAL_SEARCH_ID     =
+
+# The EXTRA_SEARCH_MAPPINGS tag can be used to enable searching through doxygen
+# projects other than the one defined by this configuration file, but that are
+# all added to the same external search index. Each project needs to have a
+# unique id set via EXTERNAL_SEARCH_ID. The search mapping then maps the id of
+# to a relative location where the documentation can be found. The format is:
+# EXTRA_SEARCH_MAPPINGS = tagname1=loc1 tagname2=loc2 ...
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTRA_SEARCH_MAPPINGS  =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the LaTeX output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_LATEX tag is set to YES, doxygen will generate LaTeX output.
+# The default value is: YES.
+
+GENERATE_LATEX         = NO
+
+# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: latex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_OUTPUT           = latex
+
+# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be
+# invoked.
+#
+# Note that when enabling USE_PDFLATEX this option is only used for generating
+# bitmaps for formulas in the HTML output, but not in the Makefile that is
+# written to the output directory.
+# The default file is: latex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_CMD_NAME         = latex
+
+# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to generate
+# index for LaTeX.
+# The default file is: makeindex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+MAKEINDEX_CMD_NAME     = makeindex
+
+# If the COMPACT_LATEX tag is set to YES, doxygen generates more compact LaTeX
+# documents. This may be useful for small projects and may help to save some
+# trees in general.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+COMPACT_LATEX          = NO
+
+# The PAPER_TYPE tag can be used to set the paper type that is used by the
+# printer.
+# Possible values are: a4 (210 x 297 mm), letter (8.5 x 11 inches), legal (8.5 x
+# 14 inches) and executive (7.25 x 10.5 inches).
+# The default value is: a4.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+PAPER_TYPE             = a4
+
+# The EXTRA_PACKAGES tag can be used to specify one or more LaTeX package names
+# that should be included in the LaTeX output. The package can be specified just
+# by its name or with the correct syntax as to be used with the LaTeX
+# \usepackage command. To get the times font for instance you can specify :
+# EXTRA_PACKAGES=times or EXTRA_PACKAGES={times}
+# To use the option intlimits with the amsmath package you can specify:
+# EXTRA_PACKAGES=[intlimits]{amsmath}
+# If left blank no extra packages will be included.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+EXTRA_PACKAGES         =
+
+# The LATEX_HEADER tag can be used to specify a personal LaTeX header for the
+# generated LaTeX document. The header should contain everything until the first
+# chapter. If it is left blank doxygen will generate a standard header. See
+# section "Doxygen usage" for information on how to let doxygen write the
+# default header to a separate file.
+#
+# Note: Only use a user-defined header if you know what you are doing! The
+# following commands have a special meaning inside the header: $title,
+# $datetime, $date, $doxygenversion, $projectname, $projectnumber,
+# $projectbrief, $projectlogo. Doxygen will replace $title with the empty
+# string, for the replacement values of the other commands the user is referred
+# to HTML_HEADER.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_HEADER           =
+
+# The LATEX_FOOTER tag can be used to specify a personal LaTeX footer for the
+# generated LaTeX document. The footer should contain everything after the last
+# chapter. If it is left blank doxygen will generate a standard footer. See
+# LATEX_HEADER for more information on how to generate a default footer and what
+# special commands can be used inside the footer.
+#
+# Note: Only use a user-defined footer if you know what you are doing!
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_FOOTER           =
+
+# The LATEX_EXTRA_STYLESHEET tag can be used to specify additional user-defined
+# LaTeX style sheets that are included after the standard style sheets created
+# by doxygen. Using this option one can overrule certain style aspects. Doxygen
+# will copy the style sheet files to the output directory.
+# Note: The order of the extra style sheet files is of importance (e.g. the last
+# style sheet in the list overrules the setting of the previous ones in the
+# list).
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_EXTRA_STYLESHEET =
+
+# The LATEX_EXTRA_FILES tag can be used to specify one or more extra images or
+# other source files which should be copied to the LATEX_OUTPUT output
+# directory. Note that the files will be copied as-is; there are no commands or
+# markers available.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_EXTRA_FILES      =
+
+# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated is
+# prepared for conversion to PDF (using ps2pdf or pdflatex). The PDF file will
+# contain links (just like the HTML output) instead of page references. This
+# makes the output suitable for online browsing using a PDF viewer.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+PDF_HYPERLINKS         = YES
+
+# If the USE_PDFLATEX tag is set to YES, doxygen will use pdflatex to generate
+# the PDF file directly from the LaTeX files. Set this option to YES, to get a
+# higher quality PDF documentation.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+USE_PDFLATEX           = YES
+
+# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \batchmode
+# command to the generated LaTeX files. This will instruct LaTeX to keep running
+# if errors occur, instead of asking the user for help. This option is also used
+# when generating formulas in HTML.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_BATCHMODE        = NO
+
+# If the LATEX_HIDE_INDICES tag is set to YES then doxygen will not include the
+# index chapters (such as File Index, Compound Index, etc.) in the output.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_HIDE_INDICES     = NO
+
+# If the LATEX_SOURCE_CODE tag is set to YES then doxygen will include source
+# code with syntax highlighting in the LaTeX output.
+#
+# Note that which sources are shown also depends on other settings such as
+# SOURCE_BROWSER.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_SOURCE_CODE      = NO
+
+# The LATEX_BIB_STYLE tag can be used to specify the style to use for the
+# bibliography, e.g. plainnat, or ieeetr. See
+# https://en.wikipedia.org/wiki/BibTeX and \cite for more info.
+# The default value is: plain.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_BIB_STYLE        = plain
+
+# If the LATEX_TIMESTAMP tag is set to YES then the footer of each generated
+# page will contain the date and time when the page was generated. Setting this
+# to NO can help when comparing the output of multiple runs.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_TIMESTAMP        = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the RTF output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_RTF tag is set to YES, doxygen will generate RTF output. The
+# RTF output is optimized for Word 97 and may not look too pretty with other RTF
+# readers/editors.
+# The default value is: NO.
+
+GENERATE_RTF           = NO
+
+# The RTF_OUTPUT tag is used to specify where the RTF docs will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: rtf.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_OUTPUT             = rtf
+
+# If the COMPACT_RTF tag is set to YES, doxygen generates more compact RTF
+# documents. This may be useful for small projects and may help to save some
+# trees in general.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+COMPACT_RTF            = NO
+
+# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated will
+# contain hyperlink fields. The RTF file will contain links (just like the HTML
+# output) instead of page references. This makes the output suitable for online
+# browsing using Word or some other Word compatible readers that support those
+# fields.
+#
+# Note: WordPad (write) and others do not support links.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_HYPERLINKS         = NO
+
+# Load stylesheet definitions from file. Syntax is similar to doxygen's config
+# file, i.e. a series of assignments. You only have to provide replacements,
+# missing definitions are set to their default value.
+#
+# See also section "Doxygen usage" for information on how to generate the
+# default style sheet that doxygen normally uses.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_STYLESHEET_FILE    =
+
+# Set optional variables used in the generation of an RTF document. Syntax is
+# similar to doxygen's config file. A template extensions file can be generated
+# using doxygen -e rtf extensionFile.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_EXTENSIONS_FILE    =
+
+# If the RTF_SOURCE_CODE tag is set to YES then doxygen will include source code
+# with syntax highlighting in the RTF output.
+#
+# Note that which sources are shown also depends on other settings such as
+# SOURCE_BROWSER.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_SOURCE_CODE        = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the man page output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_MAN tag is set to YES, doxygen will generate man pages for
+# classes and files.
+# The default value is: NO.
+
+GENERATE_MAN           = NO
+
+# The MAN_OUTPUT tag is used to specify where the man pages will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it. A directory man3 will be created inside the directory specified by
+# MAN_OUTPUT.
+# The default directory is: man.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_OUTPUT             = man
+
+# The MAN_EXTENSION tag determines the extension that is added to the generated
+# man pages. In case the manual section does not start with a number, the number
+# 3 is prepended. The dot (.) at the beginning of the MAN_EXTENSION tag is
+# optional.
+# The default value is: .3.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_EXTENSION          = .3
+
+# The MAN_SUBDIR tag determines the name of the directory created within
+# MAN_OUTPUT in which the man pages are placed. If defaults to man followed by
+# MAN_EXTENSION with the initial . removed.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_SUBDIR             =
+
+# If the MAN_LINKS tag is set to YES and doxygen generates man output, then it
+# will generate one additional man file for each entity documented in the real
+# man page(s). These additional files only source the real man page, but without
+# them the man command would be unable to find the correct page.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_LINKS              = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the XML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_XML tag is set to YES, doxygen will generate an XML file that
+# captures the structure of the code including all documentation.
+# The default value is: NO.
+
+GENERATE_XML           = NO
+
+# The XML_OUTPUT tag is used to specify where the XML pages will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: xml.
+# This tag requires that the tag GENERATE_XML is set to YES.
+
+XML_OUTPUT             = xml
+
+# If the XML_PROGRAMLISTING tag is set to YES, doxygen will dump the program
+# listings (including syntax highlighting and cross-referencing information) to
+# the XML output. Note that enabling this will significantly increase the size
+# of the XML output.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_XML is set to YES.
+
+XML_PROGRAMLISTING     = YES
+
+#---------------------------------------------------------------------------
+# Configuration options related to the DOCBOOK output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_DOCBOOK tag is set to YES, doxygen will generate Docbook files
+# that can be used to generate PDF.
+# The default value is: NO.
+
+GENERATE_DOCBOOK       = NO
+
+# The DOCBOOK_OUTPUT tag is used to specify where the Docbook pages will be put.
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be put in
+# front of it.
+# The default directory is: docbook.
+# This tag requires that the tag GENERATE_DOCBOOK is set to YES.
+
+DOCBOOK_OUTPUT         = docbook
+
+# If the DOCBOOK_PROGRAMLISTING tag is set to YES, doxygen will include the
+# program listings (including syntax highlighting and cross-referencing
+# information) to the DOCBOOK output. Note that enabling this will significantly
+# increase the size of the DOCBOOK output.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_DOCBOOK is set to YES.
+
+DOCBOOK_PROGRAMLISTING = NO
+
+#---------------------------------------------------------------------------
+# Configuration options for the AutoGen Definitions output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_AUTOGEN_DEF tag is set to YES, doxygen will generate an
+# AutoGen Definitions (see http://autogen.sourceforge.net/) file that captures
+# the structure of the code including all documentation. Note that this feature
+# is still experimental and incomplete at the moment.
+# The default value is: NO.
+
+GENERATE_AUTOGEN_DEF   = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the Perl module output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_PERLMOD tag is set to YES, doxygen will generate a Perl module
+# file that captures the structure of the code including all documentation.
+#
+# Note that this feature is still experimental and incomplete at the moment.
+# The default value is: NO.
+
+GENERATE_PERLMOD       = NO
+
+# If the PERLMOD_LATEX tag is set to YES, doxygen will generate the necessary
+# Makefile rules, Perl scripts and LaTeX code to be able to generate PDF and DVI
+# output from the Perl module output.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_PERLMOD is set to YES.
+
+PERLMOD_LATEX          = NO
+
+# If the PERLMOD_PRETTY tag is set to YES, the Perl module output will be nicely
+# formatted so it can be parsed by a human reader. This is useful if you want to
+# understand what is going on. On the other hand, if this tag is set to NO, the
+# size of the Perl module output will be much smaller and Perl will parse it
+# just the same.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_PERLMOD is set to YES.
+
+PERLMOD_PRETTY         = YES
+
+# The names of the make variables in the generated doxyrules.make file are
+# prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. This is useful
+# so different doxyrules.make files included by the same Makefile don't
+# overwrite each other's variables.
+# This tag requires that the tag GENERATE_PERLMOD is set to YES.
+
+PERLMOD_MAKEVAR_PREFIX =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the preprocessor
+#---------------------------------------------------------------------------
+
+# If the ENABLE_PREPROCESSING tag is set to YES, doxygen will evaluate all
+# C-preprocessor directives found in the sources and include files.
+# The default value is: YES.
+
+ENABLE_PREPROCESSING   = YES
+
+# If the MACRO_EXPANSION tag is set to YES, doxygen will expand all macro names
+# in the source code. If set to NO, only conditional compilation will be
+# performed. Macro expansion can be done in a controlled way by setting
+# EXPAND_ONLY_PREDEF to YES.
+# The default value is: NO.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+MACRO_EXPANSION        = NO
+
+# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES then
+# the macro expansion is limited to the macros specified with the PREDEFINED and
+# EXPAND_AS_DEFINED tags.
+# The default value is: NO.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+EXPAND_ONLY_PREDEF     = NO
+
+# If the SEARCH_INCLUDES tag is set to YES, the include files in the
+# INCLUDE_PATH will be searched if a #include is found.
+# The default value is: YES.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+SEARCH_INCLUDES        = YES
+
+# The INCLUDE_PATH tag can be used to specify one or more directories that
+# contain include files that are not input files but should be processed by the
+# preprocessor.
+# This tag requires that the tag SEARCH_INCLUDES is set to YES.
+
+INCLUDE_PATH           =
+
+# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard
+# patterns (like *.h and *.hpp) to filter out the header-files in the
+# directories. If left blank, the patterns specified with FILE_PATTERNS will be
+# used.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+INCLUDE_FILE_PATTERNS  =
+
+# The PREDEFINED tag can be used to specify one or more macro names that are
+# defined before the preprocessor is started (similar to the -D option of e.g.
+# gcc). The argument of the tag is a list of macros of the form: name or
+# name=definition (no spaces). If the definition and the "=" are omitted, "=1"
+# is assumed. To prevent a macro definition from being undefined via #undef or
+# recursively expanded use the := operator instead of the = operator.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+PREDEFINED             = DOXYGEN
+
+# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this
+# tag can be used to specify a list of macro names that should be expanded. The
+# macro definition that is found in the sources will be used. Use the PREDEFINED
+# tag if you want to use a different macro definition that overrules the
+# definition found in the source code.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+EXPAND_AS_DEFINED      =
+
+# If the SKIP_FUNCTION_MACROS tag is set to YES then doxygen's preprocessor will
+# remove all references to function-like macros that are alone on a line, have
+# an all uppercase name, and do not end with a semicolon. Such function macros
+# are typically used for boiler-plate code, and will confuse the parser if not
+# removed.
+# The default value is: YES.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+SKIP_FUNCTION_MACROS   = YES
+
+#---------------------------------------------------------------------------
+# Configuration options related to external references
+#---------------------------------------------------------------------------
+
+# The TAGFILES tag can be used to specify one or more tag files. For each tag
+# file the location of the external documentation should be added. The format of
+# a tag file without this location is as follows:
+# TAGFILES = file1 file2 ...
+# Adding location for the tag files is done as follows:
+# TAGFILES = file1=loc1 "file2 = loc2" ...
+# where loc1 and loc2 can be relative or absolute paths or URLs. See the
+# section "Linking to external documentation" for more information about the use
+# of tag files.
+# Note: Each tag file must have a unique name (where the name does NOT include
+# the path). If a tag file is not located in the directory in which doxygen is
+# run, you must also specify the path to the tagfile here.
+
+TAGFILES               =
+
+# When a file name is specified after GENERATE_TAGFILE, doxygen will create a
+# tag file that is based on the input files it reads. See section "Linking to
+# external documentation" for more information about the usage of tag files.
+
+GENERATE_TAGFILE       =
+
+# If the ALLEXTERNALS tag is set to YES, all external class will be listed in
+# the class index. If set to NO, only the inherited external classes will be
+# listed.
+# The default value is: NO.
+
+ALLEXTERNALS           = NO
+
+# If the EXTERNAL_GROUPS tag is set to YES, all external groups will be listed
+# in the modules index. If set to NO, only the current project's groups will be
+# listed.
+# The default value is: YES.
+
+EXTERNAL_GROUPS        = YES
+
+# If the EXTERNAL_PAGES tag is set to YES, all external pages will be listed in
+# the related pages index. If set to NO, only the current project's pages will
+# be listed.
+# The default value is: YES.
+
+EXTERNAL_PAGES         = YES
+
+# The PERL_PATH should be the absolute path and name of the perl script
+# interpreter (i.e. the result of 'which perl').
+# The default file (with absolute path) is: /usr/bin/perl.
+
+#PERL_PATH              = /usr/bin/perl
+
+#---------------------------------------------------------------------------
+# Configuration options related to the dot tool
+#---------------------------------------------------------------------------
+
+# If the CLASS_DIAGRAMS tag is set to YES, doxygen will generate a class diagram
+# (in HTML and LaTeX) for classes with base or super classes. Setting the tag to
+# NO turns the diagrams off. Note that this option also works with HAVE_DOT
+# disabled, but it is recommended to install and use dot, since it yields more
+# powerful graphs.
+# The default value is: YES.
+
+CLASS_DIAGRAMS         = YES
+
+# You can define message sequence charts within doxygen comments using the \msc
+# command. Doxygen will then run the mscgen tool (see:
+# http://www.mcternan.me.uk/mscgen/)) to produce the chart and insert it in the
+# documentation. The MSCGEN_PATH tag allows you to specify the directory where
+# the mscgen tool resides. If left empty the tool is assumed to be found in the
+# default search path.
+
+#MSCGEN_PATH            =
+
+# You can include diagrams made with dia in doxygen documentation. Doxygen will
+# then run dia to produce the diagram and insert it in the documentation. The
+# DIA_PATH tag allows you to specify the directory where the dia binary resides.
+# If left empty dia is assumed to be found in the default search path.
+
+DIA_PATH               =
+
+# If set to YES the inheritance and collaboration graphs will hide inheritance
+# and usage relations if the target is undocumented or is not a class.
+# The default value is: YES.
+
+HIDE_UNDOC_RELATIONS   = YES
+
+# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is
+# available from the path. This tool is part of Graphviz (see:
+# http://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent
+# Bell Labs. The other options in this section have no effect if this option is
+# set to NO
+# The default value is: NO.
+
+HAVE_DOT               = NO
+
+# The DOT_NUM_THREADS specifies the number of dot invocations doxygen is allowed
+# to run in parallel. When set to 0 doxygen will base this on the number of
+# processors available in the system. You can set it explicitly to a value
+# larger than 0 to get control over the balance between CPU load and processing
+# speed.
+# Minimum value: 0, maximum value: 32, default value: 0.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_NUM_THREADS        = 0
+
+# When you want a differently looking font in the dot files that doxygen
+# generates you can specify the font name using DOT_FONTNAME. You need to make
+# sure dot is able to find the font, which can be done by putting it in a
+# standard location or by setting the DOTFONTPATH environment variable or by
+# setting DOT_FONTPATH to the directory containing the font.
+# The default value is: Helvetica.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_FONTNAME           = Helvetica
+
+# The DOT_FONTSIZE tag can be used to set the size (in points) of the font of
+# dot graphs.
+# Minimum value: 4, maximum value: 24, default value: 10.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_FONTSIZE           = 10
+
+# By default doxygen will tell dot to use the default font as specified with
+# DOT_FONTNAME. If you specify a different font using DOT_FONTNAME you can set
+# the path where dot can find it using this tag.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_FONTPATH           =
+
+# If the CLASS_GRAPH tag is set to YES then doxygen will generate a graph for
+# each documented class showing the direct and indirect inheritance relations.
+# Setting this tag to YES will force the CLASS_DIAGRAMS tag to NO.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+CLASS_GRAPH            = YES
+
+# If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a
+# graph for each documented class showing the direct and indirect implementation
+# dependencies (inheritance, containment, and class references variables) of the
+# class with other documented classes.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+COLLABORATION_GRAPH    = YES
+
+# If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for
+# groups, showing the direct groups dependencies.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+GROUP_GRAPHS           = YES
+
+# If the UML_LOOK tag is set to YES, doxygen will generate inheritance and
+# collaboration diagrams in a style similar to the OMG's Unified Modeling
+# Language.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+UML_LOOK               = NO
+
+# If the UML_LOOK tag is enabled, the fields and methods are shown inside the
+# class node. If there are many fields or methods and many nodes the graph may
+# become too big to be useful. The UML_LIMIT_NUM_FIELDS threshold limits the
+# number of items for each type to make the size more manageable. Set this to 0
+# for no limit. Note that the threshold may be exceeded by 50% before the limit
+# is enforced. So when you set the threshold to 10, up to 15 fields may appear,
+# but if the number exceeds 15, the total amount of fields shown is limited to
+# 10.
+# Minimum value: 0, maximum value: 100, default value: 10.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+UML_LIMIT_NUM_FIELDS   = 10
+
+# If the TEMPLATE_RELATIONS tag is set to YES then the inheritance and
+# collaboration graphs will show the relations between templates and their
+# instances.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+TEMPLATE_RELATIONS     = NO
+
+# If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are set to
+# YES then doxygen will generate a graph for each documented file showing the
+# direct and indirect include dependencies of the file with other documented
+# files.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+INCLUDE_GRAPH          = YES
+
+# If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are
+# set to YES then doxygen will generate a graph for each documented file showing
+# the direct and indirect include dependencies of the file with other documented
+# files.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+INCLUDED_BY_GRAPH      = YES
+
+# If the CALL_GRAPH tag is set to YES then doxygen will generate a call
+# dependency graph for every global function or class method.
+#
+# Note that enabling this option will significantly increase the time of a run.
+# So in most cases it will be better to enable call graphs for selected
+# functions only using the \callgraph command. Disabling a call graph can be
+# accomplished by means of the command \hidecallgraph.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+CALL_GRAPH             = NO
+
+# If the CALLER_GRAPH tag is set to YES then doxygen will generate a caller
+# dependency graph for every global function or class method.
+#
+# Note that enabling this option will significantly increase the time of a run.
+# So in most cases it will be better to enable caller graphs for selected
+# functions only using the \callergraph command. Disabling a caller graph can be
+# accomplished by means of the command \hidecallergraph.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+CALLER_GRAPH           = NO
+
+# If the GRAPHICAL_HIERARCHY tag is set to YES then doxygen will graphical
+# hierarchy of all classes instead of a textual one.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+GRAPHICAL_HIERARCHY    = YES
+
+# If the DIRECTORY_GRAPH tag is set to YES then doxygen will show the
+# dependencies a directory has on other directories in a graphical way. The
+# dependency relations are determined by the #include relations between the
+# files in the directories.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DIRECTORY_GRAPH        = YES
+
+# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images
+# generated by dot. For an explanation of the image formats see the section
+# output formats in the documentation of the dot tool (Graphviz (see:
+# http://www.graphviz.org/)).
+# Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order
+# to make the SVG files visible in IE 9+ (other browsers do not have this
+# requirement).
+# Possible values are: png, jpg, gif, svg, png:gd, png:gd:gd, png:cairo,
+# png:cairo:gd, png:cairo:cairo, png:cairo:gdiplus, png:gdiplus and
+# png:gdiplus:gdiplus.
+# The default value is: png.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_IMAGE_FORMAT       = png
+
+# If DOT_IMAGE_FORMAT is set to svg, then this option can be set to YES to
+# enable generation of interactive SVG images that allow zooming and panning.
+#
+# Note that this requires a modern browser other than Internet Explorer. Tested
+# and working are Firefox, Chrome, Safari, and Opera.
+# Note: For IE 9+ you need to set HTML_FILE_EXTENSION to xhtml in order to make
+# the SVG files visible. Older versions of IE do not have SVG support.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+INTERACTIVE_SVG        = NO
+
+# The DOT_PATH tag can be used to specify the path where the dot tool can be
+# found. If left blank, it is assumed the dot tool can be found in the path.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_PATH               =
+
+# The DOTFILE_DIRS tag can be used to specify one or more directories that
+# contain dot files that are included in the documentation (see the \dotfile
+# command).
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOTFILE_DIRS           =
+
+# The MSCFILE_DIRS tag can be used to specify one or more directories that
+# contain msc files that are included in the documentation (see the \mscfile
+# command).
+
+MSCFILE_DIRS           =
+
+# The DIAFILE_DIRS tag can be used to specify one or more directories that
+# contain dia files that are included in the documentation (see the \diafile
+# command).
+
+DIAFILE_DIRS           =
+
+# When using plantuml, the PLANTUML_JAR_PATH tag should be used to specify the
+# path where java can find the plantuml.jar file. If left blank, it is assumed
+# PlantUML is not used or called during a preprocessing step. Doxygen will
+# generate a warning when it encounters a \startuml command in this case and
+# will not generate output for the diagram.
+
+PLANTUML_JAR_PATH      =
+
+# When using plantuml, the PLANTUML_CFG_FILE tag can be used to specify a
+# configuration file for plantuml.
+
+PLANTUML_CFG_FILE      =
+
+# When using plantuml, the specified paths are searched for files specified by
+# the !include statement in a plantuml block.
+
+PLANTUML_INCLUDE_PATH  =
+
+# The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of nodes
+# that will be shown in the graph. If the number of nodes in a graph becomes
+# larger than this value, doxygen will truncate the graph, which is visualized
+# by representing a node as a red box. Note that doxygen if the number of direct
+# children of the root node in a graph is already larger than
+# DOT_GRAPH_MAX_NODES then the graph will not be shown at all. Also note that
+# the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH.
+# Minimum value: 0, maximum value: 10000, default value: 50.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_GRAPH_MAX_NODES    = 50
+
+# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the graphs
+# generated by dot. A depth value of 3 means that only nodes reachable from the
+# root by following a path via at most 3 edges will be shown. Nodes that lay
+# further from the root node will be omitted. Note that setting this option to 1
+# or 2 may greatly reduce the computation time needed for large code bases. Also
+# note that the size of a graph can be further restricted by
+# DOT_GRAPH_MAX_NODES. Using a depth of 0 means no depth restriction.
+# Minimum value: 0, maximum value: 1000, default value: 0.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+MAX_DOT_GRAPH_DEPTH    = 0
+
+# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent
+# background. This is disabled by default, because dot on Windows does not seem
+# to support this out of the box.
+#
+# Warning: Depending on the platform used, enabling this option may lead to
+# badly anti-aliased labels on the edges of a graph (i.e. they become hard to
+# read).
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_TRANSPARENT        = NO
+
+# Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output
+# files in one run (i.e. multiple -o and -T options on the command line). This
+# makes dot run faster, but since only newer versions of dot (>1.8.10) support
+# this, this feature is disabled by default.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_MULTI_TARGETS      = NO
+
+# If the GENERATE_LEGEND tag is set to YES doxygen will generate a legend page
+# explaining the meaning of the various boxes and arrows in the dot generated
+# graphs.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+GENERATE_LEGEND        = YES
+
+# If the DOT_CLEANUP tag is set to YES, doxygen will remove the intermediate dot
+# files that are used to generate the various graphs.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_CLEANUP            = YES
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d64569567
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..cc65228
--- /dev/null
+++ b/README.md
@@ -0,0 +1,28 @@
+# Tint
+
+Tint is a compiler for the WebGPU Shader Language (WGSL).
+
+This is not an officially supported Google product.
+
+## Requirements
+ * Git
+ * CMake (3.10.2 or later)
+ * Ninja (or other build tool)
+ * Python, for fetching dependencies
+
+## Build options
+ * `TINT_BUILD_SPV_PARSER` : enable the SPIR-V input parser
+
+## Building
+
+```
+./tools/git-sync-deps
+mkdir -p out/Debug
+cd out/Debug
+cmake -GNinja ../..
+ninja
+```
+
+## Contributing
+Please see the CONTRIBUTING and CODE_OF_CONDUCT files on how to contribute to
+Tint.
diff --git a/fuzz/CMakeLists.txt b/fuzz/CMakeLists.txt
new file mode 100644
index 0000000..d191c54
--- /dev/null
+++ b/fuzz/CMakeLists.txt
@@ -0,0 +1,29 @@
+# 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.
+
+add_executable(tint_fuzz tint_fuzz.cc)
+target_link_libraries(tint_fuzz libtint)
+tint_default_compile_options(tint_fuzz)
+target_link_options(tint_fuzz PRIVATE
+  -fno-omit-frame-pointer
+  -fsanitize=fuzzer,address,undefined
+  -fsanitize-address-use-after-scope
+  -O1
+  -g
+)
+target_compile_options(tint_fuzz PRIVATE
+  -fsanitize=fuzzer,address,undefined
+  -Wno-missing-prototypes
+)
+
diff --git a/fuzz/tint_fuzz.cc b/fuzz/tint_fuzz.cc
new file mode 100644
index 0000000..f6d3657
--- /dev/null
+++ b/fuzz/tint_fuzz.cc
@@ -0,0 +1,27 @@
+// 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 <string>
+
+#include "src/reader/wgsl/parser.h"
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
+  std::string str(reinterpret_cast<const char*>(data), size);
+
+  tint::reader::wgsl::Parser parser(str);
+  parser.Parse();
+
+  return 0;
+}
+
diff --git a/samples/CMakeLists.txt b/samples/CMakeLists.txt
new file mode 100644
index 0000000..5fb3f61
--- /dev/null
+++ b/samples/CMakeLists.txt
@@ -0,0 +1,23 @@
+# 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.
+
+set(TINT_SRCS
+  main.cc
+)
+
+## Tint executable
+add_executable(tint ${TINT_SRCS})
+target_link_libraries(tint libtint SPIRV-Tools)
+tint_default_compile_options(tint)
+
diff --git a/samples/main.cc b/samples/main.cc
new file mode 100644
index 0000000..eb98ec6
--- /dev/null
+++ b/samples/main.cc
@@ -0,0 +1,278 @@
+// 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 <fstream>
+#include <iostream>
+#include <memory>
+#include <vector>
+
+#include "spirv-tools/libspirv.hpp"
+#include "src/reader/reader.h"
+#include "src/reader/wgsl/parser.h"
+#include "src/type_determiner.h"
+#include "src/validator.h"
+#include "src/writer/spv/generator.h"
+#include "src/writer/wgsl/generator.h"
+#include "src/writer/writer.h"
+
+namespace {
+
+enum class Format {
+  kNone = -1,
+  kSpirv,
+  kSpvAsm,
+  kWgsl,
+};
+
+struct Options {
+  bool show_help = false;
+
+  std::string input_filename;
+  std::string output_name = "";
+  std::string output_ext = "spv";
+
+  bool parse_only = false;
+  bool dump_ast = false;
+
+  Format format = Format::kSpirv;
+};
+
+const char kUsage[] = R"(Usage: tint [options] SCRIPT [SCRIPTS...]
+
+ options:
+  --format <spirv|spvasm|wgsl>  -- Output format
+  --output-name <name>      -- Name for the output file, without extension
+  --parse-only              -- Stop after parsing the input
+  --dump-ast                -- Dump the generated AST to stdout
+  -h                        -- This help text)";
+
+Format parse_format(const std::string& fmt) {
+  if (fmt == "spirv")
+    return Format::kSpirv;
+  if (fmt == "spvasm")
+    return Format::kSpvAsm;
+  if (fmt == "wgsl")
+    return Format::kWgsl;
+
+  return Format::kNone;
+}
+
+bool ParseArgs(const std::vector<std::string>& args, Options* opts) {
+  for (size_t i = 1; i < args.size(); ++i) {
+    const std::string& arg = args[i];
+    if (arg == "--format") {
+      ++i;
+      if (i >= args.size()) {
+        std::cerr << "Missing value for --format argument." << std::endl;
+        return false;
+      }
+      opts->format = parse_format(args[i]);
+
+      if (opts->format == Format::kNone) {
+        std::cerr << "Unknown output format: " << args[i] << std::endl;
+        return false;
+      }
+
+      if (opts->format == Format::kSpvAsm)
+        opts->output_ext = "spvasm";
+      else if (opts->format == Format::kWgsl)
+        opts->output_ext = "wgsl";
+    }
+    if (arg == "--output-name") {
+      ++i;
+      if (i >= args.size()) {
+        std::cerr << "Missing value for --output_name argument." << std::endl;
+        return false;
+      }
+      opts->output_name = args[i];
+
+    } else if (arg == "-h" || arg == "--help") {
+      opts->show_help = true;
+    } else if (arg == "--parse-only") {
+      opts->parse_only = true;
+    } else if (arg == "--dump-ast") {
+      opts->dump_ast = true;
+    } else if (!arg.empty()) {
+      opts->input_filename = arg;
+    }
+  }
+  return true;
+}
+
+std::vector<uint8_t> ReadFile(const std::string& input_file) {
+  FILE* file = nullptr;
+#if defined(_MSC_VER)
+  fopen_s(&file, input_file.c_str(), "rb");
+#else
+  file = fopen(input_file.c_str(), "rb");
+#endif
+  if (!file) {
+    std::cerr << "Failed to open " << input_file << std::endl;
+    return {};
+  }
+
+  fseek(file, 0, SEEK_END);
+  uint64_t tell_file_size = static_cast<uint64_t>(ftell(file));
+  if (tell_file_size <= 0) {
+    std::cerr << "Input file of incorrect size: " << input_file << std::endl;
+    fclose(file);
+    return {};
+  }
+  fseek(file, 0, SEEK_SET);
+
+  size_t file_size = static_cast<size_t>(tell_file_size);
+
+  std::vector<uint8_t> data;
+  data.resize(file_size);
+
+  size_t bytes_read = fread(data.data(), sizeof(uint8_t), file_size, file);
+  fclose(file);
+  if (bytes_read != file_size) {
+    std::cerr << "Failed to read " << input_file << std::endl;
+    return {};
+  }
+
+  return data;
+}
+
+std::string Disassemble(const std::vector<uint32_t>& data) {
+  std::string spv_errors;
+  spv_target_env target_env = SPV_ENV_UNIVERSAL_1_0;
+
+  auto msg_consumer = [&spv_errors](spv_message_level_t level, const char*,
+                                    const spv_position_t& position,
+                                    const char* message) {
+    switch (level) {
+      case SPV_MSG_FATAL:
+      case SPV_MSG_INTERNAL_ERROR:
+      case SPV_MSG_ERROR:
+        spv_errors += "error: line " + std::to_string(position.index) + ": " +
+                      message + "\n";
+        break;
+      case SPV_MSG_WARNING:
+        spv_errors += "warning: line " + std::to_string(position.index) + ": " +
+                      message + "\n";
+        break;
+      case SPV_MSG_INFO:
+        spv_errors += "info: line " + std::to_string(position.index) + ": " +
+                      message + "\n";
+        break;
+      case SPV_MSG_DEBUG:
+        break;
+    }
+  };
+
+  spvtools::SpirvTools tools(target_env);
+  tools.SetMessageConsumer(msg_consumer);
+
+  std::string result;
+  tools.Disassemble(data, &result,
+                    SPV_BINARY_TO_TEXT_OPTION_INDENT |
+                        SPV_BINARY_TO_TEXT_OPTION_FRIENDLY_NAMES);
+  return result;
+}
+
+}  // namespace
+
+int main(int argc, const char** argv) {
+  std::vector<std::string> args(argv, argv + argc);
+  Options options;
+
+  if (!ParseArgs(args, &options)) {
+    std::cerr << "Failed to parse arguments." << std::endl;
+    return 1;
+  }
+
+  if (options.show_help) {
+    std::cout << kUsage << std::endl;
+    return 0;
+  }
+  if (options.input_filename == "") {
+    std::cerr << "Input file missing" << std::endl;
+    std::cout << kUsage << std::endl;
+    return 1;
+  }
+
+  auto data = ReadFile(options.input_filename);
+  if (data.size() == 0)
+    return 1;
+
+  std::unique_ptr<tint::reader::Reader> reader;
+  std::string ext = "wgsl";
+  if (options.input_filename.size() > 4 &&
+      options.input_filename.substr(options.input_filename.size() - 4) ==
+          "wgsl") {
+    reader = std::make_unique<tint::reader::wgsl::Parser>(
+        std::string(data.begin(), data.end()));
+  }
+  if (!reader) {
+    std::cerr << "Failed to create reader for input file: "
+              << options.input_filename << std::endl;
+    return 1;
+  }
+  if (!reader->Parse()) {
+    std::cerr << reader->error() << std::endl;
+    return 1;
+  }
+
+  auto module = reader->module();
+  if (options.dump_ast) {
+    std::cout << std::endl << module.to_str() << std::endl;
+  }
+  if (options.parse_only) {
+    return 1;
+  }
+
+  tint::TypeDeterminer td;
+  if (!td.Determine(&module)) {
+    std::cerr << td.error() << std::endl;
+    return 1;
+  }
+
+  tint::Validator v;
+  if (!v.Validate(module)) {
+    std::cerr << v.error() << std::endl;
+    return 1;
+  }
+
+  std::unique_ptr<tint::writer::Writer> writer;
+  if (options.format == Format::kSpirv || options.format == Format::kSpvAsm) {
+    writer = std::make_unique<tint::writer::spv::Generator>(std::move(module));
+  } else if (options.format == Format::kWgsl) {
+    writer = std::make_unique<tint::writer::wgsl::Generator>(std::move(module));
+  } else {
+    std::cerr << "Unknown output format specified" << std::endl;
+    return 1;
+  }
+
+  if (!writer->Generate()) {
+    std::cerr << "Failed to generate SPIR-V: " << writer->error() << std::endl;
+    return 1;
+  }
+
+  if (options.format == Format::kSpvAsm) {
+    auto w = static_cast<tint::writer::spv::Generator*>(writer.get());
+    auto str = Disassemble(w->result());
+    // TODO(dsinclair): Write to file if output_file given
+    std::cout << str << std::endl;
+  } else if (options.format == Format::kSpirv) {
+    // auto w = static_cast<tint::writer::spv::Generator*>(writer.get());
+    // TODO(dsincliair): Write to to file
+  } else if (options.format == Format::kWgsl) {
+    auto w = static_cast<tint::writer::wgsl::Generator*>(writer.get());
+    std::cout << w->result() << std::endl;
+  }
+
+  return 0;
+}
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
new file mode 100644
index 0000000..3d71fc7
--- /dev/null
+++ b/src/CMakeLists.txt
@@ -0,0 +1,310 @@
+# 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.
+
+set(TINT_LIB_SRCS
+  ast/array_accessor_expression.cc
+  ast/array_accessor_expression.h
+  ast/as_expression.cc
+  ast/as_expression.h
+  ast/assignment_statement.cc
+  ast/assignment_statement.h
+  ast/binding_decoration.cc
+  ast/binding_decoration.h
+  ast/bool_literal.h
+  ast/bool_literal.cc
+  ast/break_statement.cc
+  ast/break_statement.h
+  ast/builtin.cc
+  ast/builtin.h
+  ast/builtin_decoration.cc
+  ast/builtin_decoration.h
+  ast/call_expression.cc
+  ast/call_expression.h
+  ast/case_statement.cc
+  ast/case_statement.h
+  ast/cast_expression.cc
+  ast/cast_expression.h
+  ast/const_initializer_expression.cc
+  ast/const_initializer_expression.h
+  ast/continue_statement.cc
+  ast/continue_statement.h
+  ast/decorated_variable.cc
+  ast/decorated_variable.h
+  ast/derivative_modifier.cc
+  ast/derivative_modifier.h
+  ast/else_statement.cc
+  ast/else_statement.h
+  ast/entry_point.cc
+  ast/entry_point.h
+  ast/expression.cc
+  ast/expression.h
+  ast/fallthrough_statement.cc
+  ast/fallthrough_statement.h
+  ast/float_literal.cc
+  ast/float_literal.h
+  ast/function.cc
+  ast/function.h
+  ast/identifier_expression.cc
+  ast/identifier_expression.h
+  ast/if_statement.cc
+  ast/if_statement.h
+  ast/import.cc
+  ast/import.h
+  ast/initializer_expression.cc
+  ast/initializer_expression.h
+  ast/int_literal.cc
+  ast/int_literal.h
+  ast/kill_statement.cc
+  ast/kill_statement.h
+  ast/literal.h
+  ast/literal.cc
+  ast/location_decoration.cc
+  ast/location_decoration.h
+  ast/loop_statement.cc
+  ast/loop_statement.h
+  ast/member_accessor_expression.cc
+  ast/member_accessor_expression.h
+  ast/module.cc
+  ast/module.h
+  ast/node.cc
+  ast/node.h
+  ast/nop_statement.cc
+  ast/nop_statement.h
+  ast/pipeline_stage.cc
+  ast/pipeline_stage.h
+  ast/regardless_statement.cc
+  ast/regardless_statement.h
+  ast/relational_expression.cc
+  ast/relational_expression.h
+  ast/return_statement.cc
+  ast/return_statement.h
+  ast/set_decoration.cc
+  ast/set_decoration.h
+  ast/statement.cc
+  ast/statement.h
+  ast/statement_condition.cc
+  ast/statement_condition.h
+  ast/storage_class.cc
+  ast/storage_class.h
+  ast/struct_decoration.cc
+  ast/struct_decoration.h
+  ast/struct.cc
+  ast/struct.h
+  ast/struct_member.cc
+  ast/struct_member.h
+  ast/struct_member_decoration.cc
+  ast/struct_member_decoration.h
+  ast/struct_member_offset_decoration.cc
+  ast/struct_member_offset_decoration.h
+  ast/switch_statement.cc
+  ast/switch_statement.h
+  ast/type_initializer_expression.h
+  ast/type_initializer_expression.cc
+  ast/type/alias_type.cc
+  ast/type/alias_type.h
+  ast/type/array_type.cc
+  ast/type/array_type.h
+  ast/type/bool_type.cc
+  ast/type/bool_type.h
+  ast/type/f32_type.cc
+  ast/type/f32_type.h
+  ast/type/i32_type.cc
+  ast/type/i32_type.h
+  ast/type/matrix_type.cc
+  ast/type/matrix_type.h
+  ast/type/pointer_type.cc
+  ast/type/pointer_type.h
+  ast/type/struct_type.cc
+  ast/type/struct_type.h
+  ast/type/type.cc
+  ast/type/type.h
+  ast/type/u32_type.cc
+  ast/type/u32_type.h
+  ast/type/vector_type.cc
+  ast/type/vector_type.h
+  ast/type/void_type.cc
+  ast/type/void_type.h
+  ast/uint_literal.cc
+  ast/uint_literal.h
+  ast/unary_derivative.cc
+  ast/unary_derivative.h
+  ast/unary_derivative_expression.cc
+  ast/unary_derivative_expression.h
+  ast/unary_method.cc
+  ast/unary_method.h
+  ast/unary_method_expression.cc
+  ast/unary_method_expression.h
+  ast/unary_op.cc
+  ast/unary_op.h
+  ast/unary_op_expression.cc
+  ast/unary_op_expression.h
+  ast/unless_statement.cc
+  ast/unless_statement.h
+  ast/variable.cc
+  ast/variable.h
+  ast/variable_decoration.cc
+  ast/variable_decoration.h
+  ast/variable_statement.cc
+  ast/variable_statement.h
+  reader/reader.cc
+  reader/reader.h
+  reader/wgsl/lexer.cc
+  reader/wgsl/lexer.h
+  reader/wgsl/parser.cc
+  reader/wgsl/parser.h
+  reader/wgsl/parser_impl.cc
+  reader/wgsl/parser_impl.h
+  reader/wgsl/token.cc
+  reader/wgsl/token.h
+  source.h
+  type_determiner.cc
+  type_determiner.h
+  type_manager.cc
+  type_manager.h
+  validator.cc
+  validator.h
+  # TODO(dsinclair): The writers should all be optional
+  writer/spv/generator.cc
+  writer/spv/generator.h
+  writer/wgsl/generator.cc
+  writer/wgsl/generator.h
+  writer/writer.cc
+  writer/writer.h
+)
+
+if(TINT_BUILD_SPV_PARSER)
+  list(APPEND TINT_LIB_SRCS
+    reader/spv/parser.cc
+    reader/spv/parser.h
+  )
+endif()
+
+set(TINT_TEST_SRCS
+  ast/binding_decoration_test.cc
+  ast/bool_literal_test.cc
+  ast/builtin_decoration_test.cc
+  ast/entry_point_test.cc
+  ast/import_test.cc
+  ast/int_literal_test.cc
+  ast/location_decoration_test.cc
+  ast/module_test.cc
+  ast/set_decoration_test.cc
+  ast/struct_member_test.cc
+  ast/struct_member_offset_decoration_test.cc
+  ast/struct_test.cc
+  ast/type/alias_type_test.cc
+  ast/type/array_type_test.cc
+  ast/type/bool_type_test.cc
+  ast/type/f32_type_test.cc
+  ast/type/i32_type_test.cc
+  ast/type/matrix_type_test.cc
+  ast/type/pointer_type_test.cc
+  ast/type/struct_type_test.cc
+  ast/type/u32_type_test.cc
+  ast/type/vector_type_test.cc
+  ast/uint_literal_test.cc
+  ast/variable_test.cc
+  reader/wgsl/lexer_test.cc
+  reader/wgsl/parser_test.cc
+  reader/wgsl/parser_impl_additive_expression_test.cc
+  reader/wgsl/parser_impl_and_expression_test.cc
+  reader/wgsl/parser_impl_argument_expression_list_test.cc
+  reader/wgsl/parser_impl_assignment_stmt_test.cc
+  reader/wgsl/parser_impl_body_stmt_test.cc
+  reader/wgsl/parser_impl_break_stmt_test.cc
+  reader/wgsl/parser_impl_builtin_decoration_test.cc
+  reader/wgsl/parser_impl_case_body_test.cc
+  reader/wgsl/parser_impl_const_expr_test.cc
+  reader/wgsl/parser_impl_const_literal_test.cc
+  reader/wgsl/parser_impl_continue_stmt_test.cc
+  reader/wgsl/parser_impl_continuing_stmt_test.cc
+  reader/wgsl/parser_impl_derivative_modifier_test.cc
+  reader/wgsl/parser_impl_else_stmt_test.cc
+  reader/wgsl/parser_impl_elseif_stmt_test.cc
+  reader/wgsl/parser_impl_entry_point_decl_test.cc
+  reader/wgsl/parser_impl_equality_expression_test.cc
+  reader/wgsl/parser_impl_exclusive_or_expression_test.cc
+  reader/wgsl/parser_impl_function_decl_test.cc
+  reader/wgsl/parser_impl_function_header_test.cc
+  reader/wgsl/parser_impl_function_type_decl_test.cc
+  reader/wgsl/parser_impl_global_constant_decl_test.cc
+  reader/wgsl/parser_impl_global_decl_test.cc
+  reader/wgsl/parser_impl_global_variable_decl_test.cc
+  reader/wgsl/parser_impl_if_stmt_test.cc
+  reader/wgsl/parser_impl_import_decl_test.cc
+  reader/wgsl/parser_impl_inclusive_or_expression_test.cc
+  reader/wgsl/parser_impl_logical_and_expression_test.cc
+  reader/wgsl/parser_impl_logical_or_expression_test.cc
+  reader/wgsl/parser_impl_loop_stmt_test.cc
+  reader/wgsl/parser_impl_multiplicative_expression_test.cc
+  reader/wgsl/parser_impl_param_list_test.cc
+  reader/wgsl/parser_impl_paren_rhs_stmt_test.cc
+  reader/wgsl/parser_impl_pipeline_stage_test.cc
+  reader/wgsl/parser_impl_postfix_expression_test.cc
+  reader/wgsl/parser_impl_premerge_stmt_test.cc
+  reader/wgsl/parser_impl_primary_expression_test.cc
+  reader/wgsl/parser_impl_regardless_stmt_test.cc
+  reader/wgsl/parser_impl_relational_expression_test.cc
+  reader/wgsl/parser_impl_shift_expression_test.cc
+  reader/wgsl/parser_impl_statement_test.cc
+  reader/wgsl/parser_impl_statements_test.cc
+  reader/wgsl/parser_impl_storage_class_test.cc
+  reader/wgsl/parser_impl_struct_body_decl_test.cc
+  reader/wgsl/parser_impl_struct_decl_test.cc
+  reader/wgsl/parser_impl_struct_decoration_decl_test.cc
+  reader/wgsl/parser_impl_struct_decoration_test.cc
+  reader/wgsl/parser_impl_struct_member_decoration_decl_test.cc
+  reader/wgsl/parser_impl_struct_member_decoration_test.cc
+  reader/wgsl/parser_impl_struct_member_test.cc
+  reader/wgsl/parser_impl_switch_body_test.cc
+  reader/wgsl/parser_impl_switch_stmt_test.cc
+  reader/wgsl/parser_impl_test.cc
+  reader/wgsl/parser_impl_type_alias_test.cc
+  reader/wgsl/parser_impl_type_decl_test.cc
+  reader/wgsl/parser_impl_unary_expression_test.cc
+  reader/wgsl/parser_impl_unless_stmt_test.cc
+  reader/wgsl/parser_impl_variable_decl_test.cc
+  reader/wgsl/parser_impl_variable_decoration_list_test.cc
+  reader/wgsl/parser_impl_variable_decoration_test.cc
+  reader/wgsl/parser_impl_variable_ident_decl_test.cc
+  reader/wgsl/parser_impl_variable_stmt_test.cc
+  reader/wgsl/parser_impl_variable_storage_decoration_test.cc
+  reader/wgsl/token_test.cc
+  type_manager_test.cc
+)
+
+## Tint library
+add_library(libtint ${TINT_LIB_SRCS})
+tint_default_compile_options(libtint)
+set_target_properties(libtint PROPERTIES OUTPUT_NAME "tint")
+
+if(${TINT_ENABLE_SPV_PARSER})
+  target_link_libraries(libtint SPIRV-Tools)
+endif()
+
+add_executable(tint_unittests ${TINT_TEST_SRCS})
+if (NOT MSVC)
+  target_compile_options(tint_unittests PRIVATE
+    -Wno-global-constructors
+    -Wno-weak-vtables
+  )
+endif()
+
+## Test executable
+target_include_directories(
+    tint_unittests PRIVATE ${gmock_SOURCE_DIR}/include)
+target_link_libraries(tint_unittests libtint gmock_main)
+tint_default_compile_options(tint_unittests)
+
+add_test(NAME tint_unittests COMMAND tint_unittests)
diff --git a/src/ast/array_accessor_expression.cc b/src/ast/array_accessor_expression.cc
new file mode 100644
index 0000000..a98b436
--- /dev/null
+++ b/src/ast/array_accessor_expression.cc
@@ -0,0 +1,49 @@
+// 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/ast/array_accessor_expression.h"
+
+namespace tint {
+namespace ast {
+
+ArrayAccessorExpression::ArrayAccessorExpression(
+    std::unique_ptr<Expression> array,
+    std::unique_ptr<Expression> idx_expr)
+    : Expression(), array_(std::move(array)), idx_expr_(std::move(idx_expr)) {}
+
+ArrayAccessorExpression::ArrayAccessorExpression(
+    const Source& source,
+    std::unique_ptr<Expression> array,
+    std::unique_ptr<Expression> idx_expr)
+    : Expression(source),
+      array_(std::move(array)),
+      idx_expr_(std::move(idx_expr)) {}
+
+ArrayAccessorExpression::~ArrayAccessorExpression() = default;
+
+bool ArrayAccessorExpression::IsValid() const {
+  return array_ != nullptr && idx_expr_ != nullptr;
+}
+
+void ArrayAccessorExpression::to_str(std::ostream& out, size_t indent) const {
+  make_indent(out, indent);
+  out << "ArrayAccessor{" << std::endl;
+  array_->to_str(out, indent + 2);
+  idx_expr_->to_str(out, indent + 2);
+  make_indent(out, indent);
+  out << "}" << std::endl;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/array_accessor_expression.h b/src/ast/array_accessor_expression.h
new file mode 100644
index 0000000..ddffdee
--- /dev/null
+++ b/src/ast/array_accessor_expression.h
@@ -0,0 +1,83 @@
+// 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_AST_ARRAY_ACCESSOR_EXPRESSION_H_
+#define SRC_AST_ARRAY_ACCESSOR_EXPRESSION_H_
+
+#include <memory>
+#include <utility>
+
+#include "src/ast/expression.h"
+#include "src/ast/literal.h"
+
+namespace tint {
+namespace ast {
+
+/// An array accessor expression
+class ArrayAccessorExpression : public Expression {
+ public:
+  /// Constructor
+  /// @param array the array
+  /// @param idx_expr the index expression
+  ArrayAccessorExpression(std::unique_ptr<Expression> array,
+                          std::unique_ptr<Expression> idx_expr);
+  /// Constructor
+  /// @param source the initializer source
+  /// @param array the array
+  /// @param idx_expr the index expression
+  ArrayAccessorExpression(const Source& source,
+                          std::unique_ptr<Expression> array,
+                          std::unique_ptr<Expression> idx_expr);
+  /// Move constructor
+  ArrayAccessorExpression(ArrayAccessorExpression&&) = default;
+  ~ArrayAccessorExpression() override;
+
+  /// Sets the array
+  /// @param array the array
+  void set_array(std::unique_ptr<Expression> array) {
+    array_ = std::move(array);
+  }
+  /// @returns the array
+  Expression* array() const { return array_.get(); }
+
+  /// Sets the index expression
+  /// @param idx_expr the index expression
+  void set_idx_expr(std::unique_ptr<Expression> idx_expr) {
+    idx_expr_ = std::move(idx_expr);
+  }
+  /// @returns the index expression
+  Expression* idx_expr() const { return idx_expr_.get(); }
+
+  /// @returns true if this is an array accessor expression
+  bool IsArrayAccessor() const override { return true; }
+
+  /// @returns true if the node is valid
+  bool IsValid() const override;
+
+  /// Writes a representation of the node to the output stream
+  /// @param out the stream to write to
+  /// @param indent number of spaces to indent the node when writing
+  void to_str(std::ostream& out, size_t indent) const override;
+
+ private:
+  ArrayAccessorExpression(const ArrayAccessorExpression&) = delete;
+
+  std::unique_ptr<Expression> array_;
+  std::unique_ptr<Expression> idx_expr_;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_ARRAY_ACCESSOR_EXPRESSION_H_
diff --git a/src/ast/as_expression.cc b/src/ast/as_expression.cc
new file mode 100644
index 0000000..deb9564
--- /dev/null
+++ b/src/ast/as_expression.cc
@@ -0,0 +1,41 @@
+// 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/ast/as_expression.h"
+
+namespace tint {
+namespace ast {
+
+AsExpression::AsExpression(type::Type* type, std::unique_ptr<Expression> expr)
+    : Expression(), type_(type), expr_(std::move(expr)) {}
+
+AsExpression::AsExpression(const Source& source,
+                           type::Type* type,
+                           std::unique_ptr<Expression> expr)
+    : Expression(source), type_(type), expr_(std::move(expr)) {}
+
+AsExpression::~AsExpression() = default;
+
+bool AsExpression::IsValid() const {
+  return type_ != nullptr && expr_ != nullptr;
+}
+
+void AsExpression::to_str(std::ostream& out, size_t indent) const {
+  out << "as<" << type_->type_name() << ">(";
+  expr_->to_str(out, indent);
+  out << ")";
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/as_expression.h b/src/ast/as_expression.h
new file mode 100644
index 0000000..93036f8
--- /dev/null
+++ b/src/ast/as_expression.h
@@ -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.
+
+#ifndef SRC_AST_AS_EXPRESSION_H_
+#define SRC_AST_AS_EXPRESSION_H_
+
+#include <memory>
+#include <utility>
+#include <vector>
+
+#include "src/ast/expression.h"
+#include "src/ast/literal.h"
+#include "src/ast/type/type.h"
+
+namespace tint {
+namespace ast {
+
+/// An as expression
+class AsExpression : public Expression {
+ public:
+  /// Constructor
+  /// @param type the type
+  /// @param expr the expr
+  AsExpression(type::Type* type, std::unique_ptr<Expression> expr);
+  /// Constructor
+  /// @param source the initializer source
+  /// @param type the type
+  /// @param expr the expr
+  AsExpression(const Source& source,
+               type::Type* type,
+               std::unique_ptr<Expression> expr);
+  /// Move constructor
+  AsExpression(AsExpression&&) = default;
+  ~AsExpression() override;
+
+  /// Sets the type
+  /// @param type the type
+  void set_type(type::Type* type) { type_ = std::move(type); }
+  /// @returns the left side expression
+  type::Type* type() const { return type_; }
+
+  /// Sets the expr
+  /// @param expr the expression
+  void set_expr(std::unique_ptr<Expression> expr) { expr_ = std::move(expr); }
+  /// @returns the expression
+  Expression* expr() const { return expr_.get(); }
+
+  /// @returns true if this is an as expression
+  bool IsAs() const override { return true; }
+
+  /// @returns true if the node is valid
+  bool IsValid() const override;
+
+  /// Writes a representation of the node to the output stream
+  /// @param out the stream to write to
+  /// @param indent number of spaces to indent the node when writing
+  void to_str(std::ostream& out, size_t indent) const override;
+
+ private:
+  AsExpression(const AsExpression&) = delete;
+
+  type::Type* type_;
+  std::unique_ptr<Expression> expr_;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_AS_EXPRESSION_H_
diff --git a/src/ast/assignment_statement.cc b/src/ast/assignment_statement.cc
new file mode 100644
index 0000000..8fe711b
--- /dev/null
+++ b/src/ast/assignment_statement.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 "src/ast/assignment_statement.h"
+
+namespace tint {
+namespace ast {
+
+AssignmentStatement::AssignmentStatement(std::unique_ptr<Expression> lhs,
+                                         std::unique_ptr<Expression> rhs)
+    : Statement(), lhs_(std::move(lhs)), rhs_(std::move(rhs)) {}
+
+AssignmentStatement::AssignmentStatement(const Source& source,
+                                         std::unique_ptr<Expression> lhs,
+                                         std::unique_ptr<Expression> rhs)
+    : Statement(source), lhs_(std::move(lhs)), rhs_(std::move(rhs)) {}
+
+AssignmentStatement::~AssignmentStatement() = default;
+
+bool AssignmentStatement::IsValid() const {
+  return lhs_ != nullptr && rhs_ != nullptr;
+}
+
+void AssignmentStatement::to_str(std::ostream& out, size_t indent) const {
+  make_indent(out, indent);
+  out << "Assignment{" << std::endl;
+  lhs_->to_str(out, indent + 2);
+  out << std::endl;
+  rhs_->to_str(out, indent + 2);
+  out << std::endl;
+  make_indent(out, indent);
+  out << "}" << std::endl;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/assignment_statement.h b/src/ast/assignment_statement.h
new file mode 100644
index 0000000..6441874
--- /dev/null
+++ b/src/ast/assignment_statement.h
@@ -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.
+
+#ifndef SRC_AST_ASSIGNMENT_STATEMENT_H_
+#define SRC_AST_ASSIGNMENT_STATEMENT_H_
+
+#include <memory>
+#include <utility>
+
+#include "src/ast/expression.h"
+#include "src/ast/literal.h"
+#include "src/ast/statement.h"
+
+namespace tint {
+namespace ast {
+
+/// An assignment statement
+class AssignmentStatement : public Statement {
+ public:
+  /// Constructor
+  /// @param lhs the left side of the expression
+  /// @param rhs the right side of the expression
+  AssignmentStatement(std::unique_ptr<Expression> lhs,
+                      std::unique_ptr<Expression> rhs);
+  /// Constructor
+  /// @param source the initializer source
+  /// @param lhs the left side of the expression
+  /// @param rhs the right side of the expression
+  AssignmentStatement(const Source& source,
+                      std::unique_ptr<Expression> lhs,
+                      std::unique_ptr<Expression> rhs);
+  /// Move constructor
+  AssignmentStatement(AssignmentStatement&&) = default;
+  ~AssignmentStatement() override;
+
+  /// Sets the left side of the statement
+  /// @param lhs the left side to set
+  void set_lhs(std::unique_ptr<Expression> lhs) { lhs_ = std::move(lhs); }
+  /// @returns the left side expression
+  Expression* lhs() const { return lhs_.get(); }
+
+  /// Sets the right side of the statement
+  /// @param rhs the right side to set
+  void set_rhs(std::unique_ptr<Expression> rhs) { rhs_ = std::move(rhs); }
+  /// @returns the right side expression
+  Expression* rhs() const { return rhs_.get(); }
+
+  /// @returns true if this is an assignment statement
+  bool IsAssign() const override { return true; }
+
+  /// @returns true if the node is valid
+  bool IsValid() const override;
+
+  /// Writes a representation of the node to the output stream
+  /// @param out the stream to write to
+  /// @param indent number of spaces to indent the node when writing
+  void to_str(std::ostream& out, size_t indent) const override;
+
+ private:
+  AssignmentStatement(const AssignmentStatement&) = delete;
+
+  std::unique_ptr<Expression> lhs_;
+  std::unique_ptr<Expression> rhs_;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_ASSIGNMENT_STATEMENT_H_
diff --git a/src/ast/binding_decoration.cc b/src/ast/binding_decoration.cc
new file mode 100644
index 0000000..4278dd5
--- /dev/null
+++ b/src/ast/binding_decoration.cc
@@ -0,0 +1,29 @@
+// 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/ast/binding_decoration.h"
+
+namespace tint {
+namespace ast {
+
+BindingDecoration::BindingDecoration(size_t val) : value_(val) {}
+
+BindingDecoration::~BindingDecoration() = default;
+
+void BindingDecoration::to_str(std::ostream& out) const {
+  out << "binding " << value_;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/binding_decoration.h b/src/ast/binding_decoration.h
new file mode 100644
index 0000000..1a7e573
--- /dev/null
+++ b/src/ast/binding_decoration.h
@@ -0,0 +1,50 @@
+// 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_AST_BINDING_DECORATION_H_
+#define SRC_AST_BINDING_DECORATION_H_
+
+#include <stddef.h>
+
+#include "src/ast/variable_decoration.h"
+
+namespace tint {
+namespace ast {
+
+/// A binding decoration
+class BindingDecoration : public VariableDecoration {
+ public:
+  /// constructor
+  /// @param value the binding value
+  explicit BindingDecoration(size_t value);
+  ~BindingDecoration() override;
+
+  /// @returns true if this is a binding decoration
+  bool IsBinding() const override { return true; }
+
+  /// @returns the binding value
+  size_t value() const { return value_; }
+
+  /// Outputs the decoration to the given stream
+  /// @param out the stream to output too
+  void to_str(std::ostream& out) const override;
+
+ private:
+  size_t value_;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_BINDING_DECORATION_H_
diff --git a/src/ast/binding_decoration_test.cc b/src/ast/binding_decoration_test.cc
new file mode 100644
index 0000000..0a6a57b
--- /dev/null
+++ b/src/ast/binding_decoration_test.cc
@@ -0,0 +1,38 @@
+// 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/ast/binding_decoration.h"
+
+#include "gtest/gtest.h"
+
+namespace tint {
+namespace ast {
+
+using BindingDecorationTest = testing::Test;
+
+TEST_F(BindingDecorationTest, Creation) {
+  BindingDecoration d{2};
+  EXPECT_EQ(2, d.value());
+}
+
+TEST_F(BindingDecorationTest, Is) {
+  BindingDecoration d{2};
+  EXPECT_TRUE(d.IsBinding());
+  EXPECT_FALSE(d.IsBuiltin());
+  EXPECT_FALSE(d.IsLocation());
+  EXPECT_FALSE(d.IsSet());
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/bool_literal.cc b/src/ast/bool_literal.cc
new file mode 100644
index 0000000..e11e8a5
--- /dev/null
+++ b/src/ast/bool_literal.cc
@@ -0,0 +1,29 @@
+// 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/ast/bool_literal.h"
+
+namespace tint {
+namespace ast {
+
+BoolLiteral::BoolLiteral(bool value) : value_(value) {}
+
+BoolLiteral::~BoolLiteral() = default;
+
+std::string BoolLiteral::to_str() const {
+  return value_ ? "true" : "false";
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/bool_literal.h b/src/ast/bool_literal.h
new file mode 100644
index 0000000..c568f27
--- /dev/null
+++ b/src/ast/bool_literal.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_AST_BOOL_LITERAL_H_
+#define SRC_AST_BOOL_LITERAL_H_
+
+#include <string>
+
+#include "src/ast/literal.h"
+
+namespace tint {
+namespace ast {
+
+/// A boolean literal
+class BoolLiteral : public Literal {
+ public:
+  /// Constructor
+  /// @param value the bool literals value
+  explicit BoolLiteral(bool value);
+  ~BoolLiteral() override;
+
+  /// @returns true if this is a bool literal
+  bool IsBool() const override { return true; }
+
+  /// @returns true if the bool literal is true
+  bool IsTrue() const { return value_; }
+  /// @returns true if the bool literal is false
+  bool IsFalse() const { return !value_; }
+
+  /// @returns the literal as a string
+  std::string to_str() const override;
+
+ private:
+  bool value_;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_BOOL_LITERAL_H_
diff --git a/src/ast/bool_literal_test.cc b/src/ast/bool_literal_test.cc
new file mode 100644
index 0000000..af01dae
--- /dev/null
+++ b/src/ast/bool_literal_test.cc
@@ -0,0 +1,55 @@
+// 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/ast/bool_literal.h"
+
+#include "gtest/gtest.h"
+
+namespace tint {
+namespace ast {
+
+using BoolLiteralTest = testing::Test;
+
+TEST_F(BoolLiteralTest, True) {
+  BoolLiteral b{true};
+  ASSERT_TRUE(b.IsBool());
+  ASSERT_TRUE(b.IsTrue());
+  ASSERT_FALSE(b.IsFalse());
+}
+
+TEST_F(BoolLiteralTest, False) {
+  BoolLiteral b{false};
+  ASSERT_TRUE(b.IsBool());
+  ASSERT_FALSE(b.IsTrue());
+  ASSERT_TRUE(b.IsFalse());
+}
+
+TEST_F(BoolLiteralTest, Is) {
+  BoolLiteral b{false};
+  EXPECT_TRUE(b.IsBool());
+  EXPECT_FALSE(b.IsInt());
+  EXPECT_FALSE(b.IsFloat());
+  EXPECT_FALSE(b.IsUint());
+}
+
+TEST_F(BoolLiteralTest, ToStr) {
+  BoolLiteral t{true};
+  BoolLiteral f{false};
+
+  EXPECT_EQ(t.to_str(), "true");
+  EXPECT_EQ(f.to_str(), "false");
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/break_statement.cc b/src/ast/break_statement.cc
new file mode 100644
index 0000000..a095e7d
--- /dev/null
+++ b/src/ast/break_statement.cc
@@ -0,0 +1,58 @@
+// 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/ast/break_statement.h"
+
+namespace tint {
+namespace ast {
+
+BreakStatement::BreakStatement(StatementCondition condition,
+                               std::unique_ptr<Expression> conditional)
+    : Statement(),
+      condition_(condition),
+      conditional_(std::move(conditional)) {}
+
+BreakStatement::BreakStatement(const Source& source,
+                               StatementCondition condition,
+                               std::unique_ptr<Expression> conditional)
+    : Statement(source),
+      condition_(condition),
+      conditional_(std::move(conditional)) {}
+
+BreakStatement::~BreakStatement() = default;
+
+bool BreakStatement::IsValid() const {
+  return condition_ == StatementCondition::kNone || conditional_ != nullptr;
+}
+
+void BreakStatement::to_str(std::ostream& out, size_t indent) const {
+  make_indent(out, indent);
+  out << "Break";
+
+  if (condition_ != StatementCondition::kNone) {
+    out << "{" << std::endl;
+
+    make_indent(out, indent + 2);
+    out << condition_ << std::endl;
+    conditional_->to_str(out, indent + 2);
+
+    make_indent(out, indent);
+    out << "}";
+  }
+
+  out << std::endl;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/break_statement.h b/src/ast/break_statement.h
new file mode 100644
index 0000000..9795bd3
--- /dev/null
+++ b/src/ast/break_statement.h
@@ -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.
+
+#ifndef SRC_AST_BREAK_STATEMENT_H_
+#define SRC_AST_BREAK_STATEMENT_H_
+
+#include <memory>
+#include <utility>
+
+#include "src/ast/expression.h"
+#include "src/ast/statement.h"
+#include "src/ast/statement_condition.h"
+
+namespace tint {
+namespace ast {
+
+/// An break statement
+class BreakStatement : public Statement {
+ public:
+  /// Constructor
+  BreakStatement();
+  /// Constructor
+  /// @param condition the condition type
+  /// @param conditional the condition expression
+  BreakStatement(StatementCondition condition,
+                 std::unique_ptr<Expression> conditional);
+  /// Constructor
+  /// @param source the initializer source
+  /// @param condition the condition type
+  /// @param conditional the condition expression
+  BreakStatement(const Source& source,
+                 StatementCondition condition,
+                 std::unique_ptr<Expression> conditional);
+  /// Move constructor
+  BreakStatement(BreakStatement&&) = default;
+  ~BreakStatement() override;
+
+  /// Sets the condition type
+  /// @param condition the condition type
+  void set_condition(StatementCondition condition) { condition_ = condition; }
+  /// @returns the condition type
+  StatementCondition condition() const { return condition_; }
+
+  /// Sets the conditional expression
+  /// @param conditional the conditional expression
+  void set_conditional(std::unique_ptr<Expression> conditional) {
+    conditional_ = std::move(conditional);
+  }
+  /// @returns the conditional expression
+  Expression* conditional() const { return conditional_.get(); }
+
+  /// @returns true if this is an break statement
+  bool IsBreak() const override { return true; }
+
+  /// @returns true if the node is valid
+  bool IsValid() const override;
+
+  /// Writes a representation of the node to the output stream
+  /// @param out the stream to write to
+  /// @param indent number of spaces to indent the node when writing
+  void to_str(std::ostream& out, size_t indent) const override;
+
+ private:
+  BreakStatement(const BreakStatement&) = delete;
+
+  StatementCondition condition_ = StatementCondition::kNone;
+  std::unique_ptr<Expression> conditional_;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_BREAK_STATEMENT_H_
diff --git a/src/ast/builtin.cc b/src/ast/builtin.cc
new file mode 100644
index 0000000..e579f9c
--- /dev/null
+++ b/src/ast/builtin.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 "src/ast/builtin.h"
+
+namespace tint {
+namespace ast {
+
+std::ostream& operator<<(std::ostream& out, Builtin builtin) {
+  switch (builtin) {
+    case Builtin::kNone: {
+      out << "none";
+      break;
+    }
+    case Builtin::kPosition: {
+      out << "position";
+      break;
+    }
+    case Builtin::kVertexIdx: {
+      out << "vertex_idx";
+      break;
+    }
+    case Builtin::kInstanceIdx: {
+      out << "instance_idx";
+      break;
+    }
+    case Builtin::kFrontFacing: {
+      out << "front_facing";
+      break;
+    }
+    case Builtin::kFragCoord: {
+      out << "frag_coord";
+      break;
+    }
+    case Builtin::kFragDepth: {
+      out << "frag_depth";
+      break;
+    }
+    case Builtin::kNumWorkgroups: {
+      out << "num_workgroups";
+      break;
+    }
+    case Builtin::kWorkgroupSize: {
+      out << "workgroup_size";
+      break;
+    }
+    case Builtin::kLocalInvocationId: {
+      out << "local_invocation_id";
+      break;
+    }
+    case Builtin::kLocalInvocationIdx: {
+      out << "local_invocation_idx";
+      break;
+    }
+    case Builtin::kGlobalInvocationId: {
+      out << "global_invocation_id";
+      break;
+    }
+  }
+  return out;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/builtin.h b/src/ast/builtin.h
new file mode 100644
index 0000000..511203a
--- /dev/null
+++ b/src/ast/builtin.h
@@ -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.
+
+#ifndef SRC_AST_BUILTIN_H_
+#define SRC_AST_BUILTIN_H_
+
+#include <ostream>
+
+namespace tint {
+namespace ast {
+
+/// The builtin identifiers
+enum class Builtin {
+  kNone = -1,
+  kPosition,
+  kVertexIdx,
+  kInstanceIdx,
+  kFrontFacing,
+  kFragCoord,
+  kFragDepth,
+  kNumWorkgroups,
+  kWorkgroupSize,
+  kLocalInvocationId,
+  kLocalInvocationIdx,
+  kGlobalInvocationId
+};
+
+std::ostream& operator<<(std::ostream& out, Builtin builtin);
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_BUILTIN_H_
diff --git a/src/ast/builtin_decoration.cc b/src/ast/builtin_decoration.cc
new file mode 100644
index 0000000..c0264af
--- /dev/null
+++ b/src/ast/builtin_decoration.cc
@@ -0,0 +1,29 @@
+// 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/ast/builtin_decoration.h"
+
+namespace tint {
+namespace ast {
+
+BuiltinDecoration::BuiltinDecoration(Builtin builtin) : builtin_(builtin) {}
+
+BuiltinDecoration::~BuiltinDecoration() = default;
+
+void BuiltinDecoration::to_str(std::ostream& out) const {
+  out << "builtin " << builtin_;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/builtin_decoration.h b/src/ast/builtin_decoration.h
new file mode 100644
index 0000000..5027e12
--- /dev/null
+++ b/src/ast/builtin_decoration.h
@@ -0,0 +1,49 @@
+// 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_AST_BUILTIN_DECORATION_H_
+#define SRC_AST_BUILTIN_DECORATION_H_
+
+#include "src/ast/builtin.h"
+#include "src/ast/variable_decoration.h"
+
+namespace tint {
+namespace ast {
+
+/// A builtin decoration
+class BuiltinDecoration : public VariableDecoration {
+ public:
+  /// constructor
+  /// @param builtin the builtin value
+  explicit BuiltinDecoration(Builtin builtin);
+  ~BuiltinDecoration() override;
+
+  /// @returns true if this is a builtin decoration
+  bool IsBuiltin() const override { return true; }
+
+  /// @returns the builtin value
+  Builtin value() const { return builtin_; }
+
+  /// Outputs the decoration to the given stream
+  /// @param out the stream to output too
+  void to_str(std::ostream& out) const override;
+
+ private:
+  Builtin builtin_ = Builtin::kNone;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_BUILTIN_DECORATION_H_
diff --git a/src/ast/builtin_decoration_test.cc b/src/ast/builtin_decoration_test.cc
new file mode 100644
index 0000000..288674e
--- /dev/null
+++ b/src/ast/builtin_decoration_test.cc
@@ -0,0 +1,38 @@
+// 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/ast/builtin_decoration.h"
+
+#include "gtest/gtest.h"
+
+namespace tint {
+namespace ast {
+
+using BuiltinDecorationTest = testing::Test;
+
+TEST_F(BuiltinDecorationTest, Creation) {
+  BuiltinDecoration d{Builtin::kFragDepth};
+  EXPECT_EQ(Builtin::kFragDepth, d.value());
+}
+
+TEST_F(BuiltinDecorationTest, Is) {
+  BuiltinDecoration d{Builtin::kFragDepth};
+  EXPECT_FALSE(d.IsBinding());
+  EXPECT_TRUE(d.IsBuiltin());
+  EXPECT_FALSE(d.IsLocation());
+  EXPECT_FALSE(d.IsSet());
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/call_expression.cc b/src/ast/call_expression.cc
new file mode 100644
index 0000000..4b0d97d
--- /dev/null
+++ b/src/ast/call_expression.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 "src/ast/call_expression.h"
+
+namespace tint {
+namespace ast {
+
+CallExpression::CallExpression(std::unique_ptr<Expression> func,
+                               std::vector<std::unique_ptr<Expression>> params)
+    : Expression(), func_(std::move(func)), params_(std::move(params)) {}
+
+CallExpression::CallExpression(const Source& source,
+                               std::unique_ptr<Expression> func,
+                               std::vector<std::unique_ptr<Expression>> params)
+    : Expression(source), func_(std::move(func)), params_(std::move(params)) {}
+
+CallExpression::~CallExpression() = default;
+
+bool CallExpression::IsValid() const {
+  return func_ != nullptr;
+}
+
+void CallExpression::to_str(std::ostream& out, size_t indent) const {
+  func_->to_str(out, indent);
+  make_indent(out, indent + 2);
+  out << "(" << std::endl;
+  for (const auto& param : params_)
+    param->to_str(out, indent + 4);
+
+  make_indent(out, indent + 2);
+  out << ")" << std::endl;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/call_expression.h b/src/ast/call_expression.h
new file mode 100644
index 0000000..ac5178f
--- /dev/null
+++ b/src/ast/call_expression.h
@@ -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.
+
+#ifndef SRC_AST_CALL_EXPRESSION_H_
+#define SRC_AST_CALL_EXPRESSION_H_
+
+#include <memory>
+#include <utility>
+#include <vector>
+
+#include "src/ast/expression.h"
+#include "src/ast/literal.h"
+
+namespace tint {
+namespace ast {
+
+/// A call expression
+class CallExpression : public Expression {
+ public:
+  /// Constructor
+  /// @param func the function
+  /// @param params the parameters
+  CallExpression(std::unique_ptr<Expression> func,
+                 std::vector<std::unique_ptr<Expression>> params);
+  /// Constructor
+  /// @param source the initializer source
+  /// @param func the function
+  /// @param params the parameters
+  CallExpression(const Source& source,
+                 std::unique_ptr<Expression> func,
+                 std::vector<std::unique_ptr<Expression>> params);
+  /// Move constructor
+  CallExpression(CallExpression&&) = default;
+  ~CallExpression() override;
+
+  /// Sets the func
+  /// @param func the func
+  void set_func(std::unique_ptr<Expression> func) { func_ = std::move(func); }
+  /// @returns the func
+  Expression* func() const { return func_.get(); }
+
+  /// Sets the parameters
+  /// @param params the parameters
+  void set_params(std::vector<std::unique_ptr<Expression>> params) {
+    params_ = std::move(params);
+  }
+  /// @returns the parameters
+  const std::vector<std::unique_ptr<Expression>>& params() const {
+    return params_;
+  }
+
+  /// @returns true if this is a call expression
+  bool IsCall() const override { return true; }
+
+  /// @returns true if the node is valid
+  bool IsValid() const override;
+
+  /// Writes a representation of the node to the output stream
+  /// @param out the stream to write to
+  /// @param indent number of spaces to indent the node when writing
+  void to_str(std::ostream& out, size_t indent) const override;
+
+ private:
+  CallExpression(const CallExpression&) = delete;
+
+  std::unique_ptr<Expression> func_;
+  std::vector<std::unique_ptr<Expression>> params_;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_CALL_EXPRESSION_H_
diff --git a/src/ast/case_statement.cc b/src/ast/case_statement.cc
new file mode 100644
index 0000000..a591224
--- /dev/null
+++ b/src/ast/case_statement.cc
@@ -0,0 +1,56 @@
+// 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/ast/case_statement.h"
+
+namespace tint {
+namespace ast {
+
+CaseStatement::CaseStatement() : Statement() {}
+
+CaseStatement::CaseStatement(std::unique_ptr<Literal> condition,
+                             std::vector<std::unique_ptr<Statement>> body)
+    : Statement(), condition_(std::move(condition)), body_(std::move(body)) {}
+
+CaseStatement::CaseStatement(const Source& source,
+                             std::unique_ptr<Literal> condition,
+                             std::vector<std::unique_ptr<Statement>> body)
+    : Statement(source),
+      condition_(std::move(condition)),
+      body_(std::move(body)) {}
+
+CaseStatement::~CaseStatement() = default;
+
+bool CaseStatement::IsValid() const {
+  return true;
+}
+
+void CaseStatement::to_str(std::ostream& out, size_t indent) const {
+  make_indent(out, indent);
+
+  if (IsDefault()) {
+    out << "default{" << std::endl;
+  } else {
+    out << "Case " << condition_->to_str() << "{" << std::endl;
+  }
+
+  for (const auto& stmt : body_)
+    stmt->to_str(out, indent + 2);
+
+  make_indent(out, indent);
+  out << "}" << std::endl;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/case_statement.h b/src/ast/case_statement.h
new file mode 100644
index 0000000..8a3ae09
--- /dev/null
+++ b/src/ast/case_statement.h
@@ -0,0 +1,90 @@
+// 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_AST_CASE_STATEMENT_H_
+#define SRC_AST_CASE_STATEMENT_H_
+
+#include <memory>
+#include <utility>
+#include <vector>
+
+#include "src/ast/expression.h"
+#include "src/ast/literal.h"
+#include "src/ast/statement.h"
+#include "src/ast/statement_condition.h"
+
+namespace tint {
+namespace ast {
+
+/// A case statement
+class CaseStatement : public Statement {
+ public:
+  /// Constructor
+  CaseStatement();
+  /// Constructor
+  /// @param condition the case condition
+  /// @param body the case body
+  CaseStatement(std::unique_ptr<Literal> condition,
+                std::vector<std::unique_ptr<Statement>> body);
+  /// Constructor
+  /// @param source the source information
+  /// @param condition the case condition
+  /// @param body the case body
+  CaseStatement(const Source& source,
+                std::unique_ptr<Literal> condition,
+                std::vector<std::unique_ptr<Statement>> body);
+  /// Move constructor
+  CaseStatement(CaseStatement&&) = default;
+  ~CaseStatement() override;
+
+  /// Sets the condition for the case statement
+  /// @param condition the condition to set
+  void set_condition(std::unique_ptr<Literal> condition) {
+    condition_ = std::move(condition);
+  }
+  /// @returns the case condition or nullptr if none set
+  Literal* condition() const { return condition_.get(); }
+  /// @returns true if this is a default statement
+  bool IsDefault() const { return condition_ == nullptr; }
+
+  /// Sets the case body
+  /// @param body the case body
+  void set_body(std::vector<std::unique_ptr<Statement>> body) {
+    body_ = std::move(body);
+  }
+  /// @returns the case body
+  const std::vector<std::unique_ptr<Statement>>& body() const { return body_; }
+
+  /// @returns true if this is a case statement
+  bool IsCase() const override { return true; }
+
+  /// @returns true if the node is valid
+  bool IsValid() const override;
+
+  /// Writes a representation of the node to the output stream
+  /// @param out the stream to write to
+  /// @param indent number of spaces to indent the node when writing
+  void to_str(std::ostream& out, size_t indent) const override;
+
+ private:
+  CaseStatement(const CaseStatement&) = delete;
+
+  std::unique_ptr<Literal> condition_;
+  std::vector<std::unique_ptr<Statement>> body_;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_CASE_STATEMENT_H_
diff --git a/src/ast/cast_expression.cc b/src/ast/cast_expression.cc
new file mode 100644
index 0000000..1d52350
--- /dev/null
+++ b/src/ast/cast_expression.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 "src/ast/cast_expression.h"
+
+namespace tint {
+namespace ast {
+
+CastExpression::CastExpression(type::Type* type,
+                               std::unique_ptr<Expression> expr)
+    : Expression(), type_(type), expr_(std::move(expr)) {}
+
+CastExpression::CastExpression(const Source& source,
+                               type::Type* type,
+                               std::unique_ptr<Expression> expr)
+    : Expression(source), type_(type), expr_(std::move(expr)) {}
+
+CastExpression::~CastExpression() = default;
+
+bool CastExpression::IsValid() const {
+  return type_ != nullptr && expr_ != nullptr;
+}
+
+void CastExpression::to_str(std::ostream& out, size_t indent) const {
+  out << "cast<" << type_->type_name() << ">(";
+  expr_->to_str(out, indent);
+  out << ")";
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/cast_expression.h b/src/ast/cast_expression.h
new file mode 100644
index 0000000..dcb8a3f
--- /dev/null
+++ b/src/ast/cast_expression.h
@@ -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.
+
+#ifndef SRC_AST_CAST_EXPRESSION_H_
+#define SRC_AST_CAST_EXPRESSION_H_
+
+#include <memory>
+#include <utility>
+#include <vector>
+
+#include "src/ast/expression.h"
+#include "src/ast/literal.h"
+#include "src/ast/type/type.h"
+
+namespace tint {
+namespace ast {
+
+/// A cast expression
+class CastExpression : public Expression {
+ public:
+  /// Constructor
+  /// @param type the type
+  /// @param expr the expr
+  CastExpression(type::Type* type, std::unique_ptr<Expression> expr);
+  /// Constructor
+  /// @param source the initializer source
+  /// @param type the type
+  /// @param expr the expr
+  CastExpression(const Source& source,
+                 type::Type* type,
+                 std::unique_ptr<Expression> expr);
+  /// Move constructor
+  CastExpression(CastExpression&&) = default;
+  ~CastExpression() override;
+
+  /// Sets the type
+  /// @param type the type
+  void set_type(type::Type* type) { type_ = std::move(type); }
+  /// @returns the left side expression
+  type::Type* type() const { return type_; }
+
+  /// Sets the expr
+  /// @param expr the expression
+  void set_expr(std::unique_ptr<Expression> expr) { expr_ = std::move(expr); }
+  /// @returns the expression
+  Expression* expr() const { return expr_.get(); }
+
+  /// @returns true if this is a cast expression
+  bool IsCast() const override { return true; }
+
+  /// @returns true if the node is valid
+  bool IsValid() const override;
+
+  /// Writes a representation of the node to the output stream
+  /// @param out the stream to write to
+  /// @param indent number of spaces to indent the node when writing
+  void to_str(std::ostream& out, size_t indent) const override;
+
+ private:
+  CastExpression(const CastExpression&) = delete;
+
+  type::Type* type_;
+  std::unique_ptr<Expression> expr_;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_CAST_EXPRESSION_H_
diff --git a/src/ast/const_initializer_expression.cc b/src/ast/const_initializer_expression.cc
new file mode 100644
index 0000000..11f1880
--- /dev/null
+++ b/src/ast/const_initializer_expression.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 "src/ast/const_initializer_expression.h"
+
+namespace tint {
+namespace ast {
+
+ConstInitializerExpression::ConstInitializerExpression(
+    std::unique_ptr<Literal> literal)
+    : InitializerExpression(), literal_(std::move(literal)) {}
+
+ConstInitializerExpression::ConstInitializerExpression(
+    const Source& source,
+    std::unique_ptr<Literal> litearl)
+    : InitializerExpression(source), literal_(std::move(litearl)) {}
+
+ConstInitializerExpression::~ConstInitializerExpression() = default;
+
+bool ConstInitializerExpression::IsValid() const {
+  return literal_ != nullptr;
+}
+
+void ConstInitializerExpression::to_str(std::ostream& out,
+                                        size_t indent) const {
+  make_indent(out, indent);
+  out << "ConstInitializer{" << literal_->to_str() << "}" << std::endl;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/const_initializer_expression.h b/src/ast/const_initializer_expression.h
new file mode 100644
index 0000000..31e2d44
--- /dev/null
+++ b/src/ast/const_initializer_expression.h
@@ -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.
+
+#ifndef SRC_AST_CONST_INITIALIZER_EXPRESSION_H_
+#define SRC_AST_CONST_INITIALIZER_EXPRESSION_H_
+
+#include <memory>
+#include <utility>
+
+#include "src/ast/initializer_expression.h"
+#include "src/ast/literal.h"
+
+namespace tint {
+namespace ast {
+
+/// A constant initializer
+class ConstInitializerExpression : public InitializerExpression {
+ public:
+  /// Constructor
+  /// @param literal the const literal
+  explicit ConstInitializerExpression(std::unique_ptr<Literal> literal);
+  /// Constructor
+  /// @param source the initializer source
+  /// @param literal the const literal
+  ConstInitializerExpression(const Source& source,
+                             std::unique_ptr<Literal> literal);
+  /// Move constructor
+  ConstInitializerExpression(ConstInitializerExpression&&) = default;
+  ~ConstInitializerExpression() override;
+
+  /// @returns true if this is a constant initializer
+  bool IsConstInitializer() const override { return true; }
+
+  /// Set the literal value
+  /// @param literal the literal
+  void set_literal(std::unique_ptr<Literal> literal) {
+    literal_ = std::move(literal);
+  }
+  /// @returns the literal value
+  Literal* literal() const { return literal_.get(); }
+
+  /// @returns true if the node is valid
+  bool IsValid() const override;
+
+  /// Writes a representation of the node to the output stream
+  /// @param out the stream to write to
+  /// @param indent number of spaces to indent the node when writing
+  void to_str(std::ostream& out, size_t indent) const override;
+
+ private:
+  ConstInitializerExpression(const ConstInitializerExpression&) = delete;
+
+  std::unique_ptr<Literal> literal_;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_CONST_INITIALIZER_EXPRESSION_H_
diff --git a/src/ast/continue_statement.cc b/src/ast/continue_statement.cc
new file mode 100644
index 0000000..e154455
--- /dev/null
+++ b/src/ast/continue_statement.cc
@@ -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.
+
+#include "src/ast/continue_statement.h"
+
+namespace tint {
+namespace ast {
+
+ContinueStatement::ContinueStatement(StatementCondition condition,
+                                     std::unique_ptr<Expression> conditional)
+    : Statement(),
+      condition_(condition),
+      conditional_(std::move(conditional)) {}
+
+ContinueStatement::ContinueStatement(const Source& source,
+                                     StatementCondition condition,
+                                     std::unique_ptr<Expression> conditional)
+    : Statement(source),
+      condition_(condition),
+      conditional_(std::move(conditional)) {}
+
+ContinueStatement::~ContinueStatement() = default;
+
+bool ContinueStatement::IsValid() const {
+  return condition_ == StatementCondition::kNone || conditional_ != nullptr;
+}
+
+void ContinueStatement::to_str(std::ostream& out, size_t indent) const {
+  make_indent(out, indent);
+  out << "Continue";
+
+  if (condition_ != StatementCondition::kNone) {
+    out << "{" << std::endl;
+
+    make_indent(out, indent + 2);
+    out << condition_ << std::endl;
+    conditional_->to_str(out, indent + 2);
+
+    make_indent(out, indent);
+    out << "}";
+  }
+  out << std::endl;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/continue_statement.h b/src/ast/continue_statement.h
new file mode 100644
index 0000000..8cadbb7
--- /dev/null
+++ b/src/ast/continue_statement.h
@@ -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.
+
+#ifndef SRC_AST_CONTINUE_STATEMENT_H_
+#define SRC_AST_CONTINUE_STATEMENT_H_
+
+#include <memory>
+#include <utility>
+
+#include "src/ast/expression.h"
+#include "src/ast/statement.h"
+#include "src/ast/statement_condition.h"
+
+namespace tint {
+namespace ast {
+
+/// An continue statement
+class ContinueStatement : public Statement {
+ public:
+  /// Constructor
+  ContinueStatement();
+  /// Constructor
+  /// @param condition the condition type
+  /// @param conditional the condition expression
+  ContinueStatement(StatementCondition condition,
+                    std::unique_ptr<Expression> conditional);
+  /// Constructor
+  /// @param source the initializer source
+  /// @param condition the condition type
+  /// @param conditional the condition expression
+  ContinueStatement(const Source& source,
+                    StatementCondition condition,
+                    std::unique_ptr<Expression> conditional);
+  /// Move constructor
+  ContinueStatement(ContinueStatement&&) = default;
+  ~ContinueStatement() override;
+
+  /// Sets the condition type
+  /// @param condition the condition type
+  void set_condition(StatementCondition condition) { condition_ = condition; }
+  /// @returns the condition type
+  StatementCondition condition() const { return condition_; }
+
+  /// Sets the conditional expression
+  /// @param conditional the conditional expression
+  void set_conditional(std::unique_ptr<Expression> conditional) {
+    conditional_ = std::move(conditional);
+  }
+  /// @returns the conditional expression
+  Expression* conditional() const { return conditional_.get(); }
+
+  /// @returns true if this is an continue statement
+  bool IsContinue() const override { return true; }
+
+  /// @returns true if the node is valid
+  bool IsValid() const override;
+
+  /// Writes a representation of the node to the output stream
+  /// @param out the stream to write to
+  /// @param indent number of spaces to indent the node when writing
+  void to_str(std::ostream& out, size_t indent) const override;
+
+ private:
+  ContinueStatement(const ContinueStatement&) = delete;
+
+  StatementCondition condition_ = StatementCondition::kNone;
+  std::unique_ptr<Expression> conditional_;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_CONTINUE_STATEMENT_H_
diff --git a/src/ast/decorated_variable.cc b/src/ast/decorated_variable.cc
new file mode 100644
index 0000000..2a6b42e
--- /dev/null
+++ b/src/ast/decorated_variable.cc
@@ -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.
+
+#include "src/ast/decorated_variable.h"
+
+namespace tint {
+namespace ast {
+
+DecoratedVariable::DecoratedVariable() = default;
+
+DecoratedVariable::DecoratedVariable(DecoratedVariable&&) = default;
+
+DecoratedVariable::~DecoratedVariable() = default;
+
+bool DecoratedVariable::IsValid() const {
+  return Variable::IsValid();
+}
+
+void DecoratedVariable::to_str(std::ostream& out, size_t indent) const {
+  make_indent(out, indent);
+  out << "DecoratedVariable{" << std::endl;
+
+  make_indent(out, indent + 2);
+  out << "decorations{" << std::endl;
+  for (const auto& deco : decorations_) {
+    make_indent(out, indent + 4);
+    deco->to_str(out);
+    out << std::endl;
+  }
+
+  make_indent(out, indent + 2);
+  out << "}" << std::endl;
+
+  info_to_str(out, indent + 2);
+  make_indent(out, indent);
+  out << "}" << std::endl;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/decorated_variable.h b/src/ast/decorated_variable.h
new file mode 100644
index 0000000..d81de81
--- /dev/null
+++ b/src/ast/decorated_variable.h
@@ -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.
+
+#ifndef SRC_AST_DECORATED_VARIABLE_H_
+#define SRC_AST_DECORATED_VARIABLE_H_
+
+#include <memory>
+#include <utility>
+#include <vector>
+
+#include "src/ast/variable.h"
+#include "src/ast/variable_decoration.h"
+
+namespace tint {
+namespace ast {
+
+/// A Decorated Variable statement.
+class DecoratedVariable : public Variable {
+ public:
+  /// Create a new empty decorated variable statement
+  DecoratedVariable();
+  /// Move constructor
+  DecoratedVariable(DecoratedVariable&&);
+
+  ~DecoratedVariable() override;
+
+  /// Sets a decoration to the variable
+  /// @param decos the decorations to set
+  void set_decorations(std::vector<std::unique_ptr<VariableDecoration>> decos) {
+    decorations_ = std::move(decos);
+  }
+  /// @returns the decorations attached to this variable
+  const std::vector<std::unique_ptr<VariableDecoration>>& decorations() const {
+    return decorations_;
+  }
+
+  /// @returns true if this is a decorated variable
+  bool IsDecorated() const override { return true; }
+
+  /// @returns true if the name and path are both present
+  bool IsValid() const override;
+
+  /// @param out the stream to write to
+  /// @param indent number of spaces to indent the node when writing
+  void to_str(std::ostream& out, size_t indent) const override;
+
+ private:
+  DecoratedVariable(const DecoratedVariable&) = delete;
+
+  std::vector<std::unique_ptr<VariableDecoration>> decorations_;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_DECORATED_VARIABLE_H_
diff --git a/src/ast/derivative_modifier.cc b/src/ast/derivative_modifier.cc
new file mode 100644
index 0000000..66a7f35
--- /dev/null
+++ b/src/ast/derivative_modifier.cc
@@ -0,0 +1,39 @@
+// 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/ast/derivative_modifier.h"
+
+namespace tint {
+namespace ast {
+
+std::ostream& operator<<(std::ostream& out, DerivativeModifier mod) {
+  switch (mod) {
+    case DerivativeModifier::kNone: {
+      out << "none";
+      break;
+    }
+    case DerivativeModifier::kFine: {
+      out << "fine";
+      break;
+    }
+    case DerivativeModifier::kCoarse: {
+      out << "coarse";
+      break;
+    }
+  }
+  return out;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/derivative_modifier.h b/src/ast/derivative_modifier.h
new file mode 100644
index 0000000..83789e4
--- /dev/null
+++ b/src/ast/derivative_modifier.h
@@ -0,0 +1,31 @@
+// 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_AST_DERIVATIVE_MODIFIER_H_
+#define SRC_AST_DERIVATIVE_MODIFIER_H_
+
+#include <ostream>
+
+namespace tint {
+namespace ast {
+
+/// The derivative modifier
+enum class DerivativeModifier { kNone = -1, kFine, kCoarse };
+
+std::ostream& operator<<(std::ostream& out, DerivativeModifier mod);
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_DERIVATIVE_MODIFIER_H_
diff --git a/src/ast/else_statement.cc b/src/ast/else_statement.cc
new file mode 100644
index 0000000..e41f61b
--- /dev/null
+++ b/src/ast/else_statement.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/ast/else_statement.h"
+
+namespace tint {
+namespace ast {
+
+ElseStatement::ElseStatement() : Statement() {}
+
+ElseStatement::ElseStatement(std::vector<std::unique_ptr<Statement>> body)
+    : Statement(), body_(std::move(body)) {}
+
+ElseStatement::ElseStatement(std::unique_ptr<Expression> condition,
+                             std::vector<std::unique_ptr<Statement>> body)
+    : Statement(), condition_(std::move(condition)), body_(std::move(body)) {}
+
+ElseStatement::ElseStatement(const Source& source,
+                             std::vector<std::unique_ptr<Statement>> body)
+    : Statement(source), body_(std::move(body)) {}
+
+ElseStatement::ElseStatement(const Source& source,
+                             std::unique_ptr<Expression> condition,
+                             std::vector<std::unique_ptr<Statement>> body)
+    : Statement(source),
+      condition_(std::move(condition)),
+      body_(std::move(body)) {}
+
+ElseStatement::~ElseStatement() = default;
+
+bool ElseStatement::IsValid() const {
+  return true;
+}
+
+void ElseStatement::to_str(std::ostream& out, size_t indent) const {
+  make_indent(out, indent);
+  out << "Else{" << std::endl;
+  if (condition_ != nullptr)
+    condition_->to_str(out, indent + 2);
+
+  make_indent(out, indent + 2);
+  out << "{" << std::endl;
+
+  for (const auto& stmt : body_)
+    stmt->to_str(out, indent + 4);
+
+  make_indent(out, indent + 2);
+  out << "}" << std::endl;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/else_statement.h b/src/ast/else_statement.h
new file mode 100644
index 0000000..66511f3
--- /dev/null
+++ b/src/ast/else_statement.h
@@ -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.
+
+#ifndef SRC_AST_ELSE_STATEMENT_H_
+#define SRC_AST_ELSE_STATEMENT_H_
+
+#include <memory>
+#include <utility>
+#include <vector>
+
+#include "src/ast/expression.h"
+#include "src/ast/statement.h"
+
+namespace tint {
+namespace ast {
+
+/// An else statement
+class ElseStatement : public Statement {
+ public:
+  /// Constructor
+  ElseStatement();
+  /// Constructor
+  /// @param body the else body
+  explicit ElseStatement(std::vector<std::unique_ptr<Statement>> body);
+  /// Constructor
+  /// @param condition the else condition
+  /// @param body the else body
+  ElseStatement(std::unique_ptr<Expression> condition,
+                std::vector<std::unique_ptr<Statement>> body);
+  /// Constructor
+  /// @param source the source information
+  /// @param body the else body
+  ElseStatement(const Source& source,
+                std::vector<std::unique_ptr<Statement>> body);
+  /// Constructor
+  /// @param source the source information
+  /// @param condition the else condition
+  /// @param body the else body
+  ElseStatement(const Source& source,
+                std::unique_ptr<Expression> condition,
+                std::vector<std::unique_ptr<Statement>> body);
+  /// Move constructor
+  ElseStatement(ElseStatement&&) = default;
+  ~ElseStatement() override;
+
+  /// Sets the condition for the else statement
+  /// @param condition the condition to set
+  void set_condition(std::unique_ptr<Expression> condition) {
+    condition_ = std::move(condition);
+  }
+  /// @returns the else condition or nullptr if none set
+  Expression* condition() const { return condition_.get(); }
+  /// @returns true if the else has a condition
+  bool HasCondition() const { return condition_ != nullptr; }
+
+  /// Sets the else body
+  /// @param body the else body
+  void set_body(std::vector<std::unique_ptr<Statement>> body) {
+    body_ = std::move(body);
+  }
+  /// @returns the else body
+  const std::vector<std::unique_ptr<Statement>>& body() const { return body_; }
+
+  /// @returns true if this is a else statement
+  bool IsElse() const override { return true; }
+
+  /// @returns true if the node is valid
+  bool IsValid() const override;
+
+  /// Writes a representation of the node to the output stream
+  /// @param out the stream to write to
+  /// @param indent number of spaces to indent the node when writing
+  void to_str(std::ostream& out, size_t indent) const override;
+
+ private:
+  ElseStatement(const ElseStatement&) = delete;
+
+  std::unique_ptr<Expression> condition_;
+  std::vector<std::unique_ptr<Statement>> body_;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_ELSE_STATEMENT_H_
diff --git a/src/ast/entry_point.cc b/src/ast/entry_point.cc
new file mode 100644
index 0000000..a433be1
--- /dev/null
+++ b/src/ast/entry_point.cc
@@ -0,0 +1,50 @@
+// 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/ast/entry_point.h"
+
+namespace tint {
+namespace ast {
+
+EntryPoint::EntryPoint(PipelineStage stage,
+                       const std::string& name,
+                       const std::string& fn_name)
+    : Node(), stage_(stage), name_(name), fn_name_(fn_name) {}
+
+EntryPoint::EntryPoint(const Source& source,
+                       PipelineStage stage,
+                       const std::string& name,
+                       const std::string& fn_name)
+    : Node(source), stage_(stage), name_(name), fn_name_(fn_name) {}
+
+EntryPoint::~EntryPoint() = default;
+
+bool EntryPoint::IsValid() const {
+  if (stage_ == PipelineStage::kNone) {
+    return false;
+  }
+  if (fn_name_.length() == 0) {
+    return false;
+  }
+  return true;
+}
+
+void EntryPoint::to_str(std::ostream& out, size_t indent) const {
+  make_indent(out, indent);
+  out << R"(EntryPoint{")" << stage_ << R"(" as ")" << name_ << R"(" = )"
+      << fn_name_ << "}" << std::endl;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/entry_point.h b/src/ast/entry_point.h
new file mode 100644
index 0000000..40ca20f
--- /dev/null
+++ b/src/ast/entry_point.h
@@ -0,0 +1,89 @@
+// 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_AST_ENTRY_POINT_H_
+#define SRC_AST_ENTRY_POINT_H_
+
+#include <ostream>
+#include <string>
+
+#include "src/ast/node.h"
+#include "src/ast/pipeline_stage.h"
+
+namespace tint {
+namespace ast {
+
+/// An entry point statement.
+class EntryPoint : public Node {
+ public:
+  /// Constructor
+  EntryPoint() = default;
+  /// Constructor
+  /// @param stage the entry point stage
+  /// @param name the entry point name
+  /// @param fn_name the function name
+  EntryPoint(PipelineStage stage,
+             const std::string& name,
+             const std::string& fn_name);
+  /// Constructor
+  /// @param source the source of the entry point
+  /// @param stage the entry point stage
+  /// @param name the entry point name
+  /// @param fn_name the function name
+  EntryPoint(const Source& source,
+             PipelineStage stage,
+             const std::string& name,
+             const std::string& fn_name);
+  /// Move constructor
+  EntryPoint(EntryPoint&&) = default;
+
+  ~EntryPoint() override;
+
+  /// Sets the entry point name
+  /// @param name the name to set
+  void set_name(const std::string& name) { name_ = name; }
+  /// @returns the entry points name
+  const std::string& name() const { return name_; }
+  /// Sets the entry point function name
+  /// @param name the function name
+  void set_function_name(const std::string& name) { fn_name_ = name; }
+  /// @returns the function name for the entry point
+  const std::string& function_name() const { return fn_name_; }
+  /// Sets the piepline stage
+  /// @param stage the stage to set
+  void set_pipeline_stage(PipelineStage stage) { stage_ = stage; }
+  /// @returns the pipeline stage for the entry point
+  PipelineStage stage() const { return stage_; }
+
+  /// @returns true if the entry point is valid
+  bool IsValid() const override;
+
+  /// Writes a representation of the entry point to the output stream
+  /// @param out the stream to write too
+  /// @param indent number of spaces to ident the node when writing
+  void to_str(std::ostream& out, size_t indent) const override;
+
+ private:
+  EntryPoint(const EntryPoint&) = delete;
+
+  Source source_;
+  PipelineStage stage_;
+  std::string name_;
+  std::string fn_name_;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_ENTRY_POINT_H_
diff --git a/src/ast/entry_point_test.cc b/src/ast/entry_point_test.cc
new file mode 100644
index 0000000..5765924
--- /dev/null
+++ b/src/ast/entry_point_test.cc
@@ -0,0 +1,91 @@
+// 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/ast/entry_point.h"
+
+#include <sstream>
+
+#include "gtest/gtest.h"
+
+namespace tint {
+namespace ast {
+
+using EntryPointTest = testing::Test;
+
+TEST_F(EntryPointTest, Creation) {
+  EntryPoint e(PipelineStage::kVertex, "main", "vtx_main");
+
+  EXPECT_EQ(e.name(), "main");
+  EXPECT_EQ(e.function_name(), "vtx_main");
+  EXPECT_EQ(e.stage(), PipelineStage::kVertex);
+  EXPECT_EQ(e.line(), 0);
+  EXPECT_EQ(e.column(), 0);
+}
+
+TEST_F(EntryPointTest, CreationWithSource) {
+  Source s{27, 4};
+  EntryPoint e(s, PipelineStage::kVertex, "main", "vtx_main");
+
+  EXPECT_EQ(e.name(), "main");
+  EXPECT_EQ(e.function_name(), "vtx_main");
+  EXPECT_EQ(e.stage(), PipelineStage::kVertex);
+  EXPECT_EQ(e.line(), 27);
+  EXPECT_EQ(e.column(), 4);
+}
+
+TEST_F(EntryPointTest, CreationEmpty) {
+  Source s{27, 4};
+  EntryPoint e;
+  e.set_source(s);
+  e.set_pipeline_stage(PipelineStage::kFragment);
+  e.set_function_name("my_func");
+  e.set_name("a_name");
+
+  EXPECT_EQ(e.function_name(), "my_func");
+  EXPECT_EQ(e.name(), "a_name");
+  EXPECT_EQ(e.stage(), PipelineStage::kFragment);
+  EXPECT_EQ(e.line(), 27);
+  EXPECT_EQ(e.column(), 4);
+}
+
+TEST_F(EntryPointTest, to_str) {
+  EntryPoint e(PipelineStage::kVertex, "text", "vtx_main");
+  std::ostringstream out;
+  e.to_str(out, 0);
+  EXPECT_EQ(out.str(), R"(EntryPoint{"vertex" as "text" = vtx_main}
+)");
+}
+
+TEST_F(EntryPointTest, IsValid) {
+  EntryPoint e(PipelineStage::kVertex, "main", "vtx_main");
+  EXPECT_TRUE(e.IsValid());
+}
+
+TEST_F(EntryPointTest, IsValid_MissingFunctionName) {
+  EntryPoint e(PipelineStage::kVertex, "main", "");
+  EXPECT_FALSE(e.IsValid());
+}
+
+TEST_F(EntryPointTest, IsValid_MissingStage) {
+  EntryPoint e(PipelineStage::kNone, "main", "fn");
+  EXPECT_FALSE(e.IsValid());
+}
+
+TEST_F(EntryPointTest, IsValid_MissingBoth) {
+  EntryPoint e;
+  EXPECT_FALSE(e.IsValid());
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/expression.cc b/src/ast/expression.cc
new file mode 100644
index 0000000..78041d6
--- /dev/null
+++ b/src/ast/expression.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 "src/ast/expression.h"
+
+#include <assert.h>
+
+#include "src/ast/array_accessor_expression.h"
+#include "src/ast/as_expression.h"
+#include "src/ast/call_expression.h"
+#include "src/ast/cast_expression.h"
+#include "src/ast/identifier_expression.h"
+#include "src/ast/initializer_expression.h"
+#include "src/ast/member_accessor_expression.h"
+#include "src/ast/relational_expression.h"
+#include "src/ast/unary_derivative_expression.h"
+#include "src/ast/unary_method_expression.h"
+#include "src/ast/unary_op_expression.h"
+
+namespace tint {
+namespace ast {
+
+Expression::Expression() = default;
+
+Expression::Expression(const Source& source) : Node(source) {}
+
+Expression::~Expression() = default;
+
+ArrayAccessorExpression* Expression::AsArrayAccessor() {
+  assert(IsArrayAccessor());
+  return static_cast<ArrayAccessorExpression*>(this);
+}
+
+AsExpression* Expression::AsAs() {
+  assert(IsAs());
+  return static_cast<AsExpression*>(this);
+}
+
+CallExpression* Expression::AsCall() {
+  assert(IsCall());
+  return static_cast<CallExpression*>(this);
+}
+
+CastExpression* Expression::AsCast() {
+  assert(IsCast());
+  return static_cast<CastExpression*>(this);
+}
+
+IdentifierExpression* Expression::AsIdentifier() {
+  assert(IsIdentifier());
+  return static_cast<IdentifierExpression*>(this);
+}
+
+InitializerExpression* Expression::AsInitializer() {
+  assert(IsInitializer());
+  return static_cast<InitializerExpression*>(this);
+}
+
+MemberAccessorExpression* Expression::AsMemberAccessor() {
+  assert(IsMemberAccessor());
+  return static_cast<MemberAccessorExpression*>(this);
+}
+
+RelationalExpression* Expression::AsRelational() {
+  assert(IsRelational());
+  return static_cast<RelationalExpression*>(this);
+}
+
+UnaryDerivativeExpression* Expression::AsUnaryDerivative() {
+  assert(IsUnaryDerivative());
+  return static_cast<UnaryDerivativeExpression*>(this);
+}
+
+UnaryMethodExpression* Expression::AsUnaryMethod() {
+  assert(IsUnaryMethod());
+  return static_cast<UnaryMethodExpression*>(this);
+}
+
+UnaryOpExpression* Expression::AsUnaryOp() {
+  assert(IsUnaryOp());
+  return static_cast<UnaryOpExpression*>(this);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/expression.h b/src/ast/expression.h
new file mode 100644
index 0000000..ebc694b
--- /dev/null
+++ b/src/ast/expression.h
@@ -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.
+
+#ifndef SRC_AST_EXPRESSION_H_
+#define SRC_AST_EXPRESSION_H_
+
+#include "src/ast/node.h"
+
+namespace tint {
+namespace ast {
+
+class ArrayAccessorExpression;
+class AsExpression;
+class CallExpression;
+class CastExpression;
+class IdentifierExpression;
+class InitializerExpression;
+class MemberAccessorExpression;
+class RelationalExpression;
+class UnaryDerivativeExpression;
+class UnaryMethodExpression;
+class UnaryOpExpression;
+
+/// Base expression class
+class Expression : public Node {
+ public:
+  ~Expression() override;
+
+  /// @returns true if this is an array accessor expression
+  virtual bool IsArrayAccessor() const { return false; }
+  /// @returns true if this is an as expression
+  virtual bool IsAs() const { return false; }
+  /// @returns true if this is a call expression
+  virtual bool IsCall() const { return false; }
+  /// @returns true if this is a cast expression
+  virtual bool IsCast() const { return false; }
+  /// @returns true if this is an identifier expression
+  virtual bool IsIdentifier() const { return false; }
+  /// @returns true if this is an initializer expression
+  virtual bool IsInitializer() const { return false; }
+  /// @returns true if this is a member accessor expression
+  virtual bool IsMemberAccessor() const { return false; }
+  /// @returns true if this is a relational expression
+  virtual bool IsRelational() const { return false; }
+  /// @returns true if this is a unary derivative expression
+  virtual bool IsUnaryDerivative() const { return false; }
+  /// @returns true if this is a unary method expression
+  virtual bool IsUnaryMethod() const { return false; }
+  /// @returns true if this is a unary op expression
+  virtual bool IsUnaryOp() const { return false; }
+
+  /// @returns the expression as an array accessor
+  ArrayAccessorExpression* AsArrayAccessor();
+  /// @returns the expression as an as
+  AsExpression* AsAs();
+  /// @returns the expression as a call
+  CallExpression* AsCall();
+  /// @returns the expression as a cast
+  CastExpression* AsCast();
+  /// @returns the expression as an identifier
+  IdentifierExpression* AsIdentifier();
+  /// @returns the expression as an initializer
+  InitializerExpression* AsInitializer();
+  /// @returns the expression as a member accessor
+  MemberAccessorExpression* AsMemberAccessor();
+  /// @returns the expression as a relational expression
+  RelationalExpression* AsRelational();
+  /// @returns the expression as a unary derivative expression
+  UnaryDerivativeExpression* AsUnaryDerivative();
+  /// @returns the expression as a unary method expression
+  UnaryMethodExpression* AsUnaryMethod();
+  /// @returns the expression as a unary op expression
+  UnaryOpExpression* AsUnaryOp();
+
+ protected:
+  /// Constructor
+  Expression();
+  /// Constructor
+  /// @param source the source of the expression
+  explicit Expression(const Source& source);
+  /// Move constructor
+  Expression(Expression&&) = default;
+
+ private:
+  Expression(const Expression&) = delete;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_EXPRESSION_H_
diff --git a/src/ast/fallthrough_statement.cc b/src/ast/fallthrough_statement.cc
new file mode 100644
index 0000000..f5ddcea
--- /dev/null
+++ b/src/ast/fallthrough_statement.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/ast/fallthrough_statement.h"
+
+namespace tint {
+namespace ast {
+
+FallthroughStatement::FallthroughStatement() : Statement() {}
+
+FallthroughStatement::FallthroughStatement(const Source& source)
+    : Statement(source) {}
+
+FallthroughStatement::~FallthroughStatement() = default;
+
+bool FallthroughStatement::IsValid() const {
+  return true;
+}
+
+void FallthroughStatement::to_str(std::ostream& out, size_t indent) const {
+  make_indent(out, indent);
+  out << "Fallthrough" << std::endl;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/fallthrough_statement.h b/src/ast/fallthrough_statement.h
new file mode 100644
index 0000000..34cd092
--- /dev/null
+++ b/src/ast/fallthrough_statement.h
@@ -0,0 +1,58 @@
+// 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_AST_FALLTHROUGH_STATEMENT_H_
+#define SRC_AST_FALLTHROUGH_STATEMENT_H_
+
+#include <memory>
+#include <utility>
+
+#include "src/ast/expression.h"
+#include "src/ast/statement.h"
+#include "src/ast/statement_condition.h"
+
+namespace tint {
+namespace ast {
+
+/// An fallthrough statement
+class FallthroughStatement : public Statement {
+ public:
+  /// Constructor
+  FallthroughStatement();
+  /// Constructor
+  /// @param source the source information
+  explicit FallthroughStatement(const Source& source);
+  /// Move constructor
+  FallthroughStatement(FallthroughStatement&&) = default;
+  ~FallthroughStatement() override;
+
+  /// @returns true if this is an fallthrough statement
+  bool IsFallthrough() const override { return true; }
+
+  /// @returns true if the node is valid
+  bool IsValid() const override;
+
+  /// Writes a representation of the node to the output stream
+  /// @param out the stream to write to
+  /// @param indent number of spaces to indent the node when writing
+  void to_str(std::ostream& out, size_t indent) const override;
+
+ private:
+  FallthroughStatement(const FallthroughStatement&) = delete;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_FALLTHROUGH_STATEMENT_H_
diff --git a/src/ast/float_literal.cc b/src/ast/float_literal.cc
new file mode 100644
index 0000000..1d8b40f
--- /dev/null
+++ b/src/ast/float_literal.cc
@@ -0,0 +1,29 @@
+// 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/ast/float_literal.h"
+
+namespace tint {
+namespace ast {
+
+FloatLiteral::FloatLiteral(float value) : value_(value) {}
+
+FloatLiteral::~FloatLiteral() = default;
+
+std::string FloatLiteral::to_str() const {
+  return std::to_string(value_);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/float_literal.h b/src/ast/float_literal.h
new file mode 100644
index 0000000..baf088a
--- /dev/null
+++ b/src/ast/float_literal.h
@@ -0,0 +1,49 @@
+// 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_AST_FLOAT_LITERAL_H_
+#define SRC_AST_FLOAT_LITERAL_H_
+
+#include <string>
+
+#include "src/ast/literal.h"
+
+namespace tint {
+namespace ast {
+
+/// A float literal
+class FloatLiteral : public Literal {
+ public:
+  /// Constructor
+  /// @param value the float literals value
+  explicit FloatLiteral(float value);
+  ~FloatLiteral() override;
+
+  /// @returns true if this is a float literal
+  bool IsFloat() const override { return true; }
+
+  /// @returns the float literal value
+  float value() const { return value_; }
+
+  /// @returns the literal as a string
+  std::string to_str() const override;
+
+ private:
+  float value_;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_FLOAT_LITERAL_H_
diff --git a/src/ast/float_literal_test.cc b/src/ast/float_literal_test.cc
new file mode 100644
index 0000000..8274761
--- /dev/null
+++ b/src/ast/float_literal_test.cc
@@ -0,0 +1,45 @@
+// 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/ast/float_literal.h"
+
+#include "gtest/gtest.h"
+
+namespace tint {
+namespace ast {
+
+using FloatLiteralTest = testing::Test;
+
+TEST_F(FloatLiteralTest, Value) {
+  FloatLiteral f{47.2};
+  ASSERT_TRUE(f.IsFloat());
+  EXPECT_EQ(f.value(), 47.2);
+}
+
+TEST_F(FloatLiteralTest, Is) {
+  FloatLiteral f{42};
+  EXPECT_FALSE(f.IsBool());
+  EXPECT_FALSE(f.IsInt());
+  EXPECT_TRUE(f.IsFloat());
+  EXPECT_FALSE(f.IsUint());
+}
+
+TEST_F(FloatLiteralTest, ToStr) {
+  FloatLiteral f{42.1};
+
+  EXPECT_EQ(f.to_str(), "42.1");
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/function.cc b/src/ast/function.cc
new file mode 100644
index 0000000..f53ba0b
--- /dev/null
+++ b/src/ast/function.cc
@@ -0,0 +1,71 @@
+// 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/ast/function.h"
+
+namespace tint {
+namespace ast {
+
+Function::Function(const std::string& name,
+                   std::vector<std::unique_ptr<Variable>> params,
+                   type::Type* return_type)
+    : Node(),
+      name_(name),
+      params_(std::move(params)),
+      return_type_(return_type) {}
+
+Function::Function(const Source& source,
+                   const std::string& name,
+                   std::vector<std::unique_ptr<Variable>> params,
+                   type::Type* return_type)
+    : Node(source),
+      name_(name),
+      params_(std::move(params)),
+      return_type_(return_type) {}
+
+Function::~Function() = default;
+
+bool Function::IsValid() const {
+  if (name_.length() == 0) {
+    return false;
+  }
+  if (return_type_ == nullptr) {
+    return false;
+  }
+  return true;
+}
+
+void Function::to_str(std::ostream& out, size_t indent) const {
+  make_indent(out, indent);
+  out << "function -> " << return_type_->type_name() << "{" << std::endl;
+  make_indent(out, indent + 2);
+  out << name_ << std::endl;
+
+  for (const auto& param : params_)
+    param->to_str(out, indent + 2);
+
+  make_indent(out, indent + 2);
+  out << "{" << std::endl;
+
+  for (const auto& stmt : body_)
+    stmt->to_str(out, indent + 4);
+
+  make_indent(out, indent + 2);
+  out << "}" << std::endl;
+  make_indent(out, indent);
+  out << "}" << std::endl;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/function.h b/src/ast/function.h
new file mode 100644
index 0000000..6a1158b
--- /dev/null
+++ b/src/ast/function.h
@@ -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.
+
+#ifndef SRC_AST_FUNCTION_H_
+#define SRC_AST_FUNCTION_H_
+
+#include <memory>
+#include <ostream>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "src/ast/expression.h"
+#include "src/ast/node.h"
+#include "src/ast/statement.h"
+#include "src/ast/type/type.h"
+#include "src/ast/variable.h"
+
+namespace tint {
+namespace ast {
+
+/// A Function statement.
+class Function : public Node {
+ public:
+  /// Create a new empty function statement
+  Function() = default;
+  /// Create a function
+  /// @param name the function name
+  /// @param params the function parameters
+  /// @param return_type the return type
+  Function(const std::string& name,
+           std::vector<std::unique_ptr<Variable>> params,
+           type::Type* return_type);
+  /// Create a function
+  /// @param source the variable source
+  /// @param name the function name
+  /// @param params the function parameters
+  /// @param return_type the return type
+  Function(const Source& source,
+           const std::string& name,
+           std::vector<std::unique_ptr<Variable>> params,
+           type::Type* return_type);
+  /// Move constructor
+  Function(Function&&) = default;
+
+  ~Function() override;
+
+  /// Sets the function name
+  /// @param name the name to set
+  void set_name(const std::string& name) { name_ = name; }
+  /// @returns the function name
+  const std::string& name() { return name_; }
+
+  /// Sets the function parameters
+  /// @param params the function parameters
+  void set_params(std::vector<std::unique_ptr<Variable>> params) {
+    params_ = std::move(params);
+  }
+  /// @returns the function params
+  const std::vector<std::unique_ptr<Variable>>& params() const {
+    return params_;
+  }
+
+  /// Sets the return type of the function
+  /// @param type the return type
+  void set_return_type(type::Type* type) { return_type_ = type; }
+  /// @returns the function return type.
+  type::Type* return_type() const { return return_type_; }
+
+  /// Sets the body of the function
+  /// @param body the function body
+  void set_body(std::vector<std::unique_ptr<Statement>> body) {
+    body_ = std::move(body);
+  }
+  /// @returns the function body
+  const std::vector<std::unique_ptr<Statement>>& body() const { return body_; }
+
+  /// @returns true if the name and path are both present
+  bool IsValid() const override;
+
+  /// Writes a representation of the node to the output stream
+  /// @param out the stream to write to
+  /// @param indent number of spaces to indent the node when writing
+  void to_str(std::ostream& out, size_t indent) const override;
+
+ private:
+  Function(const Function&) = delete;
+
+  std::string name_;
+  std::vector<std::unique_ptr<Variable>> params_;
+  type::Type* return_type_ = nullptr;
+  std::vector<std::unique_ptr<Statement>> body_;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_FUNCTION_H_
diff --git a/src/ast/identifier_expression.cc b/src/ast/identifier_expression.cc
new file mode 100644
index 0000000..9fcf4c5
--- /dev/null
+++ b/src/ast/identifier_expression.cc
@@ -0,0 +1,55 @@
+// 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/ast/identifier_expression.h"
+
+namespace tint {
+namespace ast {
+
+IdentifierExpression::IdentifierExpression(const std::string& name)
+    : Expression(), name_({name}) {}
+
+IdentifierExpression::IdentifierExpression(const Source& source,
+                                           const std::string& name)
+    : Expression(source), name_({name}) {}
+
+IdentifierExpression::IdentifierExpression(std::vector<std::string> name)
+    : Expression(), name_(std::move(name)) {}
+
+IdentifierExpression::IdentifierExpression(const Source& source,
+                                           std::vector<std::string> name)
+    : Expression(source), name_(std::move(name)) {}
+
+IdentifierExpression::~IdentifierExpression() = default;
+
+bool IdentifierExpression::IsValid() const {
+  return name_.size() > 0 && name_[1].size() > 0;
+}
+
+void IdentifierExpression::to_str(std::ostream& out, size_t indent) const {
+  make_indent(out, indent);
+  out << "Identifier{";
+  bool first = true;
+  for (const auto& name : name_) {
+    if (!first)
+      out << "::";
+
+    first = false;
+    out << name;
+  }
+  out << "}" << std::endl;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/identifier_expression.h b/src/ast/identifier_expression.h
new file mode 100644
index 0000000..8efe83c
--- /dev/null
+++ b/src/ast/identifier_expression.h
@@ -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.
+
+#ifndef SRC_AST_IDENTIFIER_EXPRESSION_H_
+#define SRC_AST_IDENTIFIER_EXPRESSION_H_
+
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "src/ast/expression.h"
+
+namespace tint {
+namespace ast {
+
+/// An identifier expression
+class IdentifierExpression : public Expression {
+ public:
+  /// Constructor
+  /// @param name the name
+  explicit IdentifierExpression(const std::string& name);
+  /// Constructor
+  /// @param source the source
+  /// @param name the name
+  IdentifierExpression(const Source& source, const std::string& name);
+  /// Constructor
+  /// @param name the name
+  explicit IdentifierExpression(std::vector<std::string> name);
+  /// Constructor
+  /// @param source the initializer source
+  /// @param name the name
+  IdentifierExpression(const Source& source, std::vector<std::string> name);
+  /// Move constructor
+  IdentifierExpression(IdentifierExpression&&) = default;
+  ~IdentifierExpression() override;
+
+  /// Sets the name
+  /// @param name the name
+  void set_name(std::vector<std::string> name) { name_ = std::move(name); }
+  /// @returns the name
+  std::vector<std::string> name() const { return name_; }
+
+  /// @returns true if this is an identifier expression
+  bool IsIdentifier() const override { return true; }
+
+  /// @returns true if the node is valid
+  bool IsValid() const override;
+
+  /// Writes a representation of the node to the output stream
+  /// @param out the stream to write to
+  /// @param indent number of spaces to indent the node when writing
+  void to_str(std::ostream& out, size_t indent) const override;
+
+ private:
+  IdentifierExpression(const IdentifierExpression&) = delete;
+
+  std::vector<std::string> name_;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_IDENTIFIER_EXPRESSION_H_
diff --git a/src/ast/if_statement.cc b/src/ast/if_statement.cc
new file mode 100644
index 0000000..364b828
--- /dev/null
+++ b/src/ast/if_statement.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 "src/ast/if_statement.h"
+
+#include "src/ast/else_statement.h"
+
+namespace tint {
+namespace ast {
+
+IfStatement::IfStatement() : Statement() {}
+
+IfStatement::IfStatement(std::unique_ptr<Expression> condition,
+                         std::vector<std::unique_ptr<Statement>> body)
+    : Statement(), condition_(std::move(condition)), body_(std::move(body)) {}
+
+IfStatement::IfStatement(const Source& source,
+                         std::unique_ptr<Expression> condition,
+                         std::vector<std::unique_ptr<Statement>> body)
+    : Statement(source),
+      condition_(std::move(condition)),
+      body_(std::move(body)) {}
+
+IfStatement::~IfStatement() = default;
+
+bool IfStatement::IsValid() const {
+  if (condition_ == nullptr)
+    return false;
+
+  if (premerge_.size() > 0 && else_statements_.size() > 1)
+    return false;
+
+  return true;
+}
+
+void IfStatement::to_str(std::ostream& out, size_t indent) const {
+  make_indent(out, indent);
+  out << "If{" << std::endl;
+  condition_->to_str(out, indent + 2);
+  out << std::endl;
+  make_indent(out, indent + 2);
+  out << "{" << std::endl;
+
+  for (const auto& stmt : body_)
+    stmt->to_str(out, indent + 4);
+
+  make_indent(out, indent + 2);
+  out << "}" << std::endl;
+
+  for (const auto& e : else_statements_)
+    e->to_str(out, indent + 2);
+
+  if (premerge_.size() > 0) {
+    make_indent(out, indent + 2);
+    out << "premerge{" << std::endl;
+
+    for (const auto& stmt : premerge_)
+      stmt->to_str(out, indent + 4);
+
+    make_indent(out, indent + 2);
+    out << "}" << std::endl;
+  }
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/if_statement.h b/src/ast/if_statement.h
new file mode 100644
index 0000000..f775f6e
--- /dev/null
+++ b/src/ast/if_statement.h
@@ -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.
+
+#ifndef SRC_AST_IF_STATEMENT_H_
+#define SRC_AST_IF_STATEMENT_H_
+
+#include <memory>
+#include <utility>
+#include <vector>
+
+#include "src/ast/expression.h"
+#include "src/ast/statement.h"
+
+namespace tint {
+namespace ast {
+
+/// An if statement
+class IfStatement : public Statement {
+ public:
+  /// Constructor
+  IfStatement();
+  /// Constructor
+  /// @param condition the if condition
+  /// @param body the if body
+  IfStatement(std::unique_ptr<Expression> condition,
+              std::vector<std::unique_ptr<Statement>> body);
+  /// Constructor
+  /// @param source the source information
+  /// @param condition the if condition
+  /// @param body the if body
+  IfStatement(const Source& source,
+              std::unique_ptr<Expression> condition,
+              std::vector<std::unique_ptr<Statement>> body);
+  /// Move constructor
+  IfStatement(IfStatement&&) = default;
+  ~IfStatement() override;
+
+  /// Sets the condition for the if statement
+  /// @param condition the condition to set
+  void set_condition(std::unique_ptr<Expression> condition) {
+    condition_ = std::move(condition);
+  }
+  /// @returns the if condition or nullptr if none set
+  Expression* condition() const { return condition_.get(); }
+
+  /// Sets the if body
+  /// @param body the if body
+  void set_body(std::vector<std::unique_ptr<Statement>> body) {
+    body_ = std::move(body);
+  }
+  /// @returns the if body
+  const std::vector<std::unique_ptr<Statement>>& body() const { return body_; }
+
+  /// Sets the else statements
+  /// @param else_statements the else statements to set
+  void set_else_statements(
+      std::vector<std::unique_ptr<ElseStatement>> else_statements) {
+    else_statements_ = std::move(else_statements);
+  }
+  /// @returns the else statements
+  const std::vector<std::unique_ptr<ElseStatement>>& else_statements() const {
+    return else_statements_;
+  }
+
+  /// Sets the premerge statements
+  /// @param premerge the premerge statements
+  void set_premerge(std::vector<std::unique_ptr<Statement>> premerge) {
+    premerge_ = std::move(premerge);
+  }
+  /// @returns the premerge statements
+  const std::vector<std::unique_ptr<Statement>>& premerge() const {
+    return premerge_;
+  }
+
+  /// @returns true if this is a if statement
+  bool IsIf() const override { return true; }
+
+  /// @returns true if the node is valid
+  bool IsValid() const override;
+
+  /// Writes a representation of the node to the output stream
+  /// @param out the stream to write to
+  /// @param indent number of spaces to indent the node when writing
+  void to_str(std::ostream& out, size_t indent) const override;
+
+ private:
+  IfStatement(const IfStatement&) = delete;
+
+  std::unique_ptr<Expression> condition_;
+  std::vector<std::unique_ptr<Statement>> body_;
+  std::vector<std::unique_ptr<ElseStatement>> else_statements_;
+  std::vector<std::unique_ptr<Statement>> premerge_;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_IF_STATEMENT_H_
diff --git a/src/ast/import.cc b/src/ast/import.cc
new file mode 100644
index 0000000..aa7aff8
--- /dev/null
+++ b/src/ast/import.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 "src/ast/import.h"
+
+namespace tint {
+namespace ast {
+
+Import::Import(const std::string& path, const std::string& name)
+    : Node(), path_(path), name_(name) {}
+
+Import::Import(const Source& source,
+               const std::string& path,
+               const std::string& name)
+    : Node(source), path_(path), name_(name) {}
+
+Import::~Import() = default;
+
+bool Import::IsValid() const {
+  if (path_.length() == 0) {
+    return false;
+  }
+
+  auto len = name_.length();
+  if (len == 0) {
+    return false;
+  }
+
+  // Verify the import name ends in a character, number or _
+  if (len > 2 && !std::isalnum(name_[len - 1]) && name_[len] != '_') {
+    return false;
+  }
+
+  return true;
+}
+
+void Import::to_str(std::ostream& out, size_t indent) const {
+  make_indent(out, indent);
+  out << R"(Import{")" + path_ + R"(" as )" + name_ + "}" << std::endl;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/import.h b/src/ast/import.h
new file mode 100644
index 0000000..cb67c25
--- /dev/null
+++ b/src/ast/import.h
@@ -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.
+
+#ifndef SRC_AST_IMPORT_H_
+#define SRC_AST_IMPORT_H_
+
+#include <ostream>
+#include <string>
+
+#include "src/ast/node.h"
+
+namespace tint {
+namespace ast {
+
+/// An import statement.
+class Import : public Node {
+ public:
+  /// Create a new empty import statement
+  Import() = default;
+  /// Create a new import statement
+  /// @param path The import path e.g. GLSL.std.430
+  /// @param name The import reference name e.g. std::
+  Import(const std::string& path, const std::string& name);
+  /// Create a new import statement
+  /// @param source The input source for the import statement
+  /// @param path The import path e.g. GLSL.std.430
+  /// @param name The import reference name e.g. std::
+  Import(const Source& source,
+         const std::string& path,
+         const std::string& name);
+  /// Move constructor
+  Import(Import&&) = default;
+
+  ~Import() override;
+
+  /// Sets the import path
+  /// @param path the path to set
+  void set_path(const std::string& path) { path_ = path; }
+  /// @returns the import path
+  const std::string& path() const { return path_; }
+  /// Sets the import name
+  /// @param name the name to set
+  void set_name(const std::string& name) { name_ = name; }
+  /// @returns the import name
+  const std::string& name() const { return name_; }
+
+  /// @returns true if the name and path are both present
+  bool IsValid() const override;
+
+  /// Writes a representation of the node to the output stream
+  /// @param out the stream to write to
+  /// @param indent number of spaces to indent the node when writing
+  void to_str(std::ostream& out, size_t indent) const override;
+
+ private:
+  Import(const Import&) = delete;
+
+  std::string path_;
+  std::string name_;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_IMPORT_H_
diff --git a/src/ast/import_test.cc b/src/ast/import_test.cc
new file mode 100644
index 0000000..48f5a77
--- /dev/null
+++ b/src/ast/import_test.cc
@@ -0,0 +1,91 @@
+// 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/ast/import.h"
+
+#include <sstream>
+
+#include "gtest/gtest.h"
+
+namespace tint {
+namespace ast {
+
+using ImportTest = testing::Test;
+
+TEST_F(ImportTest, Creation) {
+  Import i("GLSL.std.430", "std::glsl");
+
+  EXPECT_EQ(i.path(), "GLSL.std.430");
+  EXPECT_EQ(i.name(), "std::glsl");
+  EXPECT_EQ(i.line(), 0);
+  EXPECT_EQ(i.column(), 0);
+}
+
+TEST_F(ImportTest, CreationWithSource) {
+  Source s{27, 4};
+  Import i(s, "GLSL.std.430", "std::glsl");
+
+  EXPECT_EQ(i.path(), "GLSL.std.430");
+  EXPECT_EQ(i.name(), "std::glsl");
+  EXPECT_EQ(i.line(), 27);
+  EXPECT_EQ(i.column(), 4);
+}
+
+TEST_F(ImportTest, CreationEmpty) {
+  Source s{27, 4};
+  Import i;
+  i.set_source(s);
+  i.set_path("GLSL.std.430");
+  i.set_name("std::glsl");
+
+  EXPECT_EQ(i.path(), "GLSL.std.430");
+  EXPECT_EQ(i.name(), "std::glsl");
+  EXPECT_EQ(i.line(), 27);
+  EXPECT_EQ(i.column(), 4);
+}
+
+TEST_F(ImportTest, to_str) {
+  Import i{"GLSL.std.430", "std::glsl"};
+  std::ostringstream out;
+  i.to_str(out, 2);
+  EXPECT_EQ(out.str(), "  Import{\"GLSL.std.430\" as std::glsl}\n");
+}
+
+TEST_F(ImportTest, IsValid) {
+  Import i{"GLSL.std.430", "std::glsl"};
+  EXPECT_TRUE(i.IsValid());
+}
+
+TEST_F(ImportTest, IsValid_MissingPath) {
+  Import i{"", "std::glsl"};
+  EXPECT_FALSE(i.IsValid());
+}
+
+TEST_F(ImportTest, IsValid_MissingName) {
+  Import i{"GLSL.std.430", ""};
+  EXPECT_FALSE(i.IsValid());
+}
+
+TEST_F(ImportTest, IsValid_MissingBoth) {
+  Import i;
+  EXPECT_FALSE(i.IsValid());
+}
+
+TEST_F(ImportTest, IsValid_InvalidEndingCharacter) {
+  Import i{"GLSL.std.430", "std::glsl::"};
+  EXPECT_FALSE(i.IsValid());
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/initializer_expression.cc b/src/ast/initializer_expression.cc
new file mode 100644
index 0000000..f28f3c3
--- /dev/null
+++ b/src/ast/initializer_expression.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/ast/initializer_expression.h"
+
+#include <assert.h>
+
+#include "src/ast/const_initializer_expression.h"
+#include "src/ast/type_initializer_expression.h"
+
+namespace tint {
+namespace ast {
+
+InitializerExpression::InitializerExpression() = default;
+
+InitializerExpression::~InitializerExpression() = default;
+
+InitializerExpression::InitializerExpression(const Source& source)
+    : Expression(source) {}
+
+ConstInitializerExpression* InitializerExpression::AsConstInitializer() {
+  assert(IsConstInitializer());
+  return static_cast<ConstInitializerExpression*>(this);
+}
+
+TypeInitializerExpression* InitializerExpression::AsTypeInitializer() {
+  assert(IsTypeInitializer());
+  return static_cast<TypeInitializerExpression*>(this);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/initializer_expression.h b/src/ast/initializer_expression.h
new file mode 100644
index 0000000..0067c20
--- /dev/null
+++ b/src/ast/initializer_expression.h
@@ -0,0 +1,60 @@
+// 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_AST_INITIALIZER_EXPRESSION_H_
+#define SRC_AST_INITIALIZER_EXPRESSION_H_
+
+#include "src/ast/expression.h"
+
+namespace tint {
+namespace ast {
+
+class ConstInitializerExpression;
+class TypeInitializerExpression;
+
+/// Base class for initializer style expressions
+class InitializerExpression : public Expression {
+ public:
+  ~InitializerExpression() override;
+
+  /// @returns true if this is an initializer expression
+  bool IsInitializer() const override { return true; }
+
+  /// @returns true if this is a constant initializer
+  virtual bool IsConstInitializer() const { return false; }
+  /// @returns true if this is a type initializer
+  virtual bool IsTypeInitializer() const { return false; }
+
+  /// @returns this as a const initializer expression
+  ConstInitializerExpression* AsConstInitializer();
+  /// @returns this as a type initializer expression
+  TypeInitializerExpression* AsTypeInitializer();
+
+ protected:
+  /// Constructor
+  InitializerExpression();
+  /// Constructor
+  /// @param source the initializer source
+  explicit InitializerExpression(const Source& source);
+  /// Move constructor
+  InitializerExpression(InitializerExpression&&) = default;
+
+ private:
+  InitializerExpression(const InitializerExpression&) = delete;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_INITIALIZER_EXPRESSION_H_
diff --git a/src/ast/int_literal.cc b/src/ast/int_literal.cc
new file mode 100644
index 0000000..99e476c
--- /dev/null
+++ b/src/ast/int_literal.cc
@@ -0,0 +1,29 @@
+// 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/ast/int_literal.h"
+
+namespace tint {
+namespace ast {
+
+IntLiteral::IntLiteral(int32_t value) : value_(value) {}
+
+IntLiteral::~IntLiteral() = default;
+
+std::string IntLiteral::to_str() const {
+  return std::to_string(value_);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/int_literal.h b/src/ast/int_literal.h
new file mode 100644
index 0000000..b96e3b9
--- /dev/null
+++ b/src/ast/int_literal.h
@@ -0,0 +1,49 @@
+// 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_AST_INT_LITERAL_H_
+#define SRC_AST_INT_LITERAL_H_
+
+#include <string>
+
+#include "src/ast/literal.h"
+
+namespace tint {
+namespace ast {
+
+/// A int literal
+class IntLiteral : public Literal {
+ public:
+  /// Constructor
+  /// @param value the int literals value
+  explicit IntLiteral(int32_t value);
+  ~IntLiteral() override;
+
+  /// @returns true if this is a int literal
+  bool IsInt() const override { return true; }
+
+  /// @returns the int literal value
+  int32_t value() const { return value_; }
+
+  /// @returns the literal as a string
+  std::string to_str() const override;
+
+ private:
+  int32_t value_;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_INT_LITERAL_H_
diff --git a/src/ast/int_literal_test.cc b/src/ast/int_literal_test.cc
new file mode 100644
index 0000000..7b7fe86
--- /dev/null
+++ b/src/ast/int_literal_test.cc
@@ -0,0 +1,45 @@
+// 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/ast/int_literal.h"
+
+#include "gtest/gtest.h"
+
+namespace tint {
+namespace ast {
+
+using IntLiteralTest = testing::Test;
+
+TEST_F(IntLiteralTest, Value) {
+  IntLiteral i{47};
+  ASSERT_TRUE(i.IsInt());
+  EXPECT_EQ(i.value(), 47);
+}
+
+TEST_F(IntLiteralTest, Is) {
+  IntLiteral i{42};
+  EXPECT_FALSE(i.IsBool());
+  EXPECT_TRUE(i.IsInt());
+  EXPECT_FALSE(i.IsFloat());
+  EXPECT_FALSE(i.IsUint());
+}
+
+TEST_F(IntLiteralTest, ToStr) {
+  IntLiteral i{-42};
+
+  EXPECT_EQ(i.to_str(), "-42");
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/kill_statement.cc b/src/ast/kill_statement.cc
new file mode 100644
index 0000000..70581a7
--- /dev/null
+++ b/src/ast/kill_statement.cc
@@ -0,0 +1,36 @@
+// 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/ast/kill_statement.h"
+
+namespace tint {
+namespace ast {
+
+KillStatement::KillStatement() : Statement() {}
+
+KillStatement::KillStatement(const Source& source) : Statement(source) {}
+
+KillStatement::~KillStatement() = default;
+
+bool KillStatement::IsValid() const {
+  return true;
+}
+
+void KillStatement::to_str(std::ostream& out, size_t indent) const {
+  make_indent(out, indent);
+  out << "Kill{}" << std::endl;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/kill_statement.h b/src/ast/kill_statement.h
new file mode 100644
index 0000000..15bf061
--- /dev/null
+++ b/src/ast/kill_statement.h
@@ -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.
+
+#ifndef SRC_AST_KILL_STATEMENT_H_
+#define SRC_AST_KILL_STATEMENT_H_
+
+#include "src/ast/statement.h"
+
+namespace tint {
+namespace ast {
+
+/// A kill statement
+class KillStatement : public Statement {
+ public:
+  /// Constructor
+  KillStatement();
+  /// Constructor
+  /// @param source the initializer source
+  explicit KillStatement(const Source& source);
+  /// Move constructor
+  KillStatement(KillStatement&&) = default;
+  ~KillStatement() override;
+
+  /// @returns true if this is a kill statement
+  bool IsKill() const override { return true; }
+
+  /// @returns true if the node is valid
+  bool IsValid() const override;
+
+  /// Writes a representation of the node to the output stream
+  /// @param out the stream to write to
+  /// @param indent number of spaces to indent the node when writing
+  void to_str(std::ostream& out, size_t indent) const override;
+
+ private:
+  KillStatement(const KillStatement&) = delete;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_KILL_STATEMENT_H_
diff --git a/src/ast/literal.cc b/src/ast/literal.cc
new file mode 100644
index 0000000..055d33d
--- /dev/null
+++ b/src/ast/literal.cc
@@ -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.
+
+#include "src/ast/literal.h"
+
+#include <assert.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"
+
+namespace tint {
+namespace ast {
+
+Literal::Literal() = default;
+
+Literal::~Literal() = default;
+
+BoolLiteral* Literal::AsBool() {
+  assert(IsBool());
+  return static_cast<BoolLiteral*>(this);
+}
+
+FloatLiteral* Literal::AsFloat() {
+  assert(IsFloat());
+  return static_cast<FloatLiteral*>(this);
+}
+
+IntLiteral* Literal::AsInt() {
+  assert(IsInt());
+  return static_cast<IntLiteral*>(this);
+}
+
+UintLiteral* Literal::AsUint() {
+  assert(IsUint());
+  return static_cast<UintLiteral*>(this);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/literal.h b/src/ast/literal.h
new file mode 100644
index 0000000..af8b831
--- /dev/null
+++ b/src/ast/literal.h
@@ -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.
+
+#ifndef SRC_AST_LITERAL_H_
+#define SRC_AST_LITERAL_H_
+
+#include <string>
+
+namespace tint {
+namespace ast {
+
+class BoolLiteral;
+class FloatLiteral;
+class IntLiteral;
+class UintLiteral;
+
+/// Base class for a literal value
+class Literal {
+ public:
+  virtual ~Literal();
+
+  /// @returns true if this is a bool literal
+  virtual bool IsBool() const { return false; }
+  /// @returns true if this is a float literal
+  virtual bool IsFloat() const { return false; }
+  /// @returns true if this is a signed int literal
+  virtual bool IsInt() const { return false; }
+  /// @returns true if this is a unsigned int literal
+  virtual bool IsUint() const { return false; }
+
+  /// @returns the literal as a boolean literal
+  BoolLiteral* AsBool();
+  /// @returns the literal as a float literal
+  FloatLiteral* AsFloat();
+  /// @returns the literal as a int literal
+  IntLiteral* AsInt();
+  /// @returns the literal as a unsigned int literal
+  UintLiteral* AsUint();
+
+  /// @returns the literal as a string
+  virtual std::string to_str() const = 0;
+
+ protected:
+  /// Constructor
+  Literal();
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_LITERAL_H_
diff --git a/src/ast/location_decoration.cc b/src/ast/location_decoration.cc
new file mode 100644
index 0000000..45bec56
--- /dev/null
+++ b/src/ast/location_decoration.cc
@@ -0,0 +1,29 @@
+// 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/ast/location_decoration.h"
+
+namespace tint {
+namespace ast {
+
+LocationDecoration::LocationDecoration(size_t val) : value_(val) {}
+
+LocationDecoration::~LocationDecoration() = default;
+
+void LocationDecoration::to_str(std::ostream& out) const {
+  out << "location " << value_;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/location_decoration.h b/src/ast/location_decoration.h
new file mode 100644
index 0000000..69b61dc
--- /dev/null
+++ b/src/ast/location_decoration.h
@@ -0,0 +1,50 @@
+// 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_AST_LOCATION_DECORATION_H_
+#define SRC_AST_LOCATION_DECORATION_H_
+
+#include <stddef.h>
+
+#include "src/ast/variable_decoration.h"
+
+namespace tint {
+namespace ast {
+
+/// A location decoration
+class LocationDecoration : public VariableDecoration {
+ public:
+  /// constructor
+  /// @param value the location value
+  explicit LocationDecoration(size_t value);
+  ~LocationDecoration() override;
+
+  /// @returns true if this is a location decoration
+  bool IsLocation() const override { return true; }
+
+  /// @returns the location value
+  size_t value() const { return value_; }
+
+  /// Outputs the decoration to the given stream
+  /// @param out the stream to output too
+  void to_str(std::ostream& out) const override;
+
+ private:
+  size_t value_;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_LOCATION_DECORATION_H_
diff --git a/src/ast/location_decoration_test.cc b/src/ast/location_decoration_test.cc
new file mode 100644
index 0000000..7858193
--- /dev/null
+++ b/src/ast/location_decoration_test.cc
@@ -0,0 +1,38 @@
+// 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/ast/location_decoration.h"
+
+#include "gtest/gtest.h"
+
+namespace tint {
+namespace ast {
+
+using LocationDecorationTest = testing::Test;
+
+TEST_F(LocationDecorationTest, Creation) {
+  LocationDecoration d{2};
+  EXPECT_EQ(2, d.value());
+}
+
+TEST_F(LocationDecorationTest, Is) {
+  LocationDecoration d{2};
+  EXPECT_FALSE(d.IsBinding());
+  EXPECT_FALSE(d.IsBuiltin());
+  EXPECT_TRUE(d.IsLocation());
+  EXPECT_FALSE(d.IsSet());
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/loop_statement.cc b/src/ast/loop_statement.cc
new file mode 100644
index 0000000..b0e645b
--- /dev/null
+++ b/src/ast/loop_statement.cc
@@ -0,0 +1,58 @@
+// 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/ast/loop_statement.h"
+
+namespace tint {
+namespace ast {
+
+LoopStatement::LoopStatement(std::vector<std::unique_ptr<Statement>> body,
+                             std::vector<std::unique_ptr<Statement>> continuing)
+    : Statement(), body_(std::move(body)), continuing_(std::move(continuing)) {}
+
+LoopStatement::LoopStatement(const Source& source,
+                             std::vector<std::unique_ptr<Statement>> body,
+                             std::vector<std::unique_ptr<Statement>> continuing)
+    : Statement(source),
+      body_(std::move(body)),
+      continuing_(std::move(continuing)) {}
+
+LoopStatement::~LoopStatement() = default;
+
+bool LoopStatement::IsValid() const {
+  return true;
+}
+
+void LoopStatement::to_str(std::ostream& out, size_t indent) const {
+  make_indent(out, indent);
+  out << "Loop{" << std::endl;
+
+  for (const auto& stmt : body_)
+    stmt->to_str(out, indent + 2);
+
+  make_indent(out, indent + 2);
+  out << "continuing {" << std::endl;
+
+  for (const auto& stmt : continuing_)
+    stmt->to_str(out, indent + 4);
+
+  make_indent(out, indent + 2);
+  out << "}" << std::endl;
+
+  make_indent(out, indent);
+  out << "}" << std::endl;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/loop_statement.h b/src/ast/loop_statement.h
new file mode 100644
index 0000000..99acd96
--- /dev/null
+++ b/src/ast/loop_statement.h
@@ -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.
+
+#ifndef SRC_AST_LOOP_STATEMENT_H_
+#define SRC_AST_LOOP_STATEMENT_H_
+
+#include <memory>
+#include <utility>
+#include <vector>
+
+#include "src/ast/statement.h"
+
+namespace tint {
+namespace ast {
+
+/// A loop statement
+class LoopStatement : public Statement {
+ public:
+  /// Constructor
+  /// @param body the body statements
+  /// @param continuing the continuing statements
+  LoopStatement(std::vector<std::unique_ptr<Statement>> body,
+                std::vector<std::unique_ptr<Statement>> continuing);
+  /// Constructor
+  /// @param source the initializer source
+  /// @param body the body statements
+  /// @param continuing the continuing statements
+  LoopStatement(const Source& source,
+                std::vector<std::unique_ptr<Statement>> body,
+                std::vector<std::unique_ptr<Statement>> continuing);
+  /// Move constructor
+  LoopStatement(LoopStatement&&) = default;
+  ~LoopStatement() override;
+
+  /// Sets the body statements
+  /// @param body the body statements
+  void set_body(std::vector<std::unique_ptr<Statement>> body) {
+    body_ = std::move(body);
+  }
+  /// @returns the body statements
+  const std::vector<std::unique_ptr<Statement>>& body() const { return body_; }
+
+  /// Sets the continuing statements
+  /// @param continuing the continuing statements
+  void set_continuing(std::vector<std::unique_ptr<Statement>> continuing) {
+    continuing_ = std::move(continuing);
+  }
+  /// @returns the continuing statements
+  const std::vector<std::unique_ptr<Statement>>& continuing() const {
+    return continuing_;
+  }
+
+  /// @returns true if this is a loop statement
+  bool IsLoop() const override { return true; }
+
+  /// @returns true if the node is valid
+  bool IsValid() const override;
+
+  /// Writes a representation of the node to the output stream
+  /// @param out the stream to write to
+  /// @param indent number of spaces to indent the node when writing
+  void to_str(std::ostream& out, size_t indent) const override;
+
+ private:
+  LoopStatement(const LoopStatement&) = delete;
+
+  std::vector<std::unique_ptr<Statement>> body_;
+  std::vector<std::unique_ptr<Statement>> continuing_;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_LOOP_STATEMENT_H_
diff --git a/src/ast/member_accessor_expression.cc b/src/ast/member_accessor_expression.cc
new file mode 100644
index 0000000..24f43ba
--- /dev/null
+++ b/src/ast/member_accessor_expression.cc
@@ -0,0 +1,49 @@
+// 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/ast/member_accessor_expression.h"
+
+namespace tint {
+namespace ast {
+
+MemberAccessorExpression::MemberAccessorExpression(
+    std::unique_ptr<Expression> structure,
+    std::unique_ptr<IdentifierExpression> member)
+    : Expression(), struct_(std::move(structure)), member_(std::move(member)) {}
+
+MemberAccessorExpression::MemberAccessorExpression(
+    const Source& source,
+    std::unique_ptr<Expression> structure,
+    std::unique_ptr<IdentifierExpression> member)
+    : Expression(source),
+      struct_(std::move(structure)),
+      member_(std::move(member)) {}
+
+MemberAccessorExpression::~MemberAccessorExpression() = default;
+
+bool MemberAccessorExpression::IsValid() const {
+  return struct_ != nullptr && member_ != nullptr;
+}
+
+void MemberAccessorExpression::to_str(std::ostream& out, size_t indent) const {
+  make_indent(out, indent);
+  out << "MemberAccessor{" << std::endl;
+  struct_->to_str(out, indent + 2);
+  member_->to_str(out, indent + 2);
+  make_indent(out, indent);
+  out << "}" << std::endl;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/member_accessor_expression.h b/src/ast/member_accessor_expression.h
new file mode 100644
index 0000000..ff24bad
--- /dev/null
+++ b/src/ast/member_accessor_expression.h
@@ -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.
+
+#ifndef SRC_AST_MEMBER_ACCESSOR_EXPRESSION_H_
+#define SRC_AST_MEMBER_ACCESSOR_EXPRESSION_H_
+
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "src/ast/expression.h"
+#include "src/ast/identifier_expression.h"
+#include "src/ast/literal.h"
+
+namespace tint {
+namespace ast {
+
+/// A member accessor expression
+class MemberAccessorExpression : public Expression {
+ public:
+  /// Constructor
+  /// @param structure the structure
+  /// @param member the member
+  MemberAccessorExpression(std::unique_ptr<Expression> structure,
+                           std::unique_ptr<IdentifierExpression> member);
+  /// Constructor
+  /// @param source the initializer source
+  /// @param structure the structure
+  /// @param member the member
+  MemberAccessorExpression(const Source& source,
+                           std::unique_ptr<Expression> structure,
+                           std::unique_ptr<IdentifierExpression> member);
+  /// Move constructor
+  MemberAccessorExpression(MemberAccessorExpression&&) = default;
+  ~MemberAccessorExpression() override;
+
+  /// Sets the structure
+  /// @param structure the structure
+  void set_structure(std::unique_ptr<Expression> structure) {
+    struct_ = std::move(structure);
+  }
+  /// @returns the structure
+  Expression* structure() const { return struct_.get(); }
+
+  /// Sets the member
+  /// @param member the member
+  void set_member(std::unique_ptr<IdentifierExpression> member) {
+    member_ = std::move(member);
+  }
+  /// @returns the member expression
+  IdentifierExpression* member() const { return member_.get(); }
+
+  /// @returns true if this is a member accessor expression
+  bool IsMemberAccessor() const override { return true; }
+
+  /// @returns true if the node is valid
+  bool IsValid() const override;
+
+  /// Writes a representation of the node to the output stream
+  /// @param out the stream to write to
+  /// @param indent number of spaces to indent the node when writing
+  void to_str(std::ostream& out, size_t indent) const override;
+
+ private:
+  MemberAccessorExpression(const MemberAccessorExpression&) = delete;
+
+  std::unique_ptr<Expression> struct_;
+  std::unique_ptr<IdentifierExpression> member_;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_MEMBER_ACCESSOR_EXPRESSION_H_
diff --git a/src/ast/module.cc b/src/ast/module.cc
new file mode 100644
index 0000000..e5c5015
--- /dev/null
+++ b/src/ast/module.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 "src/ast/module.h"
+
+#include <sstream>
+
+namespace tint {
+namespace ast {
+
+const Import* Module::FindImportByName(const std::string& name) {
+  for (const auto& import : imports_) {
+    if (import->name() == name)
+      return import.get();
+  }
+  return nullptr;
+}
+
+bool Module::IsValid() const {
+  for (const auto& import : imports_) {
+    if (!import->IsValid())
+      return false;
+  }
+  return true;
+}
+
+std::string Module::to_str() const {
+  std::ostringstream out;
+
+  for (const auto& import : imports_) {
+    import->to_str(out, 0);
+  }
+  for (const auto& var : global_variables_) {
+    var->to_str(out, 0);
+  }
+  for (const auto& ep : entry_points_) {
+    ep->to_str(out, 0);
+  }
+  for (const auto& alias : alias_types_) {
+    out << alias->name() << " -> " << alias->type()->type_name() << std::endl;
+  }
+  for (const auto& func : functions_) {
+    func->to_str(out, 0);
+  }
+
+  return out.str();
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/module.h b/src/ast/module.h
new file mode 100644
index 0000000..e0d1a7e
--- /dev/null
+++ b/src/ast/module.h
@@ -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.
+
+#ifndef SRC_AST_MODULE_H_
+#define SRC_AST_MODULE_H_
+
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "src/ast/entry_point.h"
+#include "src/ast/function.h"
+#include "src/ast/import.h"
+#include "src/ast/type/alias_type.h"
+#include "src/ast/variable.h"
+
+namespace tint {
+namespace ast {
+
+/// Represents all the source in a given program.
+class Module {
+ public:
+  Module() = default;
+  /// Move constructor
+  Module(Module&&) = default;
+  ~Module() = default;
+
+  /// Add the given import to the module
+  /// @param import The import to add.
+  void AddImport(std::unique_ptr<Import> import) {
+    imports_.push_back(std::move(import));
+  }
+  /// @returns the imports for this module
+  const std::vector<std::unique_ptr<Import>>& imports() { return imports_; }
+  /// Find the import of the given name
+  /// @param name The import name to search for
+  /// @returns the import with the given name if found, nullptr otherwise.
+  const Import* FindImportByName(const std::string& name);
+
+  /// Add a global variable to the module
+  /// @param var the variable to add
+  void AddGlobalVariable(std::unique_ptr<Variable> var) {
+    global_variables_.push_back(std::move(var));
+  }
+  /// @returns the global variables for the module
+  const std::vector<std::unique_ptr<Variable>>& global_variables() const {
+    return global_variables_;
+  }
+
+  /// Adds an entry point to the module
+  /// @param ep the entry point to add
+  void AddEntryPoint(std::unique_ptr<EntryPoint> ep) {
+    entry_points_.push_back(std::move(ep));
+  }
+  /// @returns the entry points in the module
+  const std::vector<std::unique_ptr<EntryPoint>>& entry_points() const {
+    return entry_points_;
+  }
+
+  /// Adds a type alias to the module
+  /// @param type the alias to add
+  void AddAliasType(type::AliasType* type) { alias_types_.push_back(type); }
+  /// @returns the alias types in the module
+  const std::vector<type::AliasType*>& alias_types() const {
+    return alias_types_;
+  }
+
+  /// Adds a function to the module
+  /// @param func the function
+  void AddFunction(std::unique_ptr<Function> func) {
+    functions_.push_back(std::move(func));
+  }
+  /// @returns the modules functions
+  const std::vector<std::unique_ptr<Function>>& functions() const {
+    return functions_;
+  }
+
+  /// @returns true if all required fields in the AST are present.
+  bool IsValid() const;
+
+  /// @returns a string representation of the module
+  std::string to_str() const;
+
+ private:
+  Module(const Module&) = delete;
+
+  std::vector<std::unique_ptr<Import>> imports_;
+  std::vector<std::unique_ptr<Variable>> global_variables_;
+  std::vector<std::unique_ptr<EntryPoint>> entry_points_;
+  // The alias types are owned by the type manager
+  std::vector<type::AliasType*> alias_types_;
+  std::vector<std::unique_ptr<Function>> functions_;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_MODULE_H_
diff --git a/src/ast/module_test.cc b/src/ast/module_test.cc
new file mode 100644
index 0000000..2cf1626
--- /dev/null
+++ b/src/ast/module_test.cc
@@ -0,0 +1,78 @@
+// 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/ast/module.h"
+
+#include <utility>
+
+#include "gtest/gtest.h"
+
+namespace tint {
+namespace ast {
+
+using ModuleTest = testing::Test;
+
+TEST_F(ModuleTest, Creation) {
+  Module m;
+
+  EXPECT_EQ(m.imports().size(), 0);
+}
+
+TEST_F(ModuleTest, Imports) {
+  Module m;
+
+  m.AddImport(std::make_unique<Import>("GLSL.std.430", "std::glsl"));
+  m.AddImport(std::make_unique<Import>("OpenCL.debug.100", "std::debug"));
+
+  EXPECT_EQ(2, m.imports().size());
+  EXPECT_EQ("std::glsl", m.imports()[0]->name());
+}
+
+TEST_F(ModuleTest, LookupImport) {
+  Module m;
+
+  auto i = std::make_unique<Import>("GLSL.std.430", "std::glsl");
+  m.AddImport(std::move(i));
+  m.AddImport(std::make_unique<Import>("OpenCL.debug.100", "std::debug"));
+
+  auto import = m.FindImportByName("std::glsl");
+  ASSERT_NE(nullptr, import);
+  EXPECT_EQ(import->path(), "GLSL.std.430");
+  EXPECT_EQ(import->name(), "std::glsl");
+}
+
+TEST_F(ModuleTest, LookupImportMissing) {
+  Module m;
+  EXPECT_EQ(nullptr, m.FindImportByName("Missing"));
+}
+
+TEST_F(ModuleTest, IsValid_Empty) {
+  Module m;
+  EXPECT_TRUE(m.IsValid());
+}
+
+TEST_F(ModuleTest, IsValid_InvalidImport) {
+  Module m;
+  m.AddImport(std::make_unique<Import>());
+  EXPECT_FALSE(m.IsValid());
+}
+
+TEST_F(ModuleTest, IsValid_ValidImport) {
+  Module m;
+  m.AddImport(std::make_unique<Import>("GLSL.std.430", "std::glsl"));
+  EXPECT_TRUE(m.IsValid());
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/node.cc b/src/ast/node.cc
new file mode 100644
index 0000000..8870db0
--- /dev/null
+++ b/src/ast/node.cc
@@ -0,0 +1,40 @@
+// 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/ast/node.h"
+
+#include <sstream>
+
+namespace tint {
+namespace ast {
+
+Node::Node() = default;
+
+Node::Node(const Source& source) : source_(source) {}
+
+Node::~Node() = default;
+
+void Node::make_indent(std::ostream& out, size_t indent) const {
+  for (size_t i = 0; i < indent; ++i)
+    out << " ";
+}
+
+std::string Node::str() const {
+  std::ostringstream out;
+  to_str(out, 0);
+  return out.str();
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/node.h b/src/ast/node.h
new file mode 100644
index 0000000..97c3bcf
--- /dev/null
+++ b/src/ast/node.h
@@ -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.
+
+#ifndef SRC_AST_NODE_H_
+#define SRC_AST_NODE_H_
+
+#include <ostream>
+#include <string>
+
+#include "src/source.h"
+
+namespace tint {
+namespace ast {
+
+/// AST base class node
+class Node {
+ public:
+  virtual ~Node();
+
+  /// @returns the node source data
+  const Source& source() const { return source_; }
+  /// Sets the source data
+  /// @param source the source data
+  void set_source(const Source& source) { source_ = source; }
+
+  /// @returns the line the node was declared on
+  size_t line() const { return source_.line; }
+  /// @returns the column the node was declared on
+  size_t column() const { return source_.column; }
+
+  /// @returns true if the node is valid
+  virtual bool IsValid() const = 0;
+
+  /// Writes a representation of the node to the output stream
+  /// @param out the stream to write to
+  /// @param indent number of spaces to indent the node when writing
+  virtual void to_str(std::ostream& out, size_t indent) const = 0;
+
+  /// Convenience wrapper around the |to_str| method.
+  /// @returns the node as a string
+  std::string str() const;
+
+ protected:
+  /// Create a new node
+  Node();
+  /// Create a new node
+  /// @param source The input source for the node
+  explicit Node(const Source& source);
+  /// Move constructor
+  Node(Node&&) = default;
+
+  /// Writes indent into stream
+  /// @param out the stream to write to
+  /// @param indent the number of spaces to write
+  void make_indent(std::ostream& out, size_t indent) const;
+
+ private:
+  Node(const Node&) = delete;
+
+  Source source_;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_NODE_H_
diff --git a/src/ast/nop_statement.cc b/src/ast/nop_statement.cc
new file mode 100644
index 0000000..b325bb3
--- /dev/null
+++ b/src/ast/nop_statement.cc
@@ -0,0 +1,36 @@
+// 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/ast/nop_statement.h"
+
+namespace tint {
+namespace ast {
+
+NopStatement::NopStatement() : Statement() {}
+
+NopStatement::NopStatement(const Source& source) : Statement(source) {}
+
+NopStatement::~NopStatement() = default;
+
+bool NopStatement::IsValid() const {
+  return true;
+}
+
+void NopStatement::to_str(std::ostream& out, size_t indent) const {
+  make_indent(out, indent);
+  out << "Nop{}" << std::endl;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/nop_statement.h b/src/ast/nop_statement.h
new file mode 100644
index 0000000..6585728
--- /dev/null
+++ b/src/ast/nop_statement.h
@@ -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.
+
+#ifndef SRC_AST_NOP_STATEMENT_H_
+#define SRC_AST_NOP_STATEMENT_H_
+
+#include "src/ast/statement.h"
+
+namespace tint {
+namespace ast {
+
+/// A nop statement
+class NopStatement : public Statement {
+ public:
+  /// Constructor
+  NopStatement();
+  /// Constructor
+  /// @param source the initializer source
+  explicit NopStatement(const Source& source);
+  /// Move constructor
+  NopStatement(NopStatement&&) = default;
+  ~NopStatement() override;
+
+  /// @returns true if this is a nop statement
+  bool IsNop() const override { return true; }
+
+  /// @returns true if the node is valid
+  bool IsValid() const override;
+
+  /// Writes a representation of the node to the output stream
+  /// @param out the stream to write to
+  /// @param indent number of spaces to indent the node when writing
+  void to_str(std::ostream& out, size_t indent) const override;
+
+ private:
+  NopStatement(const NopStatement&) = delete;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_NOP_STATEMENT_H_
diff --git a/src/ast/pipeline_stage.cc b/src/ast/pipeline_stage.cc
new file mode 100644
index 0000000..ec2682b
--- /dev/null
+++ b/src/ast/pipeline_stage.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/ast/pipeline_stage.h"
+
+namespace tint {
+namespace ast {
+
+std::ostream& operator<<(std::ostream& out, PipelineStage stage) {
+  switch (stage) {
+    case PipelineStage::kNone: {
+      out << "none";
+      break;
+    }
+    case PipelineStage::kVertex: {
+      out << "vertex";
+      break;
+    }
+    case PipelineStage::kFragment: {
+      out << "fragment";
+      break;
+    }
+    case PipelineStage::kCompute: {
+      out << "compute";
+      break;
+    }
+  }
+  return out;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/pipeline_stage.h b/src/ast/pipeline_stage.h
new file mode 100644
index 0000000..3733c8c
--- /dev/null
+++ b/src/ast/pipeline_stage.h
@@ -0,0 +1,31 @@
+// 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_AST_PIPELINE_STAGE_H_
+#define SRC_AST_PIPELINE_STAGE_H_
+
+#include <ostream>
+
+namespace tint {
+namespace ast {
+
+/// The pipeline stage
+enum class PipelineStage { kNone = -1, kVertex, kFragment, kCompute };
+
+std::ostream& operator<<(std::ostream& out, PipelineStage stage);
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_PIPELINE_STAGE_H_
diff --git a/src/ast/regardless_statement.cc b/src/ast/regardless_statement.cc
new file mode 100644
index 0000000..2ad7bb4
--- /dev/null
+++ b/src/ast/regardless_statement.cc
@@ -0,0 +1,58 @@
+// 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/ast/regardless_statement.h"
+
+namespace tint {
+namespace ast {
+
+RegardlessStatement::RegardlessStatement(
+    std::unique_ptr<Expression> condition,
+    std::vector<std::unique_ptr<Statement>> body)
+    : Statement(), condition_(std::move(condition)), body_(std::move(body)) {}
+
+RegardlessStatement::RegardlessStatement(
+    const Source& source,
+    std::unique_ptr<Expression> condition,
+    std::vector<std::unique_ptr<Statement>> body)
+    : Statement(source),
+      condition_(std::move(condition)),
+      body_(std::move(body)) {}
+
+RegardlessStatement::~RegardlessStatement() = default;
+
+bool RegardlessStatement::IsValid() const {
+  return condition_ != nullptr;
+}
+
+void RegardlessStatement::to_str(std::ostream& out, size_t indent) const {
+  make_indent(out, indent);
+  out << "Regardless{" << std::endl;
+
+  condition_->to_str(out, indent + 2);
+  make_indent(out, indent);
+  out << "{" << std::endl;
+
+  for (const auto& stmt : body_)
+    stmt->to_str(out, indent + 4);
+
+  make_indent(out, indent + 2);
+  out << "}";
+
+  make_indent(out, indent);
+  out << "}" << std::endl;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/regardless_statement.h b/src/ast/regardless_statement.h
new file mode 100644
index 0000000..54e9c60
--- /dev/null
+++ b/src/ast/regardless_statement.h
@@ -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.
+
+#ifndef SRC_AST_REGARDLESS_STATEMENT_H_
+#define SRC_AST_REGARDLESS_STATEMENT_H_
+
+#include <memory>
+#include <utility>
+#include <vector>
+
+#include "src/ast/expression.h"
+#include "src/ast/statement.h"
+
+namespace tint {
+namespace ast {
+
+/// A regardless statement
+class RegardlessStatement : public Statement {
+ public:
+  /// Constructor
+  /// @param condition the condition expression
+  /// @param body the body statements
+  RegardlessStatement(std::unique_ptr<Expression> condition,
+                      std::vector<std::unique_ptr<Statement>> body);
+  /// Constructor
+  /// @param source the initializer source
+  /// @param condition the condition expression
+  /// @param body the body statements
+  RegardlessStatement(const Source& source,
+                      std::unique_ptr<Expression> condition,
+                      std::vector<std::unique_ptr<Statement>> body);
+  /// Move constructor
+  RegardlessStatement(RegardlessStatement&&) = default;
+  ~RegardlessStatement() override;
+
+  /// Sets the condition expression
+  /// @param condition the condition expression
+  void set_condition(std::unique_ptr<Expression> condition) {
+    condition_ = std::move(condition);
+  }
+  /// @returns the condition statements
+  Expression* condition() const { return condition_.get(); }
+
+  /// Sets the body statements
+  /// @param body the body statements
+  void set_body(std::vector<std::unique_ptr<Statement>> body) {
+    body_ = std::move(body);
+  }
+  /// @returns the body statements
+  const std::vector<std::unique_ptr<Statement>>& body() const { return body_; }
+
+  /// @returns true if this is an regardless statement
+  bool IsRegardless() const override { return true; }
+
+  /// @returns true if the node is valid
+  bool IsValid() const override;
+
+  /// Writes a representation of the node to the output stream
+  /// @param out the stream to write to
+  /// @param indent number of spaces to indent the node when writing
+  void to_str(std::ostream& out, size_t indent) const override;
+
+ private:
+  RegardlessStatement(const RegardlessStatement&) = delete;
+
+  std::unique_ptr<Expression> condition_;
+  std::vector<std::unique_ptr<Statement>> body_;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_REGARDLESS_STATEMENT_H_
diff --git a/src/ast/relational_expression.cc b/src/ast/relational_expression.cc
new file mode 100644
index 0000000..794df41
--- /dev/null
+++ b/src/ast/relational_expression.cc
@@ -0,0 +1,55 @@
+// 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/ast/relational_expression.h"
+
+namespace tint {
+namespace ast {
+
+RelationalExpression::RelationalExpression(Relation relation,
+                                           std::unique_ptr<Expression> lhs,
+                                           std::unique_ptr<Expression> rhs)
+    : Expression(),
+      relation_(relation),
+      lhs_(std::move(lhs)),
+      rhs_(std::move(rhs)) {}
+
+RelationalExpression::RelationalExpression(const Source& source,
+                                           Relation relation,
+                                           std::unique_ptr<Expression> lhs,
+                                           std::unique_ptr<Expression> rhs)
+    : Expression(source),
+      relation_(relation),
+      lhs_(std::move(lhs)),
+      rhs_(std::move(rhs)) {}
+
+RelationalExpression::~RelationalExpression() = default;
+
+bool RelationalExpression::IsValid() const {
+  return relation_ != Relation::kNone && lhs_ != nullptr && rhs_ != nullptr;
+}
+
+void RelationalExpression::to_str(std::ostream& out, size_t indent) const {
+  make_indent(out, indent);
+  out << relation_ << "(" << std::endl;
+  lhs_->to_str(out, indent + 2);
+  out << std::endl;
+  rhs_->to_str(out, indent + 2);
+  out << std::endl;
+  make_indent(out, indent);
+  out << ")" << std::endl;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/relational_expression.h b/src/ast/relational_expression.h
new file mode 100644
index 0000000..1404353
--- /dev/null
+++ b/src/ast/relational_expression.h
@@ -0,0 +1,220 @@
+// 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_AST_RELATIONAL_EXPRESSION_H_
+#define SRC_AST_RELATIONAL_EXPRESSION_H_
+
+#include <memory>
+#include <utility>
+
+#include "src/ast/expression.h"
+#include "src/ast/literal.h"
+
+namespace tint {
+namespace ast {
+
+/// The relation type
+enum class Relation {
+  kNone = 0,
+  kAnd,
+  kOr,
+  kXor,
+  kLogicalAnd,
+  kLogicalOr,
+  kEqual,
+  kNotEqual,
+  kLessThan,
+  kGreaterThan,
+  kLessThanEqual,
+  kGreaterThanEqual,
+  kUnordGreaterThan,
+  kUnordGreaterThanEqual,
+  kUnordLessThan,
+  kUnordLessThanEqual,
+  kUnordEqual,
+  kUnordNotEqual,
+  kSignedGreaterThan,
+  kSignedGreaterThanEqual,
+  kSignedLessThan,
+  kSignedLessThanEqual,
+  kShiftLeft,
+  kShiftRight,
+  kShiftRightArith,
+  kAdd,
+  kSubtract,
+  kMultiply,
+  kDivide,
+  kModulo,
+};
+
+/// An xor expression
+class RelationalExpression : public Expression {
+ public:
+  /// Constructor
+  /// @param relation the relation type
+  /// @param lhs the left side of the expression
+  /// @param rhs the right side of the expression
+  RelationalExpression(Relation relation,
+                       std::unique_ptr<Expression> lhs,
+                       std::unique_ptr<Expression> rhs);
+  /// Constructor
+  /// @param source the initializer source
+  /// @param relation the relation type
+  /// @param lhs the left side of the expression
+  /// @param rhs the right side of the expression
+  RelationalExpression(const Source& source,
+                       Relation relation,
+                       std::unique_ptr<Expression> lhs,
+                       std::unique_ptr<Expression> rhs);
+  /// Move constructor
+  RelationalExpression(RelationalExpression&&) = default;
+  ~RelationalExpression() override;
+
+  /// Sets the relation type
+  /// @param relation the relation type
+  void set_relation(Relation relation) { relation_ = relation; }
+  /// @returns the relation
+  Relation relation() const { return relation_; }
+
+  /// Sets the left side of the expression
+  /// @param lhs the left side to set
+  void set_lhs(std::unique_ptr<Expression> lhs) { lhs_ = std::move(lhs); }
+  /// @returns the left side expression
+  Expression* lhs() const { return lhs_.get(); }
+
+  /// Sets the right side of the expression
+  /// @param rhs the right side to set
+  void set_rhs(std::unique_ptr<Expression> rhs) { rhs_ = std::move(rhs); }
+  /// @returns the right side expression
+  Expression* rhs() const { return rhs_.get(); }
+
+  /// @returns true if this is a relational expression
+  bool IsRelational() const override { return true; }
+
+  /// @returns true if the node is valid
+  bool IsValid() const override;
+
+  /// Writes a representation of the node to the output stream
+  /// @param out the stream to write to
+  /// @param indent number of spaces to indent the node when writing
+  void to_str(std::ostream& out, size_t indent) const override;
+
+ private:
+  RelationalExpression(const RelationalExpression&) = delete;
+
+  Relation relation_ = Relation::kNone;
+  std::unique_ptr<Expression> lhs_;
+  std::unique_ptr<Expression> rhs_;
+};
+
+inline std::ostream& operator<<(std::ostream& out, Relation relation) {
+  switch (relation) {
+    case Relation::kNone:
+      out << "none";
+      break;
+    case Relation::kAnd:
+      out << "and";
+      break;
+    case Relation::kOr:
+      out << "or";
+      break;
+    case Relation::kXor:
+      out << "xor";
+      break;
+    case Relation::kLogicalAnd:
+      out << "logical_and";
+      break;
+    case Relation::kLogicalOr:
+      out << "logical_or";
+      break;
+    case Relation::kEqual:
+      out << "equal";
+      break;
+    case Relation::kNotEqual:
+      out << "not_equal";
+      break;
+    case Relation::kLessThan:
+      out << "less_than";
+      break;
+    case Relation::kGreaterThan:
+      out << "greater_than";
+      break;
+    case Relation::kLessThanEqual:
+      out << "less_than_equal";
+      break;
+    case Relation::kGreaterThanEqual:
+      out << "greater_than_equal";
+      break;
+    case Relation::kUnordGreaterThan:
+      out << "unord_greater_than";
+      break;
+    case Relation::kUnordGreaterThanEqual:
+      out << "unord_greater_than_equal";
+      break;
+    case Relation::kUnordLessThan:
+      out << "unord_less_than";
+      break;
+    case Relation::kUnordLessThanEqual:
+      out << "unord_less_than_equal";
+      break;
+    case Relation::kUnordEqual:
+      out << "unord_equal";
+      break;
+    case Relation::kUnordNotEqual:
+      out << "unord_not_equal";
+      break;
+    case Relation::kSignedGreaterThan:
+      out << "signed_greateR_than";
+      break;
+    case Relation::kSignedGreaterThanEqual:
+      out << "signed_greater_than_equal";
+      break;
+    case Relation::kSignedLessThan:
+      out << "signed_less_than";
+      break;
+    case Relation::kSignedLessThanEqual:
+      out << "signed_less_than_equal";
+      break;
+    case Relation::kShiftLeft:
+      out << "shift_left";
+      break;
+    case Relation::kShiftRight:
+      out << "shift_right";
+      break;
+    case Relation::kShiftRightArith:
+      out << "shift_right_arith";
+      break;
+    case Relation::kAdd:
+      out << "add";
+      break;
+    case Relation::kSubtract:
+      out << "subtract";
+      break;
+    case Relation::kMultiply:
+      out << "multiply";
+      break;
+    case Relation::kDivide:
+      out << "divide";
+      break;
+    case Relation::kModulo:
+      out << "modulo";
+      break;
+  }
+  return out;
+}
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_RELATIONAL_EXPRESSION_H_
diff --git a/src/ast/return_statement.cc b/src/ast/return_statement.cc
new file mode 100644
index 0000000..5a6dcc2
--- /dev/null
+++ b/src/ast/return_statement.cc
@@ -0,0 +1,48 @@
+// 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/ast/return_statement.h"
+
+namespace tint {
+namespace ast {
+
+ReturnStatement::ReturnStatement() : Statement() {}
+
+ReturnStatement::ReturnStatement(std::unique_ptr<Expression> value)
+    : Statement(), value_(std::move(value)) {}
+
+ReturnStatement::ReturnStatement(const Source& source,
+                                 std::unique_ptr<Expression> value)
+    : Statement(source), value_(std::move(value)) {}
+
+ReturnStatement::~ReturnStatement() = default;
+
+bool ReturnStatement::IsValid() const {
+  return true;
+}
+
+void ReturnStatement::to_str(std::ostream& out, size_t indent) const {
+  make_indent(out, indent);
+  out << "Return{";
+
+  if (value_) {
+    out << std::endl;
+    value_->to_str(out, indent);
+    make_indent(out, indent);
+  }
+  out << "}" << std::endl;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/return_statement.h b/src/ast/return_statement.h
new file mode 100644
index 0000000..30ada9c
--- /dev/null
+++ b/src/ast/return_statement.h
@@ -0,0 +1,71 @@
+// 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_AST_RETURN_STATEMENT_H_
+#define SRC_AST_RETURN_STATEMENT_H_
+
+#include <memory>
+#include <utility>
+
+#include "src/ast/expression.h"
+#include "src/ast/statement.h"
+
+namespace tint {
+namespace ast {
+
+/// A return statement
+class ReturnStatement : public Statement {
+ public:
+  /// Constructor
+  ReturnStatement();
+  /// Constructor
+  /// @param value the return value
+  explicit ReturnStatement(std::unique_ptr<Expression> value);
+  /// Constructor
+  /// @param source the initializer source
+  /// @param value the return value
+  ReturnStatement(const Source& source, std::unique_ptr<Expression> value);
+  /// Move constructor
+  ReturnStatement(ReturnStatement&&) = default;
+  ~ReturnStatement() override;
+
+  /// Sets the value
+  /// @param value the value
+  void set_value(std::unique_ptr<Expression> value) {
+    value_ = std::move(value);
+  }
+  /// @returns the value
+  Expression* value() const { return value_.get(); }
+
+  /// @returns true if this is a return statement
+  bool IsReturn() const override { return true; }
+
+  /// @returns true if the node is valid
+  bool IsValid() const override;
+
+  /// Writes a representation of the node to the output stream
+  /// @param out the stream to write to
+  /// @param indent number of spaces to indent the node when writing
+  void to_str(std::ostream& out, size_t indent) const override;
+
+ private:
+  ReturnStatement(const ReturnStatement&) = delete;
+
+  std::unique_ptr<Expression> value_;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_RETURN_STATEMENT_H_
diff --git a/src/ast/set_decoration.cc b/src/ast/set_decoration.cc
new file mode 100644
index 0000000..c27ee26
--- /dev/null
+++ b/src/ast/set_decoration.cc
@@ -0,0 +1,29 @@
+// 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/ast/set_decoration.h"
+
+namespace tint {
+namespace ast {
+
+SetDecoration::SetDecoration(size_t val) : value_(val) {}
+
+SetDecoration::~SetDecoration() = default;
+
+void SetDecoration::to_str(std::ostream& out) const {
+  out << "set " << value_;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/set_decoration.h b/src/ast/set_decoration.h
new file mode 100644
index 0000000..253e940
--- /dev/null
+++ b/src/ast/set_decoration.h
@@ -0,0 +1,50 @@
+// 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_AST_SET_DECORATION_H_
+#define SRC_AST_SET_DECORATION_H_
+
+#include <stddef.h>
+
+#include "src/ast/variable_decoration.h"
+
+namespace tint {
+namespace ast {
+
+/// A set decoration
+class SetDecoration : public VariableDecoration {
+ public:
+  /// constructor
+  /// @param value the set value
+  explicit SetDecoration(size_t value);
+  ~SetDecoration() override;
+
+  /// @returns true if this is a set decoration
+  bool IsSet() const override { return true; }
+
+  /// @returns the set value
+  size_t value() const { return value_; }
+
+  /// Outputs the decoration to the given stream
+  /// @param out the stream to output too
+  void to_str(std::ostream& out) const override;
+
+ private:
+  size_t value_;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_SET_DECORATION_H_
diff --git a/src/ast/set_decoration_test.cc b/src/ast/set_decoration_test.cc
new file mode 100644
index 0000000..8295e1f
--- /dev/null
+++ b/src/ast/set_decoration_test.cc
@@ -0,0 +1,38 @@
+// 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/ast/set_decoration.h"
+
+#include "gtest/gtest.h"
+
+namespace tint {
+namespace ast {
+
+using SetDecorationTest = testing::Test;
+
+TEST_F(SetDecorationTest, Creation) {
+  SetDecoration d{2};
+  EXPECT_EQ(2, d.value());
+}
+
+TEST_F(SetDecorationTest, Is) {
+  SetDecoration d{2};
+  EXPECT_FALSE(d.IsBinding());
+  EXPECT_FALSE(d.IsBuiltin());
+  EXPECT_FALSE(d.IsLocation());
+  EXPECT_TRUE(d.IsSet());
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/statement.cc b/src/ast/statement.cc
new file mode 100644
index 0000000..07a1f09
--- /dev/null
+++ b/src/ast/statement.cc
@@ -0,0 +1,120 @@
+// 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/ast/statement.h"
+
+#include <assert.h>
+
+#include "src/ast/assignment_statement.h"
+#include "src/ast/break_statement.h"
+#include "src/ast/case_statement.h"
+#include "src/ast/continue_statement.h"
+#include "src/ast/else_statement.h"
+#include "src/ast/fallthrough_statement.h"
+#include "src/ast/if_statement.h"
+#include "src/ast/kill_statement.h"
+#include "src/ast/loop_statement.h"
+#include "src/ast/nop_statement.h"
+#include "src/ast/regardless_statement.h"
+#include "src/ast/return_statement.h"
+#include "src/ast/switch_statement.h"
+#include "src/ast/unless_statement.h"
+#include "src/ast/variable_statement.h"
+
+namespace tint {
+namespace ast {
+
+Statement::Statement() = default;
+
+Statement::Statement(const Source& source) : Node(source) {}
+
+Statement::~Statement() = default;
+
+AssignmentStatement* Statement::AsAssign() {
+  assert(IsAssign());
+  return static_cast<AssignmentStatement*>(this);
+}
+
+BreakStatement* Statement::AsBreak() {
+  assert(IsBreak());
+  return static_cast<BreakStatement*>(this);
+}
+
+CaseStatement* Statement::AsCase() {
+  assert(IsCase());
+  return static_cast<CaseStatement*>(this);
+}
+
+ContinueStatement* Statement::AsContinue() {
+  assert(IsContinue());
+  return static_cast<ContinueStatement*>(this);
+}
+
+ElseStatement* Statement::AsElse() {
+  assert(IsElse());
+  return static_cast<ElseStatement*>(this);
+}
+
+FallthroughStatement* Statement::AsFallthrough() {
+  assert(IsFallthrough());
+  return static_cast<FallthroughStatement*>(this);
+}
+
+IfStatement* Statement::AsIf() {
+  assert(IsIf());
+  return static_cast<IfStatement*>(this);
+}
+
+KillStatement* Statement::AsKill() {
+  assert(IsKill());
+  return static_cast<KillStatement*>(this);
+}
+
+LoopStatement* Statement::AsLoop() {
+  assert(IsLoop());
+  return static_cast<LoopStatement*>(this);
+}
+
+NopStatement* Statement::AsNop() {
+  assert(IsNop());
+  return static_cast<NopStatement*>(this);
+}
+
+RegardlessStatement* Statement::AsRegardless() {
+  assert(IsRegardless());
+  return static_cast<RegardlessStatement*>(this);
+}
+
+ReturnStatement* Statement::AsReturn() {
+  assert(IsReturn());
+  return static_cast<ReturnStatement*>(this);
+}
+
+SwitchStatement* Statement::AsSwitch() {
+  assert(IsSwitch());
+  return static_cast<SwitchStatement*>(this);
+}
+
+UnlessStatement* Statement::AsUnless() {
+  assert(IsUnless());
+  return static_cast<UnlessStatement*>(this);
+}
+
+VariableStatement* Statement::AsVariable() {
+  assert(IsVariable());
+  return static_cast<VariableStatement*>(this);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/statement.h b/src/ast/statement.h
new file mode 100644
index 0000000..112f580
--- /dev/null
+++ b/src/ast/statement.h
@@ -0,0 +1,122 @@
+// 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_AST_STATEMENT_H_
+#define SRC_AST_STATEMENT_H_
+
+#include "src/ast/node.h"
+
+namespace tint {
+namespace ast {
+
+class AssignmentStatement;
+class BreakStatement;
+class CaseStatement;
+class ContinueStatement;
+class ElseStatement;
+class FallthroughStatement;
+class IfStatement;
+class KillStatement;
+class LoopStatement;
+class NopStatement;
+class RegardlessStatement;
+class ReturnStatement;
+class SwitchStatement;
+class UnlessStatement;
+class VariableStatement;
+
+/// Base statement class
+class Statement : public Node {
+ public:
+  ~Statement() override;
+
+  /// @returns true if this is an assign statement
+  virtual bool IsAssign() const { return false; }
+  /// @returns true if this is a break statement
+  virtual bool IsBreak() const { return false; }
+  /// @returns true if this is a case statement
+  virtual bool IsCase() const { return false; }
+  /// @returns true if this is a continue statement
+  virtual bool IsContinue() const { return false; }
+  /// @returns true if this is an else statement
+  virtual bool IsElse() const { return false; }
+  /// @returns true if this is a fallthrough statement
+  virtual bool IsFallthrough() const { return false; }
+  /// @returns true if this is an if statement
+  virtual bool IsIf() const { return false; }
+  /// @returns true if this is a kill statement
+  virtual bool IsKill() const { return false; }
+  /// @returns true if this is a loop statement
+  virtual bool IsLoop() const { return false; }
+  /// @returns true if this is a nop statement
+  virtual bool IsNop() const { return false; }
+  /// @returns true if this is an regardless statement
+  virtual bool IsRegardless() const { return false; }
+  /// @returns true if this is a return statement
+  virtual bool IsReturn() const { return false; }
+  /// @returns true if this is a switch statement
+  virtual bool IsSwitch() const { return false; }
+  /// @returns true if this is an unless statement
+  virtual bool IsUnless() const { return false; }
+  /// @returns true if this is an variable statement
+  virtual bool IsVariable() const { return false; }
+
+  /// @returns the statement as an assign statement
+  AssignmentStatement* AsAssign();
+  /// @returns the statement as a break statement
+  BreakStatement* AsBreak();
+  /// @returns the statement as a case statement
+  CaseStatement* AsCase();
+  /// @returns the statement as a continue statement
+  ContinueStatement* AsContinue();
+  /// @returns the statement as a else statement
+  ElseStatement* AsElse();
+  /// @returns the statement as a fallthrough statement
+  FallthroughStatement* AsFallthrough();
+  /// @returns the statement as a if statement
+  IfStatement* AsIf();
+  /// @returns the statement as a kill statement
+  KillStatement* AsKill();
+  /// @returns the statement as a loop statement
+  LoopStatement* AsLoop();
+  /// @returns the statement as a nop statement
+  NopStatement* AsNop();
+  /// @returns the statement as an regardless statement
+  RegardlessStatement* AsRegardless();
+  /// @returns the statement as a return statement
+  ReturnStatement* AsReturn();
+  /// @returns the statement as a switch statement
+  SwitchStatement* AsSwitch();
+  /// @returns the statement as an unless statement
+  UnlessStatement* AsUnless();
+  /// @returns the statement as an variable statement
+  VariableStatement* AsVariable();
+
+ protected:
+  /// Constructor
+  Statement();
+  /// Constructor
+  /// @param source the source of the expression
+  explicit Statement(const Source& source);
+  /// Move constructor
+  Statement(Statement&&) = default;
+
+ private:
+  Statement(const Statement&) = delete;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_STATEMENT_H_
diff --git a/src/ast/statement_condition.cc b/src/ast/statement_condition.cc
new file mode 100644
index 0000000..dc2136f
--- /dev/null
+++ b/src/ast/statement_condition.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/ast/statement_condition.h"
+
+namespace tint {
+namespace ast {
+
+std::ostream& operator<<(std::ostream& out, StatementCondition condition) {
+  switch (condition) {
+    case StatementCondition::kNone:
+      out << "none";
+      break;
+    case StatementCondition::kIf:
+      out << "if";
+      break;
+    case StatementCondition::kUnless:
+      out << "unless";
+      break;
+  }
+
+  return out;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/statement_condition.h b/src/ast/statement_condition.h
new file mode 100644
index 0000000..fa4149e
--- /dev/null
+++ b/src/ast/statement_condition.h
@@ -0,0 +1,31 @@
+// 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_AST_STATEMENT_CONDITION_H_
+#define SRC_AST_STATEMENT_CONDITION_H_
+
+#include <ostream>
+
+namespace tint {
+namespace ast {
+
+/// Type of  condition attached to a statement
+enum class StatementCondition { kNone = 0, kIf, kUnless };
+
+std::ostream& operator<<(std::ostream& out, StatementCondition condition);
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_STATEMENT_CONDITION_H_
diff --git a/src/ast/storage_class.cc b/src/ast/storage_class.cc
new file mode 100644
index 0000000..e0c71f7
--- /dev/null
+++ b/src/ast/storage_class.cc
@@ -0,0 +1,71 @@
+// 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/ast/storage_class.h"
+
+namespace tint {
+namespace ast {
+
+std::ostream& operator<<(std::ostream& out, StorageClass sc) {
+  switch (sc) {
+    case StorageClass::kNone: {
+      out << "none";
+      break;
+    }
+    case StorageClass::kInput: {
+      out << "input";
+      break;
+    }
+    case StorageClass::kOutput: {
+      out << "output";
+      break;
+    }
+    case StorageClass::kUniform: {
+      out << "uniform";
+      break;
+    }
+    case StorageClass::kWorkgroup: {
+      out << "workgroup";
+      break;
+    }
+    case StorageClass::kUniformConstant: {
+      out << "uniform_constant";
+      break;
+    }
+    case StorageClass::kStorageBuffer: {
+      out << "storage_buffer";
+      break;
+    }
+    case StorageClass::kImage: {
+      out << "image";
+      break;
+    }
+    case StorageClass::kPushConstant: {
+      out << "push_constant";
+      break;
+    }
+    case StorageClass::kPrivate: {
+      out << "private";
+      break;
+    }
+    case StorageClass::kFunction: {
+      out << "function";
+      break;
+    }
+  }
+  return out;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/storage_class.h b/src/ast/storage_class.h
new file mode 100644
index 0000000..3a25fed
--- /dev/null
+++ b/src/ast/storage_class.h
@@ -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.
+
+#ifndef SRC_AST_STORAGE_CLASS_H_
+#define SRC_AST_STORAGE_CLASS_H_
+
+#include <ostream>
+
+namespace tint {
+namespace ast {
+
+/// Storage class of a given pointer.
+enum class StorageClass {
+  kNone = -1,
+  kInput,
+  kOutput,
+  kUniform,
+  kWorkgroup,
+  kUniformConstant,
+  kStorageBuffer,
+  kImage,
+  kPushConstant,
+  kPrivate,
+  kFunction
+};
+
+std::ostream& operator<<(std::ostream& out, StorageClass sc);
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_STORAGE_CLASS_H_
diff --git a/src/ast/struct.cc b/src/ast/struct.cc
new file mode 100644
index 0000000..1cdf866
--- /dev/null
+++ b/src/ast/struct.cc
@@ -0,0 +1,49 @@
+// 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/ast/struct.h"
+
+namespace tint {
+namespace ast {
+
+Struct::Struct(StructDecoration decoration,
+               std::vector<std::unique_ptr<StructMember>> members)
+    : Node(), decoration_(decoration), members_(std::move(members)) {}
+
+Struct::Struct(const Source& source,
+               StructDecoration decoration,
+               std::vector<std::unique_ptr<StructMember>> members)
+    : Node(source), decoration_(decoration), members_(std::move(members)) {}
+
+Struct::~Struct() = default;
+
+bool Struct::IsValid() const {
+  return true;
+}
+
+void Struct::to_str(std::ostream& out, size_t indent) const {
+  make_indent(out, indent);
+  if (decoration_ != StructDecoration::kNone) {
+    out << "[[" << decoration_ << "]] ";
+  }
+  out << "Struct{" << std::endl;
+  for (const auto& member : members_) {
+    member->to_str(out, indent + 2);
+  }
+  make_indent(out, indent);
+  out << "}" << std::endl;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/struct.h b/src/ast/struct.h
new file mode 100644
index 0000000..a3d2e12
--- /dev/null
+++ b/src/ast/struct.h
@@ -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.
+
+#ifndef SRC_AST_STRUCT_H_
+#define SRC_AST_STRUCT_H_
+
+#include <memory>
+#include <ostream>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "src/ast/node.h"
+#include "src/ast/struct_decoration.h"
+#include "src/ast/struct_member.h"
+
+namespace tint {
+namespace ast {
+
+/// A struct statement.
+class Struct : public Node {
+ public:
+  /// Create a new empty struct statement
+  Struct() = default;
+  /// Create a new struct statement
+  /// @param decoration The struct decorations
+  /// @param members The struct members
+  Struct(StructDecoration decoration,
+         std::vector<std::unique_ptr<StructMember>> members);
+  /// Create a new struct statement
+  /// @param source The input source for the import statement
+  /// @param decoration The struct decorations
+  /// @param members The struct members
+  Struct(const Source& source,
+         StructDecoration decoration,
+         std::vector<std::unique_ptr<StructMember>> members);
+  /// Move constructor
+  Struct(Struct&&) = default;
+
+  ~Struct() override;
+
+  /// Sets the struct decoration
+  /// @param deco the decoration to set
+  void set_decoration(StructDecoration deco) { decoration_ = deco; }
+  /// @returns the struct decoration
+  StructDecoration decoration() const { return decoration_; }
+
+  /// Sets the struct members
+  /// @param members the members to set
+  void set_members(std::vector<std::unique_ptr<StructMember>> members) {
+    members_ = std::move(members);
+  }
+  /// @returns the members
+  const std::vector<std::unique_ptr<StructMember>>& members() const {
+    return members_;
+  }
+
+  /// @returns true if the node is valid
+  bool IsValid() const override;
+
+  /// Writes a representation of the node to the output stream
+  /// @param out the stream to write to
+  /// @param indent number of spaces to indent the node when writing
+  void to_str(std::ostream& out, size_t indent) const override;
+
+ private:
+  Struct(const Struct&) = delete;
+
+  StructDecoration decoration_ = StructDecoration::kNone;
+  std::vector<std::unique_ptr<StructMember>> members_;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_STRUCT_H_
diff --git a/src/ast/struct_decoration.cc b/src/ast/struct_decoration.cc
new file mode 100644
index 0000000..10ee485
--- /dev/null
+++ b/src/ast/struct_decoration.cc
@@ -0,0 +1,35 @@
+// 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/ast/struct_decoration.h"
+
+namespace tint {
+namespace ast {
+
+std::ostream& operator<<(std::ostream& out, StructDecoration stage) {
+  switch (stage) {
+    case StructDecoration::kNone: {
+      out << "none";
+      break;
+    }
+    case StructDecoration::kBlock: {
+      out << "block";
+      break;
+    }
+  }
+  return out;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/struct_decoration.h b/src/ast/struct_decoration.h
new file mode 100644
index 0000000..fab582c
--- /dev/null
+++ b/src/ast/struct_decoration.h
@@ -0,0 +1,31 @@
+// 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_AST_STRUCT_DECORATION_H_
+#define SRC_AST_STRUCT_DECORATION_H_
+
+#include <ostream>
+
+namespace tint {
+namespace ast {
+
+/// The struct decorations
+enum class StructDecoration { kNone = -1, kBlock };
+
+std::ostream& operator<<(std::ostream& out, StructDecoration stage);
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_STRUCT_DECORATION_H_
diff --git a/src/ast/struct_member.cc b/src/ast/struct_member.cc
new file mode 100644
index 0000000..4ad5230
--- /dev/null
+++ b/src/ast/struct_member.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 "src/ast/struct_member.h"
+
+namespace tint {
+namespace ast {
+
+StructMember::StructMember(
+    const std::string& name,
+    type::Type* type,
+    std::vector<std::unique_ptr<StructMemberDecoration>> decorations)
+    : Node(), name_(name), type_(type), decorations_(std::move(decorations)) {}
+
+StructMember::StructMember(
+    const Source& source,
+    const std::string& name,
+    type::Type* type,
+    std::vector<std::unique_ptr<StructMemberDecoration>> decorations)
+    : Node(source),
+      name_(name),
+      type_(type),
+      decorations_(std::move(decorations)) {}
+
+StructMember::~StructMember() = default;
+
+bool StructMember::IsValid() const {
+  if (name_.length() == 0) {
+    return false;
+  }
+  if (type_ == nullptr) {
+    return false;
+  }
+  return true;
+}
+
+void StructMember::to_str(std::ostream& out, size_t indent) const {
+  make_indent(out, indent);
+  out << "StructMember{";
+  if (decorations_.size() > 0) {
+    out << "[[ ";
+    for (const auto& deco : decorations_)
+      out << deco->to_str() << " ";
+    out << "]] ";
+  }
+
+  out << name_ << ": " << type_->type_name() << "}" << std::endl;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/struct_member.h b/src/ast/struct_member.h
new file mode 100644
index 0000000..0aceb81
--- /dev/null
+++ b/src/ast/struct_member.h
@@ -0,0 +1,100 @@
+// 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_AST_STRUCT_MEMBER_H_
+#define SRC_AST_STRUCT_MEMBER_H_
+
+#include <memory>
+#include <ostream>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "src/ast/node.h"
+#include "src/ast/struct_member_decoration.h"
+#include "src/ast/type/type.h"
+
+namespace tint {
+namespace ast {
+
+/// A struct member statement.
+class StructMember : public Node {
+ public:
+  /// Create a new empty struct member statement
+  StructMember() = default;
+  /// Create a new struct member statement
+  /// @param name The struct member name
+  /// @param type The struct member type
+  /// @param decorations The struct member decorations
+  StructMember(
+      const std::string& name,
+      type::Type* type,
+      std::vector<std::unique_ptr<StructMemberDecoration>> decorations);
+  /// Create a new struct member statement
+  /// @param source The input source for the struct member statement
+  /// @param name The struct member name
+  /// @param type The struct member type
+  /// @param decorations The struct member decorations
+  StructMember(
+      const Source& source,
+      const std::string& name,
+      type::Type* type,
+      std::vector<std::unique_ptr<StructMemberDecoration>> decorations);
+  /// Move constructor
+  StructMember(StructMember&&) = default;
+
+  ~StructMember() override;
+
+  /// Sets the name
+  /// @param name the name to set
+  void set_name(const std::string& name) { name_ = name; }
+  /// @returns the name
+  const std::string& name() const { return name_; }
+  /// Sets the type
+  /// @param type the type to set
+  void set_type(type::Type* type) { type_ = type; }
+  /// @returns the type
+  type::Type* type() const { return type_; }
+  /// Sets the decorations
+  /// @param decorations the decorations
+  void set_decorations(
+      std::vector<std::unique_ptr<StructMemberDecoration>> decorations) {
+    decorations_ = std::move(decorations);
+  }
+  /// @returns the decorations
+  const std::vector<std::unique_ptr<StructMemberDecoration>>& decorations()
+      const {
+    return decorations_;
+  }
+
+  /// @returns true if the node is valid
+  bool IsValid() const override;
+
+  /// Writes a representation of the node to the output stream
+  /// @param out the stream to write to
+  /// @param indent number of spaces to indent the node when writing
+  void to_str(std::ostream& out, size_t indent) const override;
+
+ private:
+  StructMember(const StructMember&) = delete;
+
+  std::string name_;
+  type::Type* type_ = nullptr;
+  std::vector<std::unique_ptr<StructMemberDecoration>> decorations_;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_STRUCT_MEMBER_H_
diff --git a/src/ast/struct_member_decoration.cc b/src/ast/struct_member_decoration.cc
new file mode 100644
index 0000000..bcb091a
--- /dev/null
+++ b/src/ast/struct_member_decoration.cc
@@ -0,0 +1,34 @@
+// 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/ast/struct_member_decoration.h"
+
+#include <assert.h>
+
+#include "src/ast/struct_member_offset_decoration.h"
+
+namespace tint {
+namespace ast {
+
+StructMemberDecoration::StructMemberDecoration() = default;
+
+StructMemberDecoration::~StructMemberDecoration() = default;
+
+StructMemberOffsetDecoration* StructMemberDecoration::AsOffset() {
+  assert(IsOffset());
+  return static_cast<StructMemberOffsetDecoration*>(this);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/struct_member_decoration.h b/src/ast/struct_member_decoration.h
new file mode 100644
index 0000000..525bb64
--- /dev/null
+++ b/src/ast/struct_member_decoration.h
@@ -0,0 +1,46 @@
+// 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_AST_STRUCT_MEMBER_DECORATION_H_
+#define SRC_AST_STRUCT_MEMBER_DECORATION_H_
+
+#include <string>
+
+namespace tint {
+namespace ast {
+
+class StructMemberOffsetDecoration;
+
+/// A decoration attached to a struct member
+class StructMemberDecoration {
+ public:
+  virtual ~StructMemberDecoration();
+
+  /// @returns true if this is an offset decoration
+  virtual bool IsOffset() const { return false; }
+
+  /// @returns the decoration as an offset decoration
+  StructMemberOffsetDecoration* AsOffset();
+
+  /// @returns the decoration as a string
+  virtual std::string to_str() const = 0;
+
+ protected:
+  StructMemberDecoration();
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_STRUCT_MEMBER_DECORATION_H_
diff --git a/src/ast/struct_member_offset_decoration.cc b/src/ast/struct_member_offset_decoration.cc
new file mode 100644
index 0000000..0a70a95
--- /dev/null
+++ b/src/ast/struct_member_offset_decoration.cc
@@ -0,0 +1,30 @@
+// 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/ast/struct_member_offset_decoration.h"
+
+namespace tint {
+namespace ast {
+
+StructMemberOffsetDecoration::StructMemberOffsetDecoration(size_t offset)
+    : offset_(offset) {}
+
+StructMemberOffsetDecoration::~StructMemberOffsetDecoration() = default;
+
+std::string StructMemberOffsetDecoration::to_str() const {
+  return "offset " + std::to_string(offset_);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/struct_member_offset_decoration.h b/src/ast/struct_member_offset_decoration.h
new file mode 100644
index 0000000..8e15a63
--- /dev/null
+++ b/src/ast/struct_member_offset_decoration.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_AST_STRUCT_MEMBER_OFFSET_DECORATION_H_
+#define SRC_AST_STRUCT_MEMBER_OFFSET_DECORATION_H_
+
+#include <stddef.h>
+
+#include <string>
+
+#include "src/ast/struct_member_decoration.h"
+
+namespace tint {
+namespace ast {
+
+/// A struct member offset decoration
+class StructMemberOffsetDecoration : public StructMemberDecoration {
+ public:
+  /// constructor
+  /// @param offset the offset value
+  explicit StructMemberOffsetDecoration(size_t offset);
+  ~StructMemberOffsetDecoration() override;
+
+  /// @returns true if this is an offset decoration
+  bool IsOffset() const override { return true; }
+
+  /// @returns the offset value
+  size_t offset() const { return offset_; }
+
+  /// @returns the decoration as a string
+  std::string to_str() const override;
+
+ private:
+  size_t offset_;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_STRUCT_MEMBER_OFFSET_DECORATION_H_
diff --git a/src/ast/struct_member_offset_decoration_test.cc b/src/ast/struct_member_offset_decoration_test.cc
new file mode 100644
index 0000000..cd2e2b9
--- /dev/null
+++ b/src/ast/struct_member_offset_decoration_test.cc
@@ -0,0 +1,35 @@
+// 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/ast/struct_member_offset_decoration.h"
+
+#include "gtest/gtest.h"
+
+namespace tint {
+namespace ast {
+
+using StructMemberOffsetDecorationTest = testing::Test;
+
+TEST_F(StructMemberOffsetDecorationTest, Creation) {
+  StructMemberOffsetDecoration d{2};
+  EXPECT_EQ(2, d.offset());
+}
+
+TEST_F(StructMemberOffsetDecorationTest, Is) {
+  StructMemberOffsetDecoration d{2};
+  EXPECT_TRUE(d.IsOffset());
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/struct_member_test.cc b/src/ast/struct_member_test.cc
new file mode 100644
index 0000000..9451d99
--- /dev/null
+++ b/src/ast/struct_member_test.cc
@@ -0,0 +1,92 @@
+// 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/ast/struct_member.h"
+
+#include <sstream>
+#include <utility>
+
+#include "gtest/gtest.h"
+#include "src/ast/struct_member_offset_decoration.h"
+#include "src/ast/type/i32_type.h"
+
+namespace tint {
+namespace ast {
+
+using StructMemberTest = testing::Test;
+
+TEST_F(StructMemberTest, Creation) {
+  type::I32Type i32;
+  std::vector<std::unique_ptr<StructMemberDecoration>> decorations;
+  decorations.emplace_back(std::make_unique<StructMemberOffsetDecoration>(4));
+
+  StructMember st{"a", &i32, std::move(decorations)};
+  EXPECT_EQ(st.name(), "a");
+  EXPECT_EQ(st.type(), &i32);
+  EXPECT_EQ(st.decorations().size(), 1);
+  EXPECT_TRUE(st.decorations()[0]->IsOffset());
+  EXPECT_EQ(st.line(), 0);
+  EXPECT_EQ(st.column(), 0);
+}
+
+TEST_F(StructMemberTest, CreationWithSource) {
+  type::I32Type i32;
+  Source s{27, 4};
+
+  StructMember st{s, "a", &i32, {}};
+  EXPECT_EQ(st.name(), "a");
+  EXPECT_EQ(st.type(), &i32);
+  EXPECT_EQ(st.decorations().size(), 0);
+  EXPECT_EQ(st.line(), 27);
+  EXPECT_EQ(st.column(), 4);
+}
+
+TEST_F(StructMemberTest, IsValid) {
+  type::I32Type i32;
+  StructMember st{"a", &i32, {}};
+  EXPECT_TRUE(st.IsValid());
+}
+
+TEST_F(StructMemberTest, IsValid_EmptyName) {
+  type::I32Type i32;
+  StructMember st{"", &i32, {}};
+  EXPECT_FALSE(st.IsValid());
+}
+
+TEST_F(StructMemberTest, IsValid_NullType) {
+  StructMember st{"a", nullptr, {}};
+  EXPECT_FALSE(st.IsValid());
+}
+
+TEST_F(StructMemberTest, ToStr) {
+  type::I32Type i32;
+  std::vector<std::unique_ptr<StructMemberDecoration>> decorations;
+  decorations.emplace_back(std::make_unique<StructMemberOffsetDecoration>(4));
+
+  StructMember st{"a", &i32, std::move(decorations)};
+  std::ostringstream out;
+  st.to_str(out, 0);
+  EXPECT_EQ(out.str(), "StructMember{[[ offset 4 ]] a: __i32}\n");
+}
+
+TEST_F(StructMemberTest, ToStrNoDecorations) {
+  type::I32Type i32;
+  StructMember st{"a", &i32, {}};
+  std::ostringstream out;
+  st.to_str(out, 0);
+  EXPECT_EQ(out.str(), "StructMember{a: __i32}\n");
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/struct_test.cc b/src/ast/struct_test.cc
new file mode 100644
index 0000000..9802270
--- /dev/null
+++ b/src/ast/struct_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/ast/struct.h"
+
+#include <sstream>
+#include <utility>
+
+#include "gtest/gtest.h"
+#include "src/ast/struct_decoration.h"
+#include "src/ast/struct_member.h"
+#include "src/ast/type/i32_type.h"
+
+namespace tint {
+namespace ast {
+
+using StructTest = testing::Test;
+
+TEST_F(StructTest, Creation) {
+  type::I32Type i32;
+  std::vector<std::unique_ptr<StructMember>> members;
+  members.push_back(std::make_unique<StructMember>(
+      "a", &i32, std::vector<std::unique_ptr<StructMemberDecoration>>()));
+
+  Struct s{StructDecoration::kNone, std::move(members)};
+  EXPECT_EQ(s.members().size(), 1);
+  EXPECT_EQ(s.decoration(), StructDecoration::kNone);
+  EXPECT_EQ(s.line(), 0);
+  EXPECT_EQ(s.column(), 0);
+}
+
+TEST_F(StructTest, CreationWithSource) {
+  type::I32Type i32;
+  Source source{27, 4};
+  std::vector<std::unique_ptr<StructMember>> members;
+  members.emplace_back(std::make_unique<StructMember>(
+      "a", &i32, std::vector<std::unique_ptr<StructMemberDecoration>>()));
+
+  Struct s{source, StructDecoration::kNone, std::move(members)};
+  EXPECT_EQ(s.members().size(), 1);
+  EXPECT_EQ(s.decoration(), StructDecoration::kNone);
+  EXPECT_EQ(s.line(), 27);
+  EXPECT_EQ(s.column(), 4);
+}
+
+TEST_F(StructTest, IsValid) {
+  Struct s;
+  EXPECT_TRUE(s.IsValid());
+}
+
+TEST_F(StructTest, ToStr) {
+  type::I32Type i32;
+  Source source{27, 4};
+  std::vector<std::unique_ptr<StructMember>> members;
+  members.emplace_back(std::make_unique<StructMember>(
+      "a", &i32, std::vector<std::unique_ptr<StructMemberDecoration>>()));
+
+  Struct s{source, StructDecoration::kNone, std::move(members)};
+
+  std::ostringstream out;
+  s.to_str(out, 0);
+  EXPECT_EQ(out.str(), R"(Struct{
+  StructMember{a: __i32}
+}
+)");
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/switch_statement.cc b/src/ast/switch_statement.cc
new file mode 100644
index 0000000..aabc242
--- /dev/null
+++ b/src/ast/switch_statement.cc
@@ -0,0 +1,58 @@
+// 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/ast/switch_statement.h"
+
+#include "src/ast/case_statement.h"
+
+namespace tint {
+namespace ast {
+
+SwitchStatement::SwitchStatement() : Statement() {}
+
+SwitchStatement::SwitchStatement(
+    std::unique_ptr<Expression> condition,
+    std::vector<std::unique_ptr<CaseStatement>> body)
+    : Statement(), condition_(std::move(condition)), body_(std::move(body)) {}
+
+SwitchStatement::SwitchStatement(
+    const Source& source,
+    std::unique_ptr<Expression> condition,
+    std::vector<std::unique_ptr<CaseStatement>> body)
+    : Statement(source),
+      condition_(std::move(condition)),
+      body_(std::move(body)) {}
+
+SwitchStatement::~SwitchStatement() = default;
+
+bool SwitchStatement::IsValid() const {
+  return condition_ != nullptr;
+}
+
+void SwitchStatement::to_str(std::ostream& out, size_t indent) const {
+  make_indent(out, indent);
+  out << "Switch{" << std::endl;
+  condition_->to_str(out, indent + 2);
+  make_indent(out, indent + 2);
+  out << "{" << std::endl;
+
+  for (const auto& stmt : body_)
+    stmt->to_str(out, indent + 4);
+
+  make_indent(out, indent + 2);
+  out << "}" << std::endl;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/switch_statement.h b/src/ast/switch_statement.h
new file mode 100644
index 0000000..9d13995
--- /dev/null
+++ b/src/ast/switch_statement.h
@@ -0,0 +1,92 @@
+// 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_AST_SWITCH_STATEMENT_H_
+#define SRC_AST_SWITCH_STATEMENT_H_
+
+#include <memory>
+#include <utility>
+#include <vector>
+
+#include "src/ast/expression.h"
+#include "src/ast/literal.h"
+#include "src/ast/statement.h"
+#include "src/ast/statement_condition.h"
+
+namespace tint {
+namespace ast {
+
+/// A switch statement
+class SwitchStatement : public Statement {
+ public:
+  /// Constructor
+  SwitchStatement();
+  /// Constructor
+  /// @param condition the switch condition
+  /// @param body the switch body
+  SwitchStatement(std::unique_ptr<Expression> condition,
+                  std::vector<std::unique_ptr<CaseStatement>> body);
+  /// Constructor
+  /// @param source the source information
+  /// @param condition the switch condition
+  /// @param body the switch body
+  SwitchStatement(const Source& source,
+                  std::unique_ptr<Expression> condition,
+                  std::vector<std::unique_ptr<CaseStatement>> body);
+  /// Move constructor
+  SwitchStatement(SwitchStatement&&) = default;
+  ~SwitchStatement() override;
+
+  /// Sets the condition for the switch statement
+  /// @param condition the condition to set
+  void set_condition(std::unique_ptr<Expression> condition) {
+    condition_ = std::move(condition);
+  }
+  /// @returns the switch condition or nullptr if none set
+  Expression* condition() const { return condition_.get(); }
+  /// @returns true if this is a default statement
+  bool IsDefault() const { return condition_ == nullptr; }
+
+  /// Sets the switch body
+  /// @param body the switch body
+  void set_body(std::vector<std::unique_ptr<CaseStatement>> body) {
+    body_ = std::move(body);
+  }
+  /// @returns the Switch body
+  const std::vector<std::unique_ptr<CaseStatement>>& body() const {
+    return body_;
+  }
+
+  /// @returns true if this is a switch statement
+  bool IsSwitch() const override { return true; }
+
+  /// @returns true if the node is valid
+  bool IsValid() const override;
+
+  /// Writes a representation of the node to the output stream
+  /// @param out the stream to write to
+  /// @param indent number of spaces to indent the node when writing
+  void to_str(std::ostream& out, size_t indent) const override;
+
+ private:
+  SwitchStatement(const SwitchStatement&) = delete;
+
+  std::unique_ptr<Expression> condition_;
+  std::vector<std::unique_ptr<CaseStatement>> body_;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_SWITCH_STATEMENT_H_
diff --git a/src/ast/type/alias_type.cc b/src/ast/type/alias_type.cc
new file mode 100644
index 0000000..199557e
--- /dev/null
+++ b/src/ast/type/alias_type.cc
@@ -0,0 +1,32 @@
+// 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/ast/type/alias_type.h"
+
+#include <assert.h>
+
+namespace tint {
+namespace ast {
+namespace type {
+
+AliasType::AliasType(const std::string& name, Type* subtype)
+    : name_(name), subtype_(subtype) {
+  assert(subtype_);
+}
+
+AliasType::~AliasType() = default;
+
+}  // namespace type
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/type/alias_type.h b/src/ast/type/alias_type.h
new file mode 100644
index 0000000..a555951
--- /dev/null
+++ b/src/ast/type/alias_type.h
@@ -0,0 +1,59 @@
+// 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_AST_TYPE_ALIAS_TYPE_H_
+#define SRC_AST_TYPE_ALIAS_TYPE_H_
+
+#include <string>
+
+#include "src/ast/type/type.h"
+
+namespace tint {
+namespace ast {
+namespace type {
+
+/// A type alias type. Holds a name a pointer to another type.
+class AliasType : public Type {
+ public:
+  /// Constructor
+  /// @param name the alias name
+  /// @param subtype the alias'd type
+  AliasType(const std::string& name, Type* subtype);
+  /// Move constructor
+  AliasType(AliasType&&) = default;
+  ~AliasType() override;
+
+  /// @returns true if the type is an alias type
+  bool IsAlias() const override { return true; }
+
+  /// @returns the alias name
+  const std::string& name() const { return name_; }
+  /// @returns the alias type
+  Type* type() const { return subtype_; }
+
+  /// @returns the name for this type
+  std::string type_name() const override {
+    return "__alias_" + name_ + subtype_->type_name();
+  }
+
+ private:
+  std::string name_;
+  Type* subtype_ = nullptr;
+};
+
+}  // namespace type
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_TYPE_ALIAS_TYPE_H_
diff --git a/src/ast/type/alias_type_test.cc b/src/ast/type/alias_type_test.cc
new file mode 100644
index 0000000..e3c3126
--- /dev/null
+++ b/src/ast/type/alias_type_test.cc
@@ -0,0 +1,58 @@
+// 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/ast/type/alias_type.h"
+
+#include "gtest/gtest.h"
+#include "src/ast/type/i32_type.h"
+#include "src/ast/type/u32_type.h"
+
+namespace tint {
+namespace ast {
+namespace type {
+
+using AliasTypeTest = testing::Test;
+
+TEST_F(AliasTypeTest, Create) {
+  U32Type u32;
+  AliasType a{"a_type", &u32};
+  EXPECT_EQ(a.name(), "a_type");
+  EXPECT_EQ(a.type(), &u32);
+}
+
+TEST_F(AliasTypeTest, Is) {
+  I32Type i32;
+
+  AliasType at{"a", &i32};
+  EXPECT_TRUE(at.IsAlias());
+  EXPECT_FALSE(at.IsArray());
+  EXPECT_FALSE(at.IsBool());
+  EXPECT_FALSE(at.IsF32());
+  EXPECT_FALSE(at.IsI32());
+  EXPECT_FALSE(at.IsMatrix());
+  EXPECT_FALSE(at.IsPointer());
+  EXPECT_FALSE(at.IsStruct());
+  EXPECT_FALSE(at.IsU32());
+  EXPECT_FALSE(at.IsVector());
+}
+
+TEST_F(AliasTypeTest, TypeName) {
+  I32Type i32;
+  AliasType at{"Particle", &i32};
+  EXPECT_EQ(at.type_name(), "__alias_Particle__i32");
+}
+
+}  // namespace type
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/type/array_type.cc b/src/ast/type/array_type.cc
new file mode 100644
index 0000000..c9eb6b6
--- /dev/null
+++ b/src/ast/type/array_type.cc
@@ -0,0 +1,30 @@
+// 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/ast/type/array_type.h"
+
+namespace tint {
+namespace ast {
+namespace type {
+
+ArrayType::ArrayType(Type* subtype) : subtype_(subtype) {}
+
+ArrayType::ArrayType(Type* subtype, size_t size)
+    : subtype_(subtype), size_(size) {}
+
+ArrayType::~ArrayType() = default;
+
+}  // namespace type
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/type/array_type.h b/src/ast/type/array_type.h
new file mode 100644
index 0000000..28d25e0
--- /dev/null
+++ b/src/ast/type/array_type.h
@@ -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.
+
+#ifndef SRC_AST_TYPE_ARRAY_TYPE_H_
+#define SRC_AST_TYPE_ARRAY_TYPE_H_
+
+#include <assert.h>
+
+#include <string>
+
+#include "src/ast/type/type.h"
+
+namespace tint {
+namespace ast {
+namespace type {
+
+/// An array type. If size is zero then it is a runtime array.
+class ArrayType : public Type {
+ public:
+  /// Constructor for runtime array
+  /// @param subtype the type of the array elements
+  explicit ArrayType(Type* subtype);
+  /// Constructor
+  /// @param subtype the type of the array elements
+  /// @param size the number of elements in the array
+  ArrayType(Type* subtype, size_t size);
+  /// Move constructor
+  ArrayType(ArrayType&&) = default;
+  ~ArrayType() override;
+
+  /// @returns true if the type is an array type
+  bool IsArray() const override { return true; }
+  /// @returns true if this is a runtime array.
+  /// i.e. the size is determined at runtime
+  bool IsRuntimeArray() const { return size_ == 0; }
+
+  /// @returns the array type
+  Type* type() const { return subtype_; }
+  /// @returns the array size. Size is 0 for a runtime array
+  size_t size() const { return size_; }
+
+  /// @returns the name for th type
+  std::string type_name() const override {
+    assert(subtype_);
+
+    std::string type_name = "__array" + subtype_->type_name();
+    if (!IsRuntimeArray())
+      type_name += "_" + std::to_string(size_);
+
+    return type_name;
+  }
+
+ private:
+  Type* subtype_ = nullptr;
+  size_t size_ = 0;
+};
+
+}  // namespace type
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_TYPE_ARRAY_TYPE_H_
diff --git a/src/ast/type/array_type_test.cc b/src/ast/type/array_type_test.cc
new file mode 100644
index 0000000..5495f5b
--- /dev/null
+++ b/src/ast/type/array_type_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 "src/ast/type/array_type.h"
+
+#include "gtest/gtest.h"
+#include "src/ast/type/i32_type.h"
+#include "src/ast/type/u32_type.h"
+
+namespace tint {
+namespace ast {
+namespace type {
+
+using ArrayTypeTest = testing::Test;
+
+TEST_F(ArrayTypeTest, CreateSizedArray) {
+  U32Type u32;
+  ArrayType arr{&u32, 3};
+  EXPECT_EQ(arr.type(), &u32);
+  EXPECT_EQ(arr.size(), 3);
+  EXPECT_TRUE(arr.IsArray());
+  EXPECT_FALSE(arr.IsRuntimeArray());
+}
+
+TEST_F(ArrayTypeTest, CreateRuntimeArray) {
+  U32Type u32;
+  ArrayType arr{&u32};
+  EXPECT_EQ(arr.type(), &u32);
+  EXPECT_EQ(arr.size(), 0);
+  EXPECT_TRUE(arr.IsArray());
+  EXPECT_TRUE(arr.IsRuntimeArray());
+}
+
+TEST_F(ArrayTypeTest, Is) {
+  I32Type i32;
+
+  ArrayType arr{&i32, 3};
+  EXPECT_FALSE(arr.IsAlias());
+  EXPECT_TRUE(arr.IsArray());
+  EXPECT_FALSE(arr.IsBool());
+  EXPECT_FALSE(arr.IsF32());
+  EXPECT_FALSE(arr.IsI32());
+  EXPECT_FALSE(arr.IsMatrix());
+  EXPECT_FALSE(arr.IsPointer());
+  EXPECT_FALSE(arr.IsStruct());
+  EXPECT_FALSE(arr.IsU32());
+  EXPECT_FALSE(arr.IsVector());
+}
+
+TEST_F(ArrayTypeTest, TypeName) {
+  I32Type i32;
+  ArrayType arr{&i32, 3};
+  EXPECT_EQ(arr.type_name(), "__array__i32_3");
+}
+
+}  // namespace type
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/type/bool_type.cc b/src/ast/type/bool_type.cc
new file mode 100644
index 0000000..d584181
--- /dev/null
+++ b/src/ast/type/bool_type.cc
@@ -0,0 +1,27 @@
+// 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/ast/type/bool_type.h"
+
+namespace tint {
+namespace ast {
+namespace type {
+
+BoolType::BoolType() = default;
+
+BoolType::~BoolType() = default;
+
+}  // namespace type
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/type/bool_type.h b/src/ast/type/bool_type.h
new file mode 100644
index 0000000..79c4d63
--- /dev/null
+++ b/src/ast/type/bool_type.h
@@ -0,0 +1,46 @@
+// 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_AST_TYPE_BOOL_TYPE_H_
+#define SRC_AST_TYPE_BOOL_TYPE_H_
+
+#include <string>
+
+#include "src/ast/type/type.h"
+
+namespace tint {
+namespace ast {
+namespace type {
+
+/// A boolean type
+class BoolType : public Type {
+ public:
+  /// Constructor
+  BoolType();
+  /// Move constructor
+  BoolType(BoolType&&) = default;
+  ~BoolType() override;
+
+  /// @returns true if the type is a bool type
+  bool IsBool() const override { return true; }
+
+  /// @returns the name for this type
+  std::string type_name() const override { return "__bool"; }
+};
+
+}  // namespace type
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_TYPE_BOOL_TYPE_H_
diff --git a/src/ast/type/bool_type_test.cc b/src/ast/type/bool_type_test.cc
new file mode 100644
index 0000000..6aacfe8
--- /dev/null
+++ b/src/ast/type/bool_type_test.cc
@@ -0,0 +1,46 @@
+// 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/ast/type/bool_type.h"
+
+#include "gtest/gtest.h"
+
+namespace tint {
+namespace ast {
+namespace type {
+
+using BoolTypeTest = testing::Test;
+
+TEST_F(BoolTypeTest, Is) {
+  BoolType b;
+  EXPECT_FALSE(b.IsAlias());
+  EXPECT_FALSE(b.IsArray());
+  EXPECT_TRUE(b.IsBool());
+  EXPECT_FALSE(b.IsF32());
+  EXPECT_FALSE(b.IsI32());
+  EXPECT_FALSE(b.IsMatrix());
+  EXPECT_FALSE(b.IsPointer());
+  EXPECT_FALSE(b.IsStruct());
+  EXPECT_FALSE(b.IsU32());
+  EXPECT_FALSE(b.IsVector());
+}
+
+TEST_F(BoolTypeTest, TypeName) {
+  BoolType b;
+  EXPECT_EQ(b.type_name(), "__bool");
+}
+
+}  // namespace type
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/type/f32_type.cc b/src/ast/type/f32_type.cc
new file mode 100644
index 0000000..065c22e
--- /dev/null
+++ b/src/ast/type/f32_type.cc
@@ -0,0 +1,27 @@
+// 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/ast/type/f32_type.h"
+
+namespace tint {
+namespace ast {
+namespace type {
+
+F32Type::F32Type() = default;
+
+F32Type::~F32Type() = default;
+
+}  // namespace type
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/type/f32_type.h b/src/ast/type/f32_type.h
new file mode 100644
index 0000000..881ec4ae
--- /dev/null
+++ b/src/ast/type/f32_type.h
@@ -0,0 +1,46 @@
+// 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_AST_TYPE_F32_TYPE_H_
+#define SRC_AST_TYPE_F32_TYPE_H_
+
+#include <string>
+
+#include "src/ast/type/type.h"
+
+namespace tint {
+namespace ast {
+namespace type {
+
+/// A float 32 type
+class F32Type : public Type {
+ public:
+  /// Constructor
+  F32Type();
+  /// Move constructor
+  F32Type(F32Type&&) = default;
+  ~F32Type() override;
+
+  /// @returns true if the type is an f32 type
+  bool IsF32() const override { return true; }
+
+  /// @returns the name for this type
+  std::string type_name() const override { return "__f32"; }
+};
+
+}  // namespace type
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_TYPE_F32_TYPE_H_
diff --git a/src/ast/type/f32_type_test.cc b/src/ast/type/f32_type_test.cc
new file mode 100644
index 0000000..a5287f0
--- /dev/null
+++ b/src/ast/type/f32_type_test.cc
@@ -0,0 +1,46 @@
+// 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/ast/type/f32_type.h"
+
+#include "gtest/gtest.h"
+
+namespace tint {
+namespace ast {
+namespace type {
+
+using F32TypeTest = testing::Test;
+
+TEST_F(F32TypeTest, Is) {
+  F32Type f;
+  EXPECT_FALSE(f.IsAlias());
+  EXPECT_FALSE(f.IsArray());
+  EXPECT_FALSE(f.IsBool());
+  EXPECT_TRUE(f.IsF32());
+  EXPECT_FALSE(f.IsI32());
+  EXPECT_FALSE(f.IsMatrix());
+  EXPECT_FALSE(f.IsPointer());
+  EXPECT_FALSE(f.IsStruct());
+  EXPECT_FALSE(f.IsU32());
+  EXPECT_FALSE(f.IsVector());
+}
+
+TEST_F(F32TypeTest, TypeName) {
+  F32Type f;
+  EXPECT_EQ(f.type_name(), "__f32");
+}
+
+}  // namespace type
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/type/i32_type.cc b/src/ast/type/i32_type.cc
new file mode 100644
index 0000000..0a7194d
--- /dev/null
+++ b/src/ast/type/i32_type.cc
@@ -0,0 +1,27 @@
+// 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/ast/type/i32_type.h"
+
+namespace tint {
+namespace ast {
+namespace type {
+
+I32Type::I32Type() = default;
+
+I32Type::~I32Type() = default;
+
+}  // namespace type
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/type/i32_type.h b/src/ast/type/i32_type.h
new file mode 100644
index 0000000..ea60c13
--- /dev/null
+++ b/src/ast/type/i32_type.h
@@ -0,0 +1,46 @@
+// 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_AST_TYPE_I32_TYPE_H_
+#define SRC_AST_TYPE_I32_TYPE_H_
+
+#include <string>
+
+#include "src/ast/type/type.h"
+
+namespace tint {
+namespace ast {
+namespace type {
+
+/// A signed int 32 type.
+class I32Type : public Type {
+ public:
+  /// Constructor
+  I32Type();
+  /// Move constructor
+  I32Type(I32Type&&) = default;
+  ~I32Type() override;
+
+  /// @returns true if the type is an i32 type
+  bool IsI32() const override { return true; }
+
+  /// @returns the name for this type
+  std::string type_name() const override { return "__i32"; }
+};
+
+}  // namespace type
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_TYPE_I32_TYPE_H_
diff --git a/src/ast/type/i32_type_test.cc b/src/ast/type/i32_type_test.cc
new file mode 100644
index 0000000..67f2483
--- /dev/null
+++ b/src/ast/type/i32_type_test.cc
@@ -0,0 +1,46 @@
+// 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/ast/type/i32_type.h"
+
+#include "gtest/gtest.h"
+
+namespace tint {
+namespace ast {
+namespace type {
+
+using I32TypeTest = testing::Test;
+
+TEST_F(I32TypeTest, Is) {
+  I32Type i;
+  EXPECT_FALSE(i.IsAlias());
+  EXPECT_FALSE(i.IsArray());
+  EXPECT_FALSE(i.IsBool());
+  EXPECT_FALSE(i.IsF32());
+  EXPECT_TRUE(i.IsI32());
+  EXPECT_FALSE(i.IsMatrix());
+  EXPECT_FALSE(i.IsPointer());
+  EXPECT_FALSE(i.IsStruct());
+  EXPECT_FALSE(i.IsU32());
+  EXPECT_FALSE(i.IsVector());
+}
+
+TEST_F(I32TypeTest, TypeName) {
+  I32Type i;
+  EXPECT_EQ(i.type_name(), "__i32");
+}
+
+}  // namespace type
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/type/matrix_type.cc b/src/ast/type/matrix_type.cc
new file mode 100644
index 0000000..64aef3e
--- /dev/null
+++ b/src/ast/type/matrix_type.cc
@@ -0,0 +1,35 @@
+// 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/ast/type/matrix_type.h"
+
+#include <assert.h>
+
+namespace tint {
+namespace ast {
+namespace type {
+
+MatrixType::MatrixType(Type* subtype, size_t rows, size_t columns)
+    : subtype_(subtype), rows_(rows), columns_(columns) {
+  assert(rows > 1);
+  assert(rows < 5);
+  assert(columns > 1);
+  assert(columns < 5);
+}
+
+MatrixType::~MatrixType() = default;
+
+}  // namespace type
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/type/matrix_type.h b/src/ast/type/matrix_type.h
new file mode 100644
index 0000000..f37d12a
--- /dev/null
+++ b/src/ast/type/matrix_type.h
@@ -0,0 +1,64 @@
+// 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_AST_TYPE_MATRIX_TYPE_H_
+#define SRC_AST_TYPE_MATRIX_TYPE_H_
+
+#include <string>
+
+#include "src/ast/type/type.h"
+
+namespace tint {
+namespace ast {
+namespace type {
+
+/// A matrix type
+class MatrixType : public Type {
+ public:
+  /// Constructor
+  /// @param subtype type matrix type
+  /// @param rows the number of rows in the matrix
+  /// @param columns the number of columns in the matrix
+  MatrixType(Type* subtype, size_t rows, size_t columns);
+  /// Move constructor
+  MatrixType(MatrixType&&) = default;
+  ~MatrixType() override;
+
+  /// @returns true if the type is a matrix type
+  bool IsMatrix() const override { return true; }
+
+  /// @returns the type of the matrix
+  Type* type() const { return subtype_; }
+  /// @returns the number of rows in the matrix
+  size_t rows() const { return rows_; }
+  /// @returns the number of columns in the matrix
+  size_t columns() const { return columns_; }
+
+  /// @returns the name for this type
+  std::string type_name() const override {
+    return "__mat_" + std::to_string(rows_) + "_" + std::to_string(columns_) +
+           subtype_->type_name();
+  }
+
+ private:
+  Type* subtype_ = nullptr;
+  size_t rows_ = 2;
+  size_t columns_ = 2;
+};
+
+}  // namespace type
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_TYPE_MATRIX_TYPE_H_
diff --git a/src/ast/type/matrix_type_test.cc b/src/ast/type/matrix_type_test.cc
new file mode 100644
index 0000000..0bdb7ea
--- /dev/null
+++ b/src/ast/type/matrix_type_test.cc
@@ -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.
+
+#include "src/ast/type/matrix_type.h"
+
+#include "gtest/gtest.h"
+#include "src/ast/type/i32_type.h"
+
+namespace tint {
+namespace ast {
+namespace type {
+
+using MatrixTypeTest = testing::Test;
+
+TEST_F(MatrixTypeTest, Creation) {
+  I32Type i32;
+  MatrixType m{&i32, 2, 4};
+  EXPECT_EQ(m.type(), &i32);
+  EXPECT_EQ(m.rows(), 2);
+  EXPECT_EQ(m.columns(), 4);
+}
+
+TEST_F(MatrixTypeTest, Is) {
+  I32Type i32;
+  MatrixType m{&i32, 2, 3};
+  EXPECT_FALSE(m.IsAlias());
+  EXPECT_FALSE(m.IsArray());
+  EXPECT_FALSE(m.IsBool());
+  EXPECT_FALSE(m.IsF32());
+  EXPECT_FALSE(m.IsI32());
+  EXPECT_TRUE(m.IsMatrix());
+  EXPECT_FALSE(m.IsPointer());
+  EXPECT_FALSE(m.IsStruct());
+  EXPECT_FALSE(m.IsU32());
+  EXPECT_FALSE(m.IsVector());
+}
+
+TEST_F(MatrixTypeTest, TypeName) {
+  I32Type i32;
+  MatrixType m{&i32, 2, 3};
+  EXPECT_EQ(m.type_name(), "__mat_2_3__i32");
+}
+
+}  // namespace type
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/type/pointer_type.cc b/src/ast/type/pointer_type.cc
new file mode 100644
index 0000000..2b8e9ce
--- /dev/null
+++ b/src/ast/type/pointer_type.cc
@@ -0,0 +1,28 @@
+// 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/ast/type/pointer_type.h"
+
+namespace tint {
+namespace ast {
+namespace type {
+
+PointerType::PointerType(Type* subtype, StorageClass storage_class)
+    : subtype_(subtype), storage_class_(storage_class) {}
+
+PointerType::~PointerType() = default;
+
+}  // namespace type
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/type/pointer_type.h b/src/ast/type/pointer_type.h
new file mode 100644
index 0000000..40ea116
--- /dev/null
+++ b/src/ast/type/pointer_type.h
@@ -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.
+
+#ifndef SRC_AST_TYPE_POINTER_TYPE_H_
+#define SRC_AST_TYPE_POINTER_TYPE_H_
+
+#include <sstream>
+#include <string>
+
+#include "src/ast/storage_class.h"
+#include "src/ast/type/type.h"
+
+namespace tint {
+namespace ast {
+namespace type {
+
+/// A pointer type.
+class PointerType : public Type {
+ public:
+  /// Construtor
+  /// @param subtype the pointee type
+  /// @param storage_class the storage class of the pointer
+  explicit PointerType(Type* subtype, StorageClass storage_class);
+  /// Move constructor
+  PointerType(PointerType&&) = default;
+  ~PointerType() override;
+
+  /// @returns true if the type is a pointer type
+  bool IsPointer() const override { return true; }
+
+  /// @returns the pointee type
+  Type* type() const { return subtype_; }
+  /// @returns the storage class of the pointer
+  StorageClass storage_class() const { return storage_class_; }
+
+  /// @returns the name for this type
+  std::string type_name() const override {
+    std::ostringstream out;
+    out << "__ptr_" << storage_class_ << subtype_->type_name();
+    return out.str();
+  }
+
+ private:
+  Type* subtype_;
+  StorageClass storage_class_;
+};
+
+}  // namespace type
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_TYPE_POINTER_TYPE_H_
diff --git a/src/ast/type/pointer_type_test.cc b/src/ast/type/pointer_type_test.cc
new file mode 100644
index 0000000..52d167f
--- /dev/null
+++ b/src/ast/type/pointer_type_test.cc
@@ -0,0 +1,56 @@
+// 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/ast/type/pointer_type.h"
+
+#include "gtest/gtest.h"
+#include "src/ast/type/i32_type.h"
+
+namespace tint {
+namespace ast {
+namespace type {
+
+using PointerTypeTest = testing::Test;
+
+TEST_F(PointerTypeTest, Creation) {
+  I32Type i32;
+  PointerType p{&i32, StorageClass::kStorageBuffer};
+  EXPECT_EQ(p.type(), &i32);
+  EXPECT_EQ(p.storage_class(), StorageClass::kStorageBuffer);
+}
+
+TEST_F(PointerTypeTest, Is) {
+  I32Type i32;
+  PointerType p{&i32, StorageClass::kFunction};
+  EXPECT_FALSE(p.IsAlias());
+  EXPECT_FALSE(p.IsArray());
+  EXPECT_FALSE(p.IsBool());
+  EXPECT_FALSE(p.IsF32());
+  EXPECT_FALSE(p.IsI32());
+  EXPECT_FALSE(p.IsMatrix());
+  EXPECT_TRUE(p.IsPointer());
+  EXPECT_FALSE(p.IsStruct());
+  EXPECT_FALSE(p.IsU32());
+  EXPECT_FALSE(p.IsVector());
+}
+
+TEST_F(PointerTypeTest, TypeName) {
+  I32Type i32;
+  PointerType p{&i32, StorageClass::kWorkgroup};
+  EXPECT_EQ(p.type_name(), "__ptr_workgroup__i32");
+}
+
+}  // namespace type
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/type/struct_type.cc b/src/ast/type/struct_type.cc
new file mode 100644
index 0000000..d2fcfdc
--- /dev/null
+++ b/src/ast/type/struct_type.cc
@@ -0,0 +1,30 @@
+// 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/ast/type/struct_type.h"
+
+#include <utility>
+
+namespace tint {
+namespace ast {
+namespace type {
+
+StructType::StructType(std::unique_ptr<Struct> impl)
+    : struct_(std::move(impl)) {}
+
+StructType::~StructType() = default;
+
+}  // namespace type
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/type/struct_type.h b/src/ast/type/struct_type.h
new file mode 100644
index 0000000..c33cd27
--- /dev/null
+++ b/src/ast/type/struct_type.h
@@ -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.
+
+#ifndef SRC_AST_TYPE_STRUCT_TYPE_H_
+#define SRC_AST_TYPE_STRUCT_TYPE_H_
+
+#include <memory>
+#include <string>
+
+#include "src/ast/struct.h"
+#include "src/ast/type/type.h"
+
+namespace tint {
+namespace ast {
+namespace type {
+
+/// A structure type
+class StructType : public Type {
+ public:
+  /// Constructor
+  /// @param impl the struct data
+  explicit StructType(std::unique_ptr<Struct> impl);
+  /// Move constructor
+  StructType(StructType&&) = default;
+  ~StructType() override;
+
+  /// Sets the name of the struct
+  /// @param name the name to set
+  void set_name(const std::string& name) { name_ = name; }
+  /// @returns the struct name
+  const std::string& name() const { return name_; }
+
+  /// @returns true if the type is a struct type
+  bool IsStruct() const override { return true; }
+
+  /// @returns the struct name
+  Struct* impl() const { return struct_.get(); }
+
+  /// @returns the name for th type
+  std::string type_name() const override { return "__struct_" + name_; }
+
+ private:
+  std::string name_;
+  std::unique_ptr<Struct> struct_;
+};
+
+}  // namespace type
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_TYPE_STRUCT_TYPE_H_
diff --git a/src/ast/type/struct_type_test.cc b/src/ast/type/struct_type_test.cc
new file mode 100644
index 0000000..1721079
--- /dev/null
+++ b/src/ast/type/struct_type_test.cc
@@ -0,0 +1,59 @@
+// 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/ast/type/struct_type.h"
+
+#include <utility>
+
+#include "gtest/gtest.h"
+#include "src/ast/type/i32_type.h"
+
+namespace tint {
+namespace ast {
+namespace type {
+
+using StructTypeTest = testing::Test;
+
+TEST_F(StructTypeTest, Creation) {
+  auto impl = std::make_unique<Struct>();
+  auto ptr = impl.get();
+  StructType s{std::move(impl)};
+  EXPECT_EQ(s.impl(), ptr);
+}
+
+TEST_F(StructTypeTest, Is) {
+  auto impl = std::make_unique<Struct>();
+  StructType s{std::move(impl)};
+  EXPECT_FALSE(s.IsAlias());
+  EXPECT_FALSE(s.IsArray());
+  EXPECT_FALSE(s.IsBool());
+  EXPECT_FALSE(s.IsF32());
+  EXPECT_FALSE(s.IsI32());
+  EXPECT_FALSE(s.IsMatrix());
+  EXPECT_FALSE(s.IsPointer());
+  EXPECT_TRUE(s.IsStruct());
+  EXPECT_FALSE(s.IsU32());
+  EXPECT_FALSE(s.IsVector());
+}
+
+TEST_F(StructTypeTest, TypeName) {
+  auto impl = std::make_unique<Struct>();
+  StructType s{std::move(impl)};
+  s.set_name("my_struct");
+  EXPECT_EQ(s.type_name(), "__struct_my_struct");
+}
+
+}  // namespace type
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/type/type.cc b/src/ast/type/type.cc
new file mode 100644
index 0000000..00fe94d
--- /dev/null
+++ b/src/ast/type/type.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 "src/ast/type/type.h"
+
+#include <assert.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"
+
+namespace tint {
+namespace ast {
+namespace type {
+
+Type::Type() = default;
+
+Type::~Type() = default;
+
+AliasType* Type::AsAlias() {
+  assert(IsAlias());
+  return static_cast<AliasType*>(this);
+}
+
+ArrayType* Type::AsArray() {
+  assert(IsArray());
+  return static_cast<ArrayType*>(this);
+}
+
+BoolType* Type::AsBool() {
+  assert(IsBool());
+  return static_cast<BoolType*>(this);
+}
+
+F32Type* Type::AsF32() {
+  assert(IsF32());
+  return static_cast<F32Type*>(this);
+}
+
+I32Type* Type::AsI32() {
+  assert(IsI32());
+  return static_cast<I32Type*>(this);
+}
+
+MatrixType* Type::AsMatrix() {
+  assert(IsMatrix());
+  return static_cast<MatrixType*>(this);
+}
+
+PointerType* Type::AsPointer() {
+  assert(IsPointer());
+  return static_cast<PointerType*>(this);
+}
+
+StructType* Type::AsStruct() {
+  assert(IsStruct());
+  return static_cast<StructType*>(this);
+}
+
+U32Type* Type::AsU32() {
+  assert(IsU32());
+  return static_cast<U32Type*>(this);
+}
+
+VectorType* Type::AsVector() {
+  assert(IsVector());
+  return static_cast<VectorType*>(this);
+}
+
+VoidType* Type::AsVoid() {
+  assert(IsVoid());
+  return static_cast<VoidType*>(this);
+}
+
+}  // namespace type
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/type/type.h b/src/ast/type/type.h
new file mode 100644
index 0000000..cf66f3b
--- /dev/null
+++ b/src/ast/type/type.h
@@ -0,0 +1,100 @@
+// 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_AST_TYPE_TYPE_H_
+#define SRC_AST_TYPE_TYPE_H_
+
+#include <string>
+
+namespace tint {
+namespace ast {
+namespace type {
+
+class AliasType;
+class ArrayType;
+class BoolType;
+class F32Type;
+class I32Type;
+class MatrixType;
+class PointerType;
+class StructType;
+class U32Type;
+class VectorType;
+class VoidType;
+
+/// Base class for a type in the system
+class Type {
+ public:
+  /// Move constructor
+  Type(Type&&) = default;
+  virtual ~Type();
+
+  /// @returns true if the type is an alias type
+  virtual bool IsAlias() const { return false; }
+  /// @returns true if the type is an array type
+  virtual bool IsArray() const { return false; }
+  /// @returns true if the type is a bool type
+  virtual bool IsBool() const { return false; }
+  /// @returns true if the type is an f32 type
+  virtual bool IsF32() const { return false; }
+  /// @returns true if the type is an i32 type
+  virtual bool IsI32() const { return false; }
+  /// @returns true if the type is a matrix type
+  virtual bool IsMatrix() const { return false; }
+  /// @returns true if the type is a ptr type
+  virtual bool IsPointer() const { return false; }
+  /// @returns true if the type is a struct type
+  virtual bool IsStruct() const { return false; }
+  /// @returns true if the type is a u32 type
+  virtual bool IsU32() const { return false; }
+  /// @returns true if the type is a vec type
+  virtual bool IsVector() const { return false; }
+  /// @returns true if the type is a void type
+  virtual bool IsVoid() const { return false; }
+
+  /// @returns the name for this type
+  virtual std::string type_name() const = 0;
+
+  /// @returns the type as an alias type
+  AliasType* AsAlias();
+  /// @returns the type as an array type
+  ArrayType* AsArray();
+  /// @returns the type as a bool type
+  BoolType* AsBool();
+  /// @returns the type as a f32 type
+  F32Type* AsF32();
+  /// @returns the type as an i32 type
+  I32Type* AsI32();
+  /// @returns the type as a matrix type
+  MatrixType* AsMatrix();
+  /// @returns the type as a pointer type
+  PointerType* AsPointer();
+  /// @returns the type as a struct type
+  StructType* AsStruct();
+  /// @returns the type as a u32 type
+  U32Type* AsU32();
+  /// @returns the type as a vector type
+  VectorType* AsVector();
+  /// @returns the type as a void type
+  VoidType* AsVoid();
+
+ protected:
+  Type();
+};
+
+}  // namespace type
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_TYPE_TYPE_H_
diff --git a/src/ast/type/u32_type.cc b/src/ast/type/u32_type.cc
new file mode 100644
index 0000000..b155b1d
--- /dev/null
+++ b/src/ast/type/u32_type.cc
@@ -0,0 +1,27 @@
+// 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/ast/type/u32_type.h"
+
+namespace tint {
+namespace ast {
+namespace type {
+
+U32Type::U32Type() = default;
+
+U32Type::~U32Type() = default;
+
+}  // namespace type
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/type/u32_type.h b/src/ast/type/u32_type.h
new file mode 100644
index 0000000..b0d9440
--- /dev/null
+++ b/src/ast/type/u32_type.h
@@ -0,0 +1,46 @@
+// 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_AST_TYPE_U32_TYPE_H_
+#define SRC_AST_TYPE_U32_TYPE_H_
+
+#include <string>
+
+#include "src/ast/type/type.h"
+
+namespace tint {
+namespace ast {
+namespace type {
+
+/// A unsigned int 32 type.
+class U32Type : public Type {
+ public:
+  /// Constructor
+  U32Type();
+  /// Move constructor
+  U32Type(U32Type&&) = default;
+  ~U32Type() override;
+
+  /// @returns true if the type is a u32 type
+  bool IsU32() const override { return true; }
+
+  /// @returns the name for th type
+  std::string type_name() const override { return "__u32"; }
+};
+
+}  // namespace type
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_TYPE_U32_TYPE_H_
diff --git a/src/ast/type/u32_type_test.cc b/src/ast/type/u32_type_test.cc
new file mode 100644
index 0000000..5f4508e
--- /dev/null
+++ b/src/ast/type/u32_type_test.cc
@@ -0,0 +1,46 @@
+// 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/ast/type/u32_type.h"
+
+#include "gtest/gtest.h"
+
+namespace tint {
+namespace ast {
+namespace type {
+
+using U32TypeTest = testing::Test;
+
+TEST_F(U32TypeTest, Is) {
+  U32Type u;
+  EXPECT_FALSE(u.IsAlias());
+  EXPECT_FALSE(u.IsArray());
+  EXPECT_FALSE(u.IsBool());
+  EXPECT_FALSE(u.IsF32());
+  EXPECT_FALSE(u.IsI32());
+  EXPECT_FALSE(u.IsMatrix());
+  EXPECT_FALSE(u.IsPointer());
+  EXPECT_FALSE(u.IsStruct());
+  EXPECT_TRUE(u.IsU32());
+  EXPECT_FALSE(u.IsVector());
+}
+
+TEST_F(U32TypeTest, TypeName) {
+  U32Type u;
+  EXPECT_EQ(u.type_name(), "__u32");
+}
+
+}  // namespace type
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/type/vector_type.cc b/src/ast/type/vector_type.cc
new file mode 100644
index 0000000..8530214
--- /dev/null
+++ b/src/ast/type/vector_type.cc
@@ -0,0 +1,33 @@
+// 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/ast/type/vector_type.h"
+
+#include <assert.h>
+
+namespace tint {
+namespace ast {
+namespace type {
+
+VectorType::VectorType(Type* subtype, size_t size)
+    : subtype_(subtype), size_(size) {
+  assert(size_ > 1);
+  assert(size_ < 5);
+}
+
+VectorType::~VectorType() = default;
+
+}  // namespace type
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/type/vector_type.h b/src/ast/type/vector_type.h
new file mode 100644
index 0000000..a773275
--- /dev/null
+++ b/src/ast/type/vector_type.h
@@ -0,0 +1,59 @@
+// 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_AST_TYPE_VECTOR_TYPE_H_
+#define SRC_AST_TYPE_VECTOR_TYPE_H_
+
+#include <string>
+
+#include "src/ast/type/type.h"
+
+namespace tint {
+namespace ast {
+namespace type {
+
+/// A vector type.
+class VectorType : public Type {
+ public:
+  /// Constructor
+  /// @param subtype the vector element type
+  /// @param size the number of elements in the vector
+  VectorType(Type* subtype, size_t size);
+  /// Move constructor
+  VectorType(VectorType&&) = default;
+  ~VectorType() override;
+
+  /// @returns true if the type is a vector type
+  bool IsVector() const override { return true; }
+
+  /// @returns the type of the vector elements
+  Type* type() const { return subtype_; }
+  /// @returns the size of the vector
+  size_t size() const { return size_; }
+
+  /// @returns the name for th type
+  std::string type_name() const override {
+    return "__vec_" + std::to_string(size_) + subtype_->type_name();
+  }
+
+ private:
+  Type* subtype_ = nullptr;
+  size_t size_ = 2;
+};
+
+}  // namespace type
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_TYPE_VECTOR_TYPE_H_
diff --git a/src/ast/type/vector_type_test.cc b/src/ast/type/vector_type_test.cc
new file mode 100644
index 0000000..bc08383
--- /dev/null
+++ b/src/ast/type/vector_type_test.cc
@@ -0,0 +1,56 @@
+// 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/ast/type/vector_type.h"
+
+#include "gtest/gtest.h"
+#include "src/ast/type/i32_type.h"
+
+namespace tint {
+namespace ast {
+namespace type {
+
+using VectorTypeTest = testing::Test;
+
+TEST_F(VectorTypeTest, Creation) {
+  I32Type i32;
+  VectorType v{&i32, 2};
+  EXPECT_EQ(v.type(), &i32);
+  EXPECT_EQ(v.size(), 2);
+}
+
+TEST_F(VectorTypeTest, Is) {
+  I32Type i32;
+  VectorType v{&i32, 4};
+  EXPECT_FALSE(v.IsAlias());
+  EXPECT_FALSE(v.IsArray());
+  EXPECT_FALSE(v.IsBool());
+  EXPECT_FALSE(v.IsF32());
+  EXPECT_FALSE(v.IsI32());
+  EXPECT_FALSE(v.IsMatrix());
+  EXPECT_FALSE(v.IsPointer());
+  EXPECT_FALSE(v.IsStruct());
+  EXPECT_FALSE(v.IsU32());
+  EXPECT_TRUE(v.IsVector());
+}
+
+TEST_F(VectorTypeTest, TypeName) {
+  I32Type i32;
+  VectorType v{&i32, 3};
+  EXPECT_EQ(v.type_name(), "__vec_3__i32");
+}
+
+}  // namespace type
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/type/void_type.cc b/src/ast/type/void_type.cc
new file mode 100644
index 0000000..f41a9ba
--- /dev/null
+++ b/src/ast/type/void_type.cc
@@ -0,0 +1,27 @@
+// 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/ast/type/void_type.h"
+
+namespace tint {
+namespace ast {
+namespace type {
+
+VoidType::VoidType() = default;
+
+VoidType::~VoidType() = default;
+
+}  // namespace type
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/type/void_type.h b/src/ast/type/void_type.h
new file mode 100644
index 0000000..702a9b5
--- /dev/null
+++ b/src/ast/type/void_type.h
@@ -0,0 +1,46 @@
+// 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_AST_TYPE_VOID_TYPE_H_
+#define SRC_AST_TYPE_VOID_TYPE_H_
+
+#include <string>
+
+#include "src/ast/type/type.h"
+
+namespace tint {
+namespace ast {
+namespace type {
+
+/// A void type
+class VoidType : public Type {
+ public:
+  /// Constructor
+  VoidType();
+  /// Move constructor
+  VoidType(VoidType&&) = default;
+  ~VoidType() override;
+
+  /// @returns true if the type is a void type
+  bool IsVoid() const override { return true; }
+
+  /// @returns the name for this type
+  std::string type_name() const override { return "__void"; }
+};
+
+}  // namespace type
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_TYPE_VOID_TYPE_H_
diff --git a/src/ast/type_initializer_expression.cc b/src/ast/type_initializer_expression.cc
new file mode 100644
index 0000000..0b092f3
--- /dev/null
+++ b/src/ast/type_initializer_expression.cc
@@ -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.
+
+#include "src/ast/type_initializer_expression.h"
+
+namespace tint {
+namespace ast {
+
+TypeInitializerExpression::TypeInitializerExpression(
+    type::Type* type,
+    std::vector<std::unique_ptr<Expression>> values)
+    : InitializerExpression(), type_(type), values_(std::move(values)) {}
+
+TypeInitializerExpression::TypeInitializerExpression(
+    const Source& source,
+    type::Type* type,
+    std::vector<std::unique_ptr<Expression>> values)
+    : InitializerExpression(source), type_(type), values_(std::move(values)) {}
+
+TypeInitializerExpression::~TypeInitializerExpression() = default;
+
+bool TypeInitializerExpression::IsValid() const {
+  return values_.size() > 0;
+}
+
+void TypeInitializerExpression::to_str(std::ostream& out, size_t indent) const {
+  make_indent(out, indent);
+  out << "TypeInitializer{" << std::endl;
+  make_indent(out, indent + 2);
+  out << type_->type_name() << std::endl;
+
+  for (const auto& val : values_) {
+    val->to_str(out, indent + 2);
+  }
+  make_indent(out, indent);
+  out << "}" << std::endl;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/type_initializer_expression.h b/src/ast/type_initializer_expression.h
new file mode 100644
index 0000000..9b7af2f
--- /dev/null
+++ b/src/ast/type_initializer_expression.h
@@ -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.
+
+#ifndef SRC_AST_TYPE_INITIALIZER_EXPRESSION_H_
+#define SRC_AST_TYPE_INITIALIZER_EXPRESSION_H_
+
+#include <memory>
+#include <utility>
+#include <vector>
+
+#include "src/ast/initializer_expression.h"
+#include "src/ast/type/type.h"
+
+namespace tint {
+namespace ast {
+
+/// A type specific initializer
+class TypeInitializerExpression : public InitializerExpression {
+ public:
+  /// Constructor
+  /// @param type the type
+  /// @param values the values
+  explicit TypeInitializerExpression(
+      type::Type* type,
+      std::vector<std::unique_ptr<Expression>> values);
+  /// Constructor
+  /// @param source the initializer source
+  /// @param type the type
+  /// @param values the initializer values
+  TypeInitializerExpression(const Source& source,
+                            type::Type* type,
+                            std::vector<std::unique_ptr<Expression>> values);
+  /// Move constructor
+  TypeInitializerExpression(TypeInitializerExpression&&) = default;
+  ~TypeInitializerExpression() override;
+
+  /// @returns true if this is a type initializer
+  bool IsTypeInitializer() const override { return true; }
+
+  /// Set the type
+  /// @param type the type
+  void set_type(type::Type* type) { type_ = type; }
+  /// @returns the type
+  type::Type* type() const { return type_; }
+
+  /// Set the values
+  /// @param values the values
+  void set_values(std::vector<std::unique_ptr<Expression>> values) {
+    values_ = std::move(values);
+  }
+  /// @returns the values
+  const std::vector<std::unique_ptr<Expression>>& values() const {
+    return values_;
+  }
+
+  /// @returns true if the node is valid
+  bool IsValid() const override;
+
+  /// Writes a representation of the node to the output stream
+  /// @param out the stream to write to
+  /// @param indent number of spaces to indent the node when writing
+  void to_str(std::ostream& out, size_t indent) const override;
+
+ private:
+  TypeInitializerExpression(const TypeInitializerExpression&) = delete;
+
+  type::Type* type_;
+  std::vector<std::unique_ptr<Expression>> values_;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_TYPE_INITIALIZER_EXPRESSION_H_
diff --git a/src/ast/uint_literal.cc b/src/ast/uint_literal.cc
new file mode 100644
index 0000000..273f9ca
--- /dev/null
+++ b/src/ast/uint_literal.cc
@@ -0,0 +1,29 @@
+// 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/ast/uint_literal.h"
+
+namespace tint {
+namespace ast {
+
+UintLiteral::UintLiteral(uint32_t value) : value_(value) {}
+
+UintLiteral::~UintLiteral() = default;
+
+std::string UintLiteral::to_str() const {
+  return std::to_string(value_);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/uint_literal.h b/src/ast/uint_literal.h
new file mode 100644
index 0000000..0b2394e
--- /dev/null
+++ b/src/ast/uint_literal.h
@@ -0,0 +1,49 @@
+// 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_AST_UINT_LITERAL_H_
+#define SRC_AST_UINT_LITERAL_H_
+
+#include <string>
+
+#include "src/ast/literal.h"
+
+namespace tint {
+namespace ast {
+
+/// A uint literal
+class UintLiteral : public Literal {
+ public:
+  /// Constructor
+  /// @param value the uint literals value
+  explicit UintLiteral(uint32_t value);
+  ~UintLiteral() override;
+
+  /// @returns true if this is a uint literal
+  bool IsUint() const override { return true; }
+
+  /// @returns the uint literal value
+  uint32_t value() const { return value_; }
+
+  /// @returns the literal as a string
+  std::string to_str() const override;
+
+ private:
+  uint32_t value_;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_UINT_LITERAL_H_
diff --git a/src/ast/uint_literal_test.cc b/src/ast/uint_literal_test.cc
new file mode 100644
index 0000000..04628c3
--- /dev/null
+++ b/src/ast/uint_literal_test.cc
@@ -0,0 +1,45 @@
+// 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/ast/uint_literal.h"
+
+#include "gtest/gtest.h"
+
+namespace tint {
+namespace ast {
+
+using UintLiteralTest = testing::Test;
+
+TEST_F(UintLiteralTest, Value) {
+  UintLiteral u{47};
+  ASSERT_TRUE(u.IsUint());
+  EXPECT_EQ(u.value(), 47);
+}
+
+TEST_F(UintLiteralTest, Is) {
+  UintLiteral u{42};
+  EXPECT_FALSE(u.IsBool());
+  EXPECT_FALSE(u.IsInt());
+  EXPECT_FALSE(u.IsFloat());
+  EXPECT_TRUE(u.IsUint());
+}
+
+TEST_F(UintLiteralTest, ToStr) {
+  UintLiteral i{42};
+
+  EXPECT_EQ(i.to_str(), "42");
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/unary_derivative.cc b/src/ast/unary_derivative.cc
new file mode 100644
index 0000000..82ae23c
--- /dev/null
+++ b/src/ast/unary_derivative.cc
@@ -0,0 +1,39 @@
+// 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/ast/unary_derivative.h"
+
+namespace tint {
+namespace ast {
+
+std::ostream& operator<<(std::ostream& out, UnaryDerivative mod) {
+  switch (mod) {
+    case UnaryDerivative::kDpdx: {
+      out << "dpdx";
+      break;
+    }
+    case UnaryDerivative::kDpdy: {
+      out << "dpdy";
+      break;
+    }
+    case UnaryDerivative::kFwidth: {
+      out << "fwidth";
+      break;
+    }
+  }
+  return out;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/unary_derivative.h b/src/ast/unary_derivative.h
new file mode 100644
index 0000000..741834c
--- /dev/null
+++ b/src/ast/unary_derivative.h
@@ -0,0 +1,31 @@
+// 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_AST_UNARY_DERIVATIVE_H_
+#define SRC_AST_UNARY_DERIVATIVE_H_
+
+#include <ostream>
+
+namespace tint {
+namespace ast {
+
+/// The unary derivative
+enum class UnaryDerivative { kDpdx = 0, kDpdy, kFwidth };
+
+std::ostream& operator<<(std::ostream& out, UnaryDerivative mod);
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_UNARY_DERIVATIVE_H_
diff --git a/src/ast/unary_derivative_expression.cc b/src/ast/unary_derivative_expression.cc
new file mode 100644
index 0000000..1ae9a86
--- /dev/null
+++ b/src/ast/unary_derivative_expression.cc
@@ -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.
+
+#include "src/ast/unary_derivative_expression.h"
+
+namespace tint {
+namespace ast {
+
+UnaryDerivativeExpression::UnaryDerivativeExpression(
+    UnaryDerivative op,
+    DerivativeModifier mod,
+    std::unique_ptr<Expression> param)
+    : Expression(), op_(op), modifier_(mod), param_(std::move(param)) {}
+
+UnaryDerivativeExpression::UnaryDerivativeExpression(
+    const Source& source,
+    UnaryDerivative op,
+    DerivativeModifier mod,
+    std::unique_ptr<Expression> param)
+    : Expression(source), op_(op), modifier_(mod), param_(std::move(param)) {}
+
+UnaryDerivativeExpression::~UnaryDerivativeExpression() = default;
+
+bool UnaryDerivativeExpression::IsValid() const {
+  return true;
+}
+
+void UnaryDerivativeExpression::to_str(std::ostream& out, size_t indent) const {
+  make_indent(out, indent);
+  out << "UnaryDerivative{" << std::endl;
+  make_indent(out, indent + 2);
+  out << op_ << std::endl;
+  make_indent(out, indent + 2);
+  out << modifier_ << std::endl;
+  param_->to_str(out, indent);
+  make_indent(out, indent);
+  out << "}";
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/unary_derivative_expression.h b/src/ast/unary_derivative_expression.h
new file mode 100644
index 0000000..f601286
--- /dev/null
+++ b/src/ast/unary_derivative_expression.h
@@ -0,0 +1,94 @@
+// 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_AST_UNARY_DERIVATIVE_EXPRESSION_H_
+#define SRC_AST_UNARY_DERIVATIVE_EXPRESSION_H_
+
+#include <memory>
+#include <utility>
+
+#include "src/ast/derivative_modifier.h"
+#include "src/ast/expression.h"
+#include "src/ast/literal.h"
+#include "src/ast/unary_derivative.h"
+
+namespace tint {
+namespace ast {
+
+/// A unary derivative expression
+class UnaryDerivativeExpression : public Expression {
+ public:
+  /// Constructor
+  /// @param op the op
+  /// @param mod the derivative modifier
+  /// @param param the param
+  UnaryDerivativeExpression(UnaryDerivative op,
+                            DerivativeModifier mod,
+                            std::unique_ptr<Expression> param);
+  /// Constructor
+  /// @param source the initializer source
+  /// @param op the op
+  /// @param mod the derivative modifier
+  /// @param param the param
+  UnaryDerivativeExpression(const Source& source,
+                            UnaryDerivative op,
+                            DerivativeModifier mod,
+                            std::unique_ptr<Expression> param);
+  /// Move constructor
+  UnaryDerivativeExpression(UnaryDerivativeExpression&&) = default;
+  ~UnaryDerivativeExpression() override;
+
+  /// Sets the op
+  /// @param op the op
+  void set_op(UnaryDerivative op) { op_ = op; }
+  /// @returns the op
+  UnaryDerivative op() const { return op_; }
+
+  /// Sets the derivative modifier
+  /// @param mod the modifier
+  void set_modifier(DerivativeModifier mod) { modifier_ = mod; }
+  /// @returns the derivative modifier
+  DerivativeModifier modifier() const { return modifier_; }
+
+  /// Sets the param
+  /// @param param the param
+  void set_param(std::unique_ptr<Expression> param) {
+    param_ = std::move(param);
+  }
+  /// @returns the param
+  Expression* param() const { return param_.get(); }
+
+  /// @returns true if this is an as expression
+  bool IsUnaryDerivative() const override { return true; }
+
+  /// @returns true if the node is valid
+  bool IsValid() const override;
+
+  /// Writes a representation of the node to the output stream
+  /// @param out the stream to write to
+  /// @param indent number of spaces to indent the node when writing
+  void to_str(std::ostream& out, size_t indent) const override;
+
+ private:
+  UnaryDerivativeExpression(const UnaryDerivativeExpression&) = delete;
+
+  UnaryDerivative op_ = UnaryDerivative::kDpdx;
+  DerivativeModifier modifier_ = DerivativeModifier::kNone;
+  std::unique_ptr<Expression> param_;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_UNARY_DERIVATIVE_EXPRESSION_H_
diff --git a/src/ast/unary_method.cc b/src/ast/unary_method.cc
new file mode 100644
index 0000000..7ff4653
--- /dev/null
+++ b/src/ast/unary_method.cc
@@ -0,0 +1,59 @@
+// 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/ast/unary_method.h"
+
+namespace tint {
+namespace ast {
+
+std::ostream& operator<<(std::ostream& out, UnaryMethod mod) {
+  switch (mod) {
+    case UnaryMethod::kAny: {
+      out << "any";
+      break;
+    }
+    case UnaryMethod::kAll: {
+      out << "all";
+      break;
+    }
+    case UnaryMethod::kIsNan: {
+      out << "is_nan";
+      break;
+    }
+    case UnaryMethod::kIsInf: {
+      out << "is_inf";
+      break;
+    }
+    case UnaryMethod::kIsFinite: {
+      out << "is_finite";
+      break;
+    }
+    case UnaryMethod::kIsNormal: {
+      out << "is_normal";
+      break;
+    }
+    case UnaryMethod::kDot: {
+      out << "dot";
+      break;
+    }
+    case UnaryMethod::kOuterProduct: {
+      out << "outer_product";
+      break;
+    }
+  }
+  return out;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/unary_method.h b/src/ast/unary_method.h
new file mode 100644
index 0000000..3f719ef
--- /dev/null
+++ b/src/ast/unary_method.h
@@ -0,0 +1,40 @@
+// 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_AST_UNARY_METHOD_H_
+#define SRC_AST_UNARY_METHOD_H_
+
+#include <ostream>
+
+namespace tint {
+namespace ast {
+
+/// The unary methods
+enum class UnaryMethod {
+  kAny = 0,
+  kAll,
+  kIsNan,
+  kIsInf,
+  kIsFinite,
+  kIsNormal,
+  kDot,
+  kOuterProduct
+};
+
+std::ostream& operator<<(std::ostream& out, UnaryMethod mod);
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_UNARY_METHOD_H_
diff --git a/src/ast/unary_method_expression.cc b/src/ast/unary_method_expression.cc
new file mode 100644
index 0000000..058c683
--- /dev/null
+++ b/src/ast/unary_method_expression.cc
@@ -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.
+
+#include "src/ast/unary_method_expression.h"
+
+namespace tint {
+namespace ast {
+
+UnaryMethodExpression::UnaryMethodExpression(
+    UnaryMethod op,
+    std::vector<std::unique_ptr<Expression>> params)
+    : Expression(), op_(op), params_(std::move(params)) {}
+
+UnaryMethodExpression::UnaryMethodExpression(
+    const Source& source,
+    UnaryMethod op,
+    std::vector<std::unique_ptr<Expression>> params)
+    : Expression(source), op_(op), params_(std::move(params)) {}
+
+UnaryMethodExpression::~UnaryMethodExpression() = default;
+
+bool UnaryMethodExpression::IsValid() const {
+  return true;
+}
+
+void UnaryMethodExpression::to_str(std::ostream& out, size_t indent) const {
+  make_indent(out, indent);
+
+  out << "UnaryMethod{" << std::endl;
+  make_indent(out, indent + 2);
+  out << op_ << std::endl;
+  for (const auto& param : params_) {
+    param->to_str(out, indent + 2);
+    out << std::endl;
+  }
+  make_indent(out, indent);
+  out << "}";
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/unary_method_expression.h b/src/ast/unary_method_expression.h
new file mode 100644
index 0000000..806d190
--- /dev/null
+++ b/src/ast/unary_method_expression.h
@@ -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.
+
+#ifndef SRC_AST_UNARY_METHOD_EXPRESSION_H_
+#define SRC_AST_UNARY_METHOD_EXPRESSION_H_
+
+#include <memory>
+#include <utility>
+#include <vector>
+
+#include "src/ast/expression.h"
+#include "src/ast/literal.h"
+#include "src/ast/unary_method.h"
+
+namespace tint {
+namespace ast {
+
+/// A unary method expression
+class UnaryMethodExpression : public Expression {
+ public:
+  /// Constructor
+  /// @param op the op
+  /// @param params the params
+  UnaryMethodExpression(UnaryMethod op,
+                        std::vector<std::unique_ptr<Expression>> params);
+  /// Constructor
+  /// @param source the initializer source
+  /// @param op the op
+  /// @param params the params
+  UnaryMethodExpression(const Source& source,
+                        UnaryMethod op,
+                        std::vector<std::unique_ptr<Expression>> params);
+  /// Move constructor
+  UnaryMethodExpression(UnaryMethodExpression&&) = default;
+  ~UnaryMethodExpression() override;
+
+  /// Sets the op
+  /// @param op the op
+  void set_op(UnaryMethod op) { op_ = op; }
+  /// @returns the op
+  UnaryMethod op() const { return op_; }
+
+  /// Sets the params
+  /// @param params the parameters
+  void set_params(std::vector<std::unique_ptr<Expression>> params) {
+    params_ = std::move(params);
+  }
+  /// @returns the params
+  const std::vector<std::unique_ptr<Expression>>& params() const {
+    return params_;
+  }
+
+  /// @returns true if this is an as expression
+  bool IsUnaryMethod() const override { return true; }
+
+  /// @returns true if the node is valid
+  bool IsValid() const override;
+
+  /// Writes a representation of the node to the output stream
+  /// @param out the stream to write to
+  /// @param indent number of spaces to indent the node when writing
+  void to_str(std::ostream& out, size_t indent) const override;
+
+ private:
+  UnaryMethodExpression(const UnaryMethodExpression&) = delete;
+
+  UnaryMethod op_ = UnaryMethod::kAny;
+  std::vector<std::unique_ptr<Expression>> params_;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_UNARY_METHOD_EXPRESSION_H_
diff --git a/src/ast/unary_op.cc b/src/ast/unary_op.cc
new file mode 100644
index 0000000..5c8bba1
--- /dev/null
+++ b/src/ast/unary_op.cc
@@ -0,0 +1,35 @@
+// 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/ast/unary_op.h"
+
+namespace tint {
+namespace ast {
+
+std::ostream& operator<<(std::ostream& out, UnaryOp mod) {
+  switch (mod) {
+    case UnaryOp::kNegation: {
+      out << "negation";
+      break;
+    }
+    case UnaryOp::kNot: {
+      out << "not";
+      break;
+    }
+  }
+  return out;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/unary_op.h b/src/ast/unary_op.h
new file mode 100644
index 0000000..8a6b9f7
--- /dev/null
+++ b/src/ast/unary_op.h
@@ -0,0 +1,31 @@
+// 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_AST_UNARY_OP_H_
+#define SRC_AST_UNARY_OP_H_
+
+#include <ostream>
+
+namespace tint {
+namespace ast {
+
+/// The unary op
+enum class UnaryOp { kNegation = 0, kNot };
+
+std::ostream& operator<<(std::ostream& out, UnaryOp mod);
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_UNARY_OP_H_
diff --git a/src/ast/unary_op_expression.cc b/src/ast/unary_op_expression.cc
new file mode 100644
index 0000000..da31e8e
--- /dev/null
+++ b/src/ast/unary_op_expression.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 "src/ast/unary_op_expression.h"
+
+namespace tint {
+namespace ast {
+
+UnaryOpExpression::UnaryOpExpression(UnaryOp op,
+                                     std::unique_ptr<Expression> expr)
+    : Expression(), op_(op), expr_(std::move(expr)) {}
+
+UnaryOpExpression::UnaryOpExpression(const Source& source,
+                                     UnaryOp op,
+                                     std::unique_ptr<Expression> expr)
+    : Expression(source), op_(op), expr_(std::move(expr)) {}
+
+UnaryOpExpression::~UnaryOpExpression() = default;
+
+bool UnaryOpExpression::IsValid() const {
+  return expr_ != nullptr;
+}
+
+void UnaryOpExpression::to_str(std::ostream& out, size_t indent) const {
+  make_indent(out, indent);
+  out << "UnaryOp{" << op_ << std::endl;
+  expr_->to_str(out, indent + 2);
+  make_indent(out, indent);
+  out << "}" << std::endl;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/unary_op_expression.h b/src/ast/unary_op_expression.h
new file mode 100644
index 0000000..b77c185
--- /dev/null
+++ b/src/ast/unary_op_expression.h
@@ -0,0 +1,79 @@
+// 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_AST_UNARY_OP_EXPRESSION_H_
+#define SRC_AST_UNARY_OP_EXPRESSION_H_
+
+#include <memory>
+#include <utility>
+
+#include "src/ast/expression.h"
+#include "src/ast/literal.h"
+#include "src/ast/unary_op.h"
+
+namespace tint {
+namespace ast {
+
+/// A unary op expression
+class UnaryOpExpression : public Expression {
+ public:
+  /// Constructor
+  /// @param op the op
+  /// @param expr the expr
+  UnaryOpExpression(UnaryOp op, std::unique_ptr<Expression> expr);
+  /// Constructor
+  /// @param source the initializer source
+  /// @param op the op
+  /// @param expr the expr
+  UnaryOpExpression(const Source& source,
+                    UnaryOp op,
+                    std::unique_ptr<Expression> expr);
+  /// Move constructor
+  UnaryOpExpression(UnaryOpExpression&&) = default;
+  ~UnaryOpExpression() override;
+
+  /// Sets the op
+  /// @param op the op
+  void set_op(UnaryOp op) { op_ = op; }
+  /// @returns the op
+  UnaryOp op() const { return op_; }
+
+  /// Sets the expr
+  /// @param expr the expression
+  void set_expr(std::unique_ptr<Expression> expr) { expr_ = std::move(expr); }
+  /// @returns the expression
+  Expression* expr() const { return expr_.get(); }
+
+  /// @returns true if this is an as expression
+  bool IsUnaryOp() const override { return true; }
+
+  /// @returns true if the node is valid
+  bool IsValid() const override;
+
+  /// Writes a representation of the node to the output stream
+  /// @param out the stream to write to
+  /// @param indent number of spaces to indent the node when writing
+  void to_str(std::ostream& out, size_t indent) const override;
+
+ private:
+  UnaryOpExpression(const UnaryOpExpression&) = delete;
+
+  UnaryOp op_ = UnaryOp::kNegation;
+  std::unique_ptr<Expression> expr_;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_UNARY_OP_EXPRESSION_H_
diff --git a/src/ast/unless_statement.cc b/src/ast/unless_statement.cc
new file mode 100644
index 0000000..7f591b0
--- /dev/null
+++ b/src/ast/unless_statement.cc
@@ -0,0 +1,56 @@
+// 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/ast/unless_statement.h"
+
+namespace tint {
+namespace ast {
+
+UnlessStatement::UnlessStatement(std::unique_ptr<Expression> condition,
+                                 std::vector<std::unique_ptr<Statement>> body)
+    : Statement(), condition_(std::move(condition)), body_(std::move(body)) {}
+
+UnlessStatement::UnlessStatement(const Source& source,
+                                 std::unique_ptr<Expression> condition,
+                                 std::vector<std::unique_ptr<Statement>> body)
+    : Statement(source),
+      condition_(std::move(condition)),
+      body_(std::move(body)) {}
+
+UnlessStatement::~UnlessStatement() = default;
+
+bool UnlessStatement::IsValid() const {
+  return condition_ != nullptr;
+}
+
+void UnlessStatement::to_str(std::ostream& out, size_t indent) const {
+  make_indent(out, indent);
+  out << "Unless{" << std::endl;
+
+  condition_->to_str(out, indent + 2);
+  make_indent(out, indent);
+  out << "{" << std::endl;
+
+  for (const auto& stmt : body_)
+    stmt->to_str(out, indent + 4);
+
+  make_indent(out, indent + 2);
+  out << "}";
+
+  make_indent(out, indent);
+  out << "}" << std::endl;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/unless_statement.h b/src/ast/unless_statement.h
new file mode 100644
index 0000000..bafbb43
--- /dev/null
+++ b/src/ast/unless_statement.h
@@ -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.
+
+#ifndef SRC_AST_UNLESS_STATEMENT_H_
+#define SRC_AST_UNLESS_STATEMENT_H_
+
+#include <memory>
+#include <utility>
+#include <vector>
+
+#include "src/ast/expression.h"
+#include "src/ast/statement.h"
+
+namespace tint {
+namespace ast {
+
+/// A unless statement
+class UnlessStatement : public Statement {
+ public:
+  /// Constructor
+  /// @param condition the condition expression
+  /// @param body the body statements
+  UnlessStatement(std::unique_ptr<Expression> condition,
+                  std::vector<std::unique_ptr<Statement>> body);
+  /// Constructor
+  /// @param source the initializer source
+  /// @param condition the condition expression
+  /// @param body the body statements
+  UnlessStatement(const Source& source,
+                  std::unique_ptr<Expression> condition,
+                  std::vector<std::unique_ptr<Statement>> body);
+  /// Move constructor
+  UnlessStatement(UnlessStatement&&) = default;
+  ~UnlessStatement() override;
+
+  /// Sets the condition expression
+  /// @param condition the condition expression
+  void set_condition(std::unique_ptr<Expression> condition) {
+    condition_ = std::move(condition);
+  }
+  /// @returns the condition statements
+  Expression* condition() const { return condition_.get(); }
+
+  /// Sets the body statements
+  /// @param body the body statements
+  void set_body(std::vector<std::unique_ptr<Statement>> body) {
+    body_ = std::move(body);
+  }
+  /// @returns the body statements
+  const std::vector<std::unique_ptr<Statement>>& body() const { return body_; }
+
+  /// @returns true if this is an unless statement
+  bool IsUnless() const override { return true; }
+
+  /// @returns true if the node is valid
+  bool IsValid() const override;
+
+  /// Writes a representation of the node to the output stream
+  /// @param out the stream to write to
+  /// @param indent number of spaces to indent the node when writing
+  void to_str(std::ostream& out, size_t indent) const override;
+
+ private:
+  UnlessStatement(const UnlessStatement&) = delete;
+
+  std::unique_ptr<Expression> condition_;
+  std::vector<std::unique_ptr<Statement>> body_;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_UNLESS_STATEMENT_H_
diff --git a/src/ast/variable.cc b/src/ast/variable.cc
new file mode 100644
index 0000000..1caa64b
--- /dev/null
+++ b/src/ast/variable.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 "src/ast/variable.h"
+
+#include <assert.h>
+
+#include "src/ast/decorated_variable.h"
+
+namespace tint {
+namespace ast {
+
+Variable::Variable(const std::string& name, StorageClass sc, type::Type* type)
+    : Node(), name_(name), storage_class_(sc), type_(type) {}
+
+Variable::Variable(const Source& source,
+                   const std::string& name,
+                   StorageClass sc,
+                   type::Type* type)
+    : Node(source), name_(name), storage_class_(sc), type_(type) {}
+
+Variable::~Variable() = default;
+
+DecoratedVariable* Variable::AsDecorated() {
+  assert(IsDecorated());
+  return static_cast<DecoratedVariable*>(this);
+}
+
+bool Variable::IsValid() const {
+  if (name_.length() == 0) {
+    return false;
+  }
+  if (type_ == nullptr) {
+    return false;
+  }
+  return true;
+}
+
+void Variable::info_to_str(std::ostream& out, size_t indent) const {
+  make_indent(out, indent);
+  out << name_ << std::endl;
+  make_indent(out, indent);
+  out << storage_class_ << std::endl;
+  make_indent(out, indent);
+  out << type_->type_name() << std::endl;
+}
+
+void Variable::to_str(std::ostream& out, size_t indent) const {
+  info_to_str(out, indent);
+
+  if (initializer_ != nullptr) {
+    make_indent(out, indent);
+    out << "{" << std::endl;
+
+    initializer_->to_str(out, indent + 2);
+
+    make_indent(out, indent);
+    out << "}" << std::endl;
+  }
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/variable.h b/src/ast/variable.h
new file mode 100644
index 0000000..db27ed2
--- /dev/null
+++ b/src/ast/variable.h
@@ -0,0 +1,122 @@
+// 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_AST_VARIABLE_H_
+#define SRC_AST_VARIABLE_H_
+
+#include <memory>
+#include <ostream>
+#include <string>
+#include <utility>
+
+#include "src/ast/expression.h"
+#include "src/ast/node.h"
+#include "src/ast/storage_class.h"
+#include "src/ast/type/type.h"
+
+namespace tint {
+namespace ast {
+
+class DecoratedVariable;
+
+/// A Variable statement.
+class Variable : public Node {
+ public:
+  /// Create a new empty variable statement
+  Variable() = default;
+  /// Create a variable
+  /// @param name the variables name
+  /// @param sc the variable storage class
+  /// @param type the variables type
+  Variable(const std::string& name, StorageClass sc, type::Type* type);
+  /// Create a variable
+  /// @param source the variable source
+  /// @param name the variables name
+  /// @param sc the variable storage class
+  /// @param type the variables type
+  Variable(const Source& source,
+           const std::string& name,
+           StorageClass sc,
+           type::Type* type);
+  /// Move constructor
+  Variable(Variable&&) = default;
+
+  ~Variable() override;
+
+  /// Sets the variable name
+  /// @param name the name to set
+  void set_name(const std::string& name) { name_ = name; }
+  /// @returns the variable name
+  const std::string& name() { return name_; }
+
+  /// Sets the type of the variable
+  /// @param type the type
+  void set_type(type::Type* type) { type_ = type; }
+  /// @returns the variables type.
+  type::Type* type() const { return type_; }
+
+  /// Sets the storage class
+  /// @param sc the storage class
+  void set_storage_class(StorageClass sc) { storage_class_ = sc; }
+  /// @returns the storage class
+  StorageClass storage_class() const { return storage_class_; }
+
+  /// Sets the initializer
+  /// @param expr the initializer expression
+  void set_initializer(std::unique_ptr<Expression> expr) {
+    initializer_ = std::move(expr);
+  }
+  /// @returns the initializer expression or nullptr if none set
+  Expression* initializer() const { return initializer_.get(); }
+
+  /// Sets if the variable is constant
+  /// @param val the value to be set
+  void set_is_const(bool val) { is_const_ = val; }
+  /// @returns true if this is a constant, false otherwise
+  bool is_const() const { return is_const_; }
+
+  /// @returns true if this is a decorated variable
+  virtual bool IsDecorated() const { return false; }
+
+  /// @returns the expression as a decorated variable
+  DecoratedVariable* AsDecorated();
+
+  /// @returns true if the name and path are both present
+  bool IsValid() const override;
+
+  /// Writes a representation of the node to the output stream
+  /// @param out the stream to write to
+  /// @param indent number of spaces to indent the node when writing
+  void to_str(std::ostream& out, size_t indent) const override;
+
+ protected:
+  /// Output information for this variable.
+  /// @param out the stream to write to
+  /// @param indent number of spaces to indent the node when writing
+  void info_to_str(std::ostream& out, size_t indent) const;
+
+ private:
+  Variable(const Variable&) = delete;
+
+  bool is_const_ = false;
+  std::string name_;
+  StorageClass storage_class_ = StorageClass::kNone;
+  type::Type* type_ = nullptr;
+  std::unique_ptr<Expression> initializer_;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_VARIABLE_H_
diff --git a/src/ast/variable_decoration.cc b/src/ast/variable_decoration.cc
new file mode 100644
index 0000000..2f70268
--- /dev/null
+++ b/src/ast/variable_decoration.cc
@@ -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.
+
+#include "src/ast/variable_decoration.h"
+
+#include <assert.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"
+
+namespace tint {
+namespace ast {
+
+VariableDecoration::VariableDecoration() = default;
+
+VariableDecoration::~VariableDecoration() = default;
+
+BindingDecoration* VariableDecoration::AsBinding() {
+  assert(IsBinding());
+  return static_cast<BindingDecoration*>(this);
+}
+
+BuiltinDecoration* VariableDecoration::AsBuiltin() {
+  assert(IsBuiltin());
+  return static_cast<BuiltinDecoration*>(this);
+}
+
+LocationDecoration* VariableDecoration::AsLocation() {
+  assert(IsLocation());
+  return static_cast<LocationDecoration*>(this);
+}
+
+SetDecoration* VariableDecoration::AsSet() {
+  assert(IsSet());
+  return static_cast<SetDecoration*>(this);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/variable_decoration.h b/src/ast/variable_decoration.h
new file mode 100644
index 0000000..2e7bd04
--- /dev/null
+++ b/src/ast/variable_decoration.h
@@ -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.
+
+#ifndef SRC_AST_VARIABLE_DECORATION_H_
+#define SRC_AST_VARIABLE_DECORATION_H_
+
+#include <ostream>
+#include <string>
+
+namespace tint {
+namespace ast {
+
+class BindingDecoration;
+class BuiltinDecoration;
+class LocationDecoration;
+class SetDecoration;
+
+/// A decoration attached to a variable
+class VariableDecoration {
+ public:
+  virtual ~VariableDecoration();
+
+  /// @returns true if this is a binding decoration
+  virtual bool IsBinding() const { return false; }
+  /// @returns true if this is a builtin decoration
+  virtual bool IsBuiltin() const { return false; }
+  /// @returns true if this is a location decoration
+  virtual bool IsLocation() const { return false; }
+  /// @returns true if this is a set decoration
+  virtual bool IsSet() const { return false; }
+
+  /// @returns the decoration as a binding decoration
+  BindingDecoration* AsBinding();
+  /// @returns the decoration as a builtin decoration
+  BuiltinDecoration* AsBuiltin();
+  /// @returns the decoration as a location decoration
+  LocationDecoration* AsLocation();
+  /// @returns the decoration as a set decoration
+  SetDecoration* AsSet();
+
+  /// Outputs the variable decoration to the given stream
+  /// @param out the stream to output too
+  virtual void to_str(std::ostream& out) const = 0;
+
+ protected:
+  VariableDecoration();
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_VARIABLE_DECORATION_H_
diff --git a/src/ast/variable_statement.cc b/src/ast/variable_statement.cc
new file mode 100644
index 0000000..a195575
--- /dev/null
+++ b/src/ast/variable_statement.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 "src/ast/variable_statement.h"
+
+namespace tint {
+namespace ast {
+
+VariableStatement::VariableStatement(std::unique_ptr<Variable> variable)
+    : Statement(), variable_(std::move(variable)) {}
+
+VariableStatement::VariableStatement(const Source& source,
+                                     std::unique_ptr<Variable> variable)
+    : Statement(source), variable_(std::move(variable)) {}
+
+VariableStatement::~VariableStatement() = default;
+
+bool VariableStatement::IsValid() const {
+  return variable_ != nullptr;
+}
+
+void VariableStatement::to_str(std::ostream& out, size_t indent) const {
+  make_indent(out, indent);
+  out << "Variable{" << std::endl;
+  variable_->to_str(out, indent + 2);
+  make_indent(out, indent);
+  out << "}" << std::endl;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/ast/variable_statement.h b/src/ast/variable_statement.h
new file mode 100644
index 0000000..17e0742
--- /dev/null
+++ b/src/ast/variable_statement.h
@@ -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.
+
+#ifndef SRC_AST_VARIABLE_STATEMENT_H_
+#define SRC_AST_VARIABLE_STATEMENT_H_
+
+#include <memory>
+#include <utility>
+
+#include "src/ast/expression.h"
+#include "src/ast/statement.h"
+#include "src/ast/variable.h"
+
+namespace tint {
+namespace ast {
+
+/// A variable statement
+class VariableStatement : public Statement {
+ public:
+  /// Constructor
+  /// @param variable the variable
+  explicit VariableStatement(std::unique_ptr<Variable> variable);
+  /// Constructor
+  /// @param source the initializer source
+  /// @param variable the variable
+  VariableStatement(const Source& source, std::unique_ptr<Variable> variable);
+  /// Move constructor
+  VariableStatement(VariableStatement&&) = default;
+  ~VariableStatement() override;
+
+  /// Sets the variable
+  /// @param variable the variable to set
+  void set_variable(std::unique_ptr<Variable> variable) {
+    variable_ = std::move(variable);
+  }
+  /// @returns the variable
+  Variable* variable() const { return variable_.get(); }
+
+  /// @returns true if this is an variable statement
+  bool IsVariable() const override { return true; }
+
+  /// @returns true if the node is valid
+  bool IsValid() const override;
+
+  /// Writes a representation of the node to the output stream
+  /// @param out the stream to write to
+  /// @param indent number of spaces to indent the node when writing
+  void to_str(std::ostream& out, size_t indent) const override;
+
+ private:
+  VariableStatement(const VariableStatement&) = delete;
+
+  std::unique_ptr<Variable> variable_;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_AST_VARIABLE_STATEMENT_H_
diff --git a/src/ast/variable_test.cc b/src/ast/variable_test.cc
new file mode 100644
index 0000000..0f4f59b
--- /dev/null
+++ b/src/ast/variable_test.cc
@@ -0,0 +1,100 @@
+// 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/ast/variable.h"
+
+#include "gtest/gtest.h"
+#include "src/ast/type/f32_type.h"
+#include "src/ast/type/i32_type.h"
+
+namespace tint {
+namespace ast {
+
+using VariableTest = testing::Test;
+
+TEST_F(VariableTest, Creation) {
+  type::I32Type t;
+  Variable v("my_var", StorageClass::kFunction, &t);
+
+  EXPECT_EQ(v.name(), "my_var");
+  EXPECT_EQ(v.storage_class(), StorageClass::kFunction);
+  EXPECT_EQ(v.type(), &t);
+  EXPECT_EQ(v.line(), 0);
+  EXPECT_EQ(v.column(), 0);
+}
+
+TEST_F(VariableTest, CreationWithSource) {
+  Source s{27, 4};
+  type::F32Type t;
+  Variable v(s, "i", StorageClass::kPrivate, &t);
+
+  EXPECT_EQ(v.name(), "i");
+  EXPECT_EQ(v.storage_class(), StorageClass::kPrivate);
+  EXPECT_EQ(v.type(), &t);
+  EXPECT_EQ(v.line(), 27);
+  EXPECT_EQ(v.column(), 4);
+}
+
+TEST_F(VariableTest, CreationEmpty) {
+  Source s{27, 4};
+  Variable v;
+  v.set_source(s);
+  v.set_storage_class(StorageClass::kWorkgroup);
+  v.set_name("a_var");
+
+  type::I32Type t;
+  v.set_type(&t);
+
+  EXPECT_EQ(v.name(), "a_var");
+  EXPECT_EQ(v.storage_class(), StorageClass::kWorkgroup);
+  EXPECT_EQ(v.type(), &t);
+  EXPECT_EQ(v.line(), 27);
+  EXPECT_EQ(v.column(), 4);
+}
+
+TEST_F(VariableTest, IsValid) {
+  type::I32Type t;
+  Variable v{"my_var", StorageClass::kNone, &t};
+  EXPECT_TRUE(v.IsValid());
+}
+
+TEST_F(VariableTest, IsValid_MissinName) {
+  type::I32Type t;
+  Variable v{"", StorageClass::kNone, &t};
+  EXPECT_FALSE(v.IsValid());
+}
+
+TEST_F(VariableTest, IsValid_MissingType) {
+  Variable v{"x", StorageClass::kNone, nullptr};
+  EXPECT_FALSE(v.IsValid());
+}
+
+TEST_F(VariableTest, IsValid_MissingBoth) {
+  Variable v;
+  EXPECT_FALSE(v.IsValid());
+}
+
+TEST_F(VariableTest, to_str) {
+  type::F32Type t;
+  Variable v{"my_var", StorageClass::kFunction, &t};
+  std::ostringstream out;
+  v.to_str(out, 0);
+  EXPECT_EQ(out.str(), R"(my_var
+function
+__f32
+)");
+}
+
+}  // namespace ast
+}  // namespace tint
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..ea2f05bc
--- /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
diff --git a/src/source.h b/src/source.h
new file mode 100644
index 0000000..0305350
--- /dev/null
+++ b/src/source.h
@@ -0,0 +1,33 @@
+
+// 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_SOURCE_H_
+#define SRC_SOURCE_H_
+
+#include <stddef.h>
+
+namespace tint {
+
+/// Represents a line and column position
+struct Source {
+  /// The line the token appeared on
+  size_t line = 0;
+  /// The column the token appeared in
+  size_t column = 0;
+};
+
+}  // namespace tint
+
+#endif  // SRC_SOURCE_H_
diff --git a/src/type_determiner.cc b/src/type_determiner.cc
new file mode 100644
index 0000000..0faf0af
--- /dev/null
+++ b/src/type_determiner.cc
@@ -0,0 +1,27 @@
+// 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/type_determiner.h"
+
+namespace tint {
+
+TypeDeterminer::TypeDeterminer() = default;
+
+TypeDeterminer::~TypeDeterminer() = default;
+
+bool TypeDeterminer::Determine(ast::Module*) {
+  return true;
+}
+
+}  // namespace tint
diff --git a/src/type_determiner.h b/src/type_determiner.h
new file mode 100644
index 0000000..8f2062e
--- /dev/null
+++ b/src/type_determiner.h
@@ -0,0 +1,45 @@
+// 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_TYPE_DETERMINER_H_
+#define SRC_TYPE_DETERMINER_H_
+
+#include <string>
+
+#include "src/ast/module.h"
+
+namespace tint {
+
+/// Determines types for all items in the given tint module
+class TypeDeterminer {
+ public:
+  /// Constructor
+  TypeDeterminer();
+  ~TypeDeterminer();
+
+  /// Runs the type determiner
+  /// @param module the module to update with typing information
+  /// @returns true if the type determiner was successful
+  bool Determine(ast::Module* module);
+
+  /// @returns error messages from the type determiner
+  const std::string& error() { return error_; }
+
+ private:
+  std::string error_;
+};
+
+}  // namespace tint
+
+#endif  // SRC_TYPE_DETERMINER_H_
diff --git a/src/type_manager.cc b/src/type_manager.cc
new file mode 100644
index 0000000..5380f5f
--- /dev/null
+++ b/src/type_manager.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 "src/type_manager.h"
+
+#include <utility>
+
+namespace tint {
+namespace {
+
+TypeManager* manager_ = nullptr;
+
+}  // namespace
+
+// static
+TypeManager* TypeManager::Instance() {
+  if (!manager_) {
+    manager_ = new TypeManager();
+  }
+  return manager_;
+}
+
+// static
+void TypeManager::Destroy() {
+  delete manager_;
+  manager_ = nullptr;
+}
+
+TypeManager::TypeManager() = default;
+
+TypeManager::~TypeManager() = default;
+
+ast::type::Type* TypeManager::Get(std::unique_ptr<ast::type::Type> type) {
+  auto name = type->type_name();
+
+  if (types_.find(name) == types_.end()) {
+    types_[name] = std::move(type);
+  }
+  return types_.find(name)->second.get();
+}
+
+}  // namespace tint
diff --git a/src/type_manager.h b/src/type_manager.h
new file mode 100644
index 0000000..573542a
--- /dev/null
+++ b/src/type_manager.h
@@ -0,0 +1,49 @@
+// 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_TYPE_MANAGER_H_
+#define SRC_TYPE_MANAGER_H_
+
+#include <memory>
+#include <string>
+#include <unordered_map>
+
+#include "src/ast/type/type.h"
+
+namespace tint {
+
+/// The type manager holds all the pointers to the known types.
+class TypeManager {
+ public:
+  /// @returns a pointer to the type manager
+  static TypeManager* Instance();
+  /// Frees the type manager and any associated types. The types should not be
+  /// used after the manager is freed.
+  static void Destroy();
+
+  /// Get the given type from the type manager
+  /// @param type The type to register
+  /// @return the pointer to the registered type
+  ast::type::Type* Get(std::unique_ptr<ast::type::Type> type);
+
+ private:
+  TypeManager();
+  ~TypeManager();
+
+  std::unordered_map<std::string, std::unique_ptr<ast::type::Type>> types_;
+};
+
+}  // namespace tint
+
+#endif  // SRC_TYPE_MANAGER_H_
diff --git a/src/type_manager_test.cc b/src/type_manager_test.cc
new file mode 100644
index 0000000..821e559
--- /dev/null
+++ b/src/type_manager_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 "src/type_manager.h"
+
+#include "gtest/gtest.h"
+#include "src/ast/type/i32_type.h"
+#include "src/ast/type/u32_type.h"
+
+namespace tint {
+
+using TypeManagerTest = testing::Test;
+
+TEST_F(TypeManagerTest, Singleton) {
+  auto tm = TypeManager::Instance();
+  ASSERT_NE(tm, nullptr);
+  ASSERT_EQ(tm, TypeManager::Instance());
+
+  TypeManager::Destroy();
+}
+
+TEST_F(TypeManagerTest, Destroy) {
+  auto tm = TypeManager::Instance();
+  ASSERT_NE(tm, nullptr);
+  ASSERT_EQ(tm, TypeManager::Instance());
+
+  TypeManager::Destroy();
+
+  tm = TypeManager::Instance();
+  ASSERT_NE(tm, nullptr);
+
+  TypeManager::Destroy();
+}
+
+TEST_F(TypeManagerTest, GetUnregistered) {
+  auto tm = TypeManager::Instance();
+  auto t = tm->Get(std::make_unique<ast::type::I32Type>());
+  ASSERT_NE(t, nullptr);
+  EXPECT_TRUE(t->IsI32());
+
+  TypeManager::Destroy();
+}
+
+TEST_F(TypeManagerTest, GetSameTypeReturnsSamePtr) {
+  auto tm = TypeManager::Instance();
+  auto t = tm->Get(std::make_unique<ast::type::I32Type>());
+  ASSERT_NE(t, nullptr);
+  EXPECT_TRUE(t->IsI32());
+
+  auto t2 = tm->Get(std::make_unique<ast::type::I32Type>());
+  EXPECT_EQ(t, t2);
+
+  TypeManager::Destroy();
+}
+
+TEST_F(TypeManagerTest, GetDifferentTypeReturnsDifferentPtr) {
+  auto tm = TypeManager::Instance();
+  auto t = tm->Get(std::make_unique<ast::type::I32Type>());
+  ASSERT_NE(t, nullptr);
+  EXPECT_TRUE(t->IsI32());
+
+  auto t2 = tm->Get(std::make_unique<ast::type::U32Type>());
+  ASSERT_NE(t2, nullptr);
+  EXPECT_NE(t, t2);
+  EXPECT_TRUE(t2->IsU32());
+
+  TypeManager::Destroy();
+}
+
+}  // namespace tint
diff --git a/src/validator.cc b/src/validator.cc
new file mode 100644
index 0000000..1ee1c4c
--- /dev/null
+++ b/src/validator.cc
@@ -0,0 +1,27 @@
+// 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/validator.h"
+
+namespace tint {
+
+Validator::Validator() = default;
+
+Validator::~Validator() = default;
+
+bool Validator::Validate(const ast::Module&) {
+  return true;
+}
+
+}  // namespace tint
diff --git a/src/validator.h b/src/validator.h
new file mode 100644
index 0000000..32c64b7
--- /dev/null
+++ b/src/validator.h
@@ -0,0 +1,45 @@
+// 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_VALIDATOR_H_
+#define SRC_VALIDATOR_H_
+
+#include <string>
+
+#include "src/ast/module.h"
+
+namespace tint {
+
+/// Determines if the module is complete and valid
+class Validator {
+ public:
+  /// Constructor
+  Validator();
+  ~Validator();
+
+  /// Runs the validator
+  /// @param module the module to validate
+  /// @returns true if the validation was successful
+  bool Validate(const ast::Module& module);
+
+  /// @returns error messages from the validator
+  const std::string& error() { return error_; }
+
+ private:
+  std::string error_;
+};
+
+}  // namespace tint
+
+#endif  // SRC_VALIDATOR_H_
diff --git a/src/writer/spv/generator.cc b/src/writer/spv/generator.cc
new file mode 100644
index 0000000..8d15522
--- /dev/null
+++ b/src/writer/spv/generator.cc
@@ -0,0 +1,33 @@
+// 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/writer/spv/generator.h"
+
+#include <utility>
+
+namespace tint {
+namespace writer {
+namespace spv {
+
+Generator::Generator(ast::Module module) : writer::Writer(std::move(module)) {}
+
+Generator::~Generator() = default;
+
+bool Generator::Generate() {
+  return true;
+}
+
+}  // namespace spv
+}  // namespace writer
+}  // namespace tint
diff --git a/src/writer/spv/generator.h b/src/writer/spv/generator.h
new file mode 100644
index 0000000..e677019
--- /dev/null
+++ b/src/writer/spv/generator.h
@@ -0,0 +1,49 @@
+// 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_WRITER_SPV_GENERATOR_H_
+#define SRC_WRITER_SPV_GENERATOR_H_
+
+#include <vector>
+
+#include "src/writer/writer.h"
+
+namespace tint {
+namespace writer {
+namespace spv {
+
+/// Class to generate SPIR-V from a Tint module
+class Generator : public writer::Writer {
+ public:
+  /// Constructor
+  /// @param module the module to convert
+  explicit Generator(ast::Module module);
+  ~Generator() override;
+
+  /// Generates the result data
+  /// @returns true on successful generation; false otherwise
+  bool Generate() override;
+
+  /// @returns the result data
+  const std::vector<uint32_t>& result() const { return result_; }
+
+ private:
+  std::vector<uint32_t> result_;
+};
+
+}  // namespace spv
+}  // namespace writer
+}  // namespace tint
+
+#endif  // SRC_WRITER_SPV_GENERATOR_H_
diff --git a/src/writer/wgsl/generator.cc b/src/writer/wgsl/generator.cc
new file mode 100644
index 0000000..015e99c
--- /dev/null
+++ b/src/writer/wgsl/generator.cc
@@ -0,0 +1,33 @@
+// 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/writer/wgsl/generator.h"
+
+#include <utility>
+
+namespace tint {
+namespace writer {
+namespace wgsl {
+
+Generator::Generator(ast::Module module) : writer::Writer(std::move(module)) {}
+
+Generator::~Generator() = default;
+
+bool Generator::Generate() {
+  return true;
+}
+
+}  // namespace wgsl
+}  // namespace writer
+}  // namespace tint
diff --git a/src/writer/wgsl/generator.h b/src/writer/wgsl/generator.h
new file mode 100644
index 0000000..b0a00e2
--- /dev/null
+++ b/src/writer/wgsl/generator.h
@@ -0,0 +1,49 @@
+// 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_WRITER_WGSL_GENERATOR_H_
+#define SRC_WRITER_WGSL_GENERATOR_H_
+
+#include <string>
+
+#include "src/writer/writer.h"
+
+namespace tint {
+namespace writer {
+namespace wgsl {
+
+/// Class to generate WGSL source from a WGSL module
+class Generator : public writer::Writer {
+ public:
+  /// Constructor
+  /// @param module the module to convert
+  explicit Generator(ast::Module module);
+  ~Generator() override;
+
+  /// Generates the result data
+  /// @returns true on successful generation; false otherwise
+  bool Generate() override;
+
+  /// @returns the result data
+  const std::string& result() const { return result_; }
+
+ private:
+  std::string result_;
+};
+
+}  // namespace wgsl
+}  // namespace writer
+}  // namespace tint
+
+#endif  // SRC_WRITER_WGSL_GENERATOR_H_
diff --git a/src/writer/writer.cc b/src/writer/writer.cc
new file mode 100644
index 0000000..545ed3b
--- /dev/null
+++ b/src/writer/writer.cc
@@ -0,0 +1,27 @@
+// 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/writer/writer.h"
+
+#include <utility>
+
+namespace tint {
+namespace writer {
+
+Writer::Writer(ast::Module module) : module_(std::move(module)) {}
+
+Writer::~Writer() = default;
+
+}  // namespace writer
+}  // namespace tint
diff --git a/src/writer/writer.h b/src/writer/writer.h
new file mode 100644
index 0000000..b4a7840
--- /dev/null
+++ b/src/writer/writer.h
@@ -0,0 +1,55 @@
+// 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_WRITER_WRITER_H_
+#define SRC_WRITER_WRITER_H_
+
+#include <string>
+
+#include "src/ast/module.h"
+
+namespace tint {
+namespace writer {
+
+/// Base class for the output writers
+class Writer {
+ public:
+  virtual ~Writer();
+
+  /// @returns the writer error string
+  const std::string& error() const { return error_; }
+
+  /// Converts the module into the desired format
+  /// @returns true on success; false on failure
+  virtual bool Generate() = 0;
+
+ protected:
+  /// Constructor
+  /// @param module the tint module to convert
+  explicit Writer(ast::Module module);
+
+  /// 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_;
+  /// The module being converted
+  ast::Module module_;
+};
+
+}  // namespace writer
+}  // namespace tint
+
+#endif  // SRC_WRITER_WRITER_H_
diff --git a/test/compute_boids.wgsl b/test/compute_boids.wgsl
new file mode 100644
index 0000000..6df8a30
--- /dev/null
+++ b/test/compute_boids.wgsl
@@ -0,0 +1,152 @@
+# 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.
+
+import "GLSL.std.450" as std;
+
+# vertex shader
+
+[[location 0]] var<in> a_particlePos : vec2<f32>;
+[[location 1]] var<in> a_particleVel : vec2<f32>;
+[[location 2]] var<in> a_pos : vec2<f32>;
+[[builtin position]] var gl_Position : vec4<f32>;
+
+fn vtx_main() -> void {
+  var angle : f32 = -std::atan2(a_particleVel.x, a_particleVel.y);
+  var pos : vec2<f32> = vec2<f32>(
+      (a_pos.x * std::cos(angle)) - (a_pos.y * std::sin(angle)),
+      (a_pos.x * std::sin(angle)) + (a_pos.y * std::cos(angle)));
+  gl_Position = vec4<f32>(pos + a_particlePos, 0, 1);
+  return;
+}
+entry_point vertex as "main" = vtx_main;
+
+# fragment shader
+[[location 0]] var<out> fragColor : vec4<f32>;
+
+fn frag_main() -> void {
+  fragColor = vec4<f32>(1.0, 1.0, 1.0, 1.0);
+  return;
+}
+entry_point fragment as "main" = frag_main;
+
+# compute shader
+type Particle = struct {
+  [[offset 0]] pos : vec2<f32>;
+  [[offset 8]] vel : vec2<f32>;
+};
+
+type SimParams = struct {
+  [[offset 0]] deltaT : f32;
+  [[offset 4]] rule1Distance : f32;
+  [[offset 8]] rule2Distance : f32;
+  [[offset 12]] rule3Distance : f32;
+  [[offset 16]] rule1Scale : f32;
+  [[offset 20]] rule2Scale : f32;
+  [[offset 24]] rule3Scale : f32;
+};
+
+type Particles = struct {
+  [[offset 0]] particles : array<Particle, 5>;
+};
+
+[[binding 0, set 0]] var<uniform> params : SimParams;
+[[binding 1, set 0]] var<storage_buffer> particlesA : Particles;
+[[binding 2, set 0]] var<storage_buffer> particlesB : Particles;
+
+[[builtin global_invocation_id]] var gl_GlobalInvocationID : vec3<f32>;
+
+# https://github.com/austinEng/Project6-Vulkan-Flocking/blob/master/data/shaders/computeparticles/particle.comp
+fn compute_main() -> void {
+  var index : u32 = gl_GlobalInvocationID.x;
+  if (index >= 5) {
+    return;
+  }
+
+  var vPos : vec2<f32> = particlesA.particles[index].pos;
+  var vVel : vec2<f32> = particlesA.particles[index].vel;
+
+  var cMass : vec2<f32> = vec2<f32>(0, 0);
+  var cVel : vec2<f32> = vec2<f32>(0, 0);
+  var colVel : vec2<f32> = vec2<f32>(0, 0);
+  var cMassCount : i32 = 0;
+  var cVelCount : i32 = 0;
+
+  var pos : vec2<f32>;
+  var vel : vec2<f32>;
+  var i : i32 = 0;
+  loop {
+    if (i >= 5) {
+      break;
+    }
+    if (i == index) {
+      continue;
+    }
+
+    pos = particlesA.particles[i].pos.xy;
+    vel = particlesA.particles[i].vel.xy;
+
+    if (std::distance(pos, vPos) < params.rule1Distance) {
+      cMass = cMass + pos;
+      cMassCount = cMassCount + 1;
+    }
+    if (std::distance(pos, vPos) < params.rule2Distance) {
+      colVel = colVel - (pos - vPos);
+    }
+    if (std::distance(pos, vPos) < params.rule3Distance) {
+      cVel = cVel + vel;
+      cVelCount = cVelCount + 1;
+    }
+
+    continuing {
+      i = i + 1;
+    }
+  }
+  if (cMassCount > 0) {
+    cMass = (cMass / vec2<f32>(cMassCount, cMassCount)) + vPos;
+  }
+  if (cVelCount > 0) {
+    cVel = cVel / vec2<f32>(cVelCount, cVelCount);
+  }
+
+  vVel = vVel + (cMass * params.rule1Scale) + (colVel * params.rule2Scale) +
+      (cVel * params.rule3Scale);
+
+  # clamp velocity for a more pleasing simulation
+  vVel = std::normalize(vVel) * std::fclamp(std::length(vVel), 0.0, 0.1);
+
+  # kinematic update
+  vPos = vPos + (vVel * params.deltaT);
+
+  # Wrap around boundary
+  if (vPos.x < -1.0) {
+    vPos.x = 1.0;
+  }
+  if (vPos.x > 1.0) {
+    vPos.x = -1.0;
+  }
+  if (vPos.y < -1.0) {
+    vPos.y = 1.0;
+  }
+  if (vPos.y > 1.0) {
+    vPos.y = -1.0;
+  }
+
+  # Write back
+  particlesB.particles[index].pos = vPos;
+  particlesB.particles[index].vel = vVel;
+
+  return;
+}
+entry_point compute as "main" = compute_main;
+
diff --git a/test/cube.wgsl b/test/cube.wgsl
new file mode 100644
index 0000000..ff56db5
--- /dev/null
+++ b/test/cube.wgsl
@@ -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.
+
+entry_point vertex as "main" = vtx_main;
+entry_point fragment as "main" = frag_main;
+
+# Vertex shader
+type Uniforms = struct {
+  [[offset 0]] modelViewProjectionMatrix : mat4x4<f32>;
+};
+
+[[binding 0, set 0]] var<uniform> uniforms : Uniforms;
+
+[[location 0]] var<in> cur_position : vec4<f32>;
+[[location 1]] var<in> color : vec4<f32>;
+[[location 0]] var<out> fragColor : vec4<f32>;
+[[builtin position]] var<out> Position : vec4<f32>;
+
+fn vtx_main() -> void {
+   Position = uniforms.modelViewProjectionMatrix * cur_position;
+   fragColor = color;
+   return;
+}
+
+# Fragment shader
+[[location 0]] var<in> fragColor : vec4<f32>;
+[[location 0]] var<out> outColor : vec4<f32>;
+
+fn frag_main() -> void {
+  outColor = fragColor;
+  return;
+}
diff --git a/test/simple.wgsl b/test/simple.wgsl
new file mode 100644
index 0000000..da941a2
--- /dev/null
+++ b/test/simple.wgsl
@@ -0,0 +1,21 @@
+# 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.
+
+[[location 0]] var<out> gl_FragColor : vec4<f32>;
+
+fn main() -> void {
+    gl_FragColor = vec4<f32>(0.4, 0.4, 0.8, 1.0);
+    return;
+}
+entry_point fragment = main;
diff --git a/test/triangle.wgsl b/test/triangle.wgsl
new file mode 100644
index 0000000..864417f
--- /dev/null
+++ b/test/triangle.wgsl
@@ -0,0 +1,36 @@
+# 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.
+
+# Vertex shader
+const pos : array<vec2<f32>, 3> = array<vec2<f32>, 3>(
+    vec2<f32>(0.0, 0.5),
+    vec2<f32>(-0.5, -0.5),
+    vec2<f32>(0.5, -0.5));
+
+[[builtin position]] var<out> Position : vec4<f32>;
+[[builtin vertex_idx]] var<in> VertexIndex : i32;
+
+fn vtx_main() -> void {
+  Position = vec4<f32>(pos[VertexIndex], 0.0, 1.0);
+  return;
+}
+entry_point vertex as "main" = vtx_main;
+
+# Fragment shader
+[[location 0]] var outColor : ptr<out, vec4<f32>>;
+fn frag_main() -> void {
+  outColor = vec4<f32>(1, 0, 0, 1);
+  return;
+}
+entry_point fragment as "main" = frag_main;
diff --git a/third_party/CMakeLists.txt b/third_party/CMakeLists.txt
new file mode 100644
index 0000000..48e646c
--- /dev/null
+++ b/third_party/CMakeLists.txt
@@ -0,0 +1,21 @@
+# 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.
+
+add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/googletest EXCLUDE_FROM_ALL)
+
+# SPIRV-Tools is only linked into tint if we're using the SPIR-V parser. We
+# always build it regardless so we can use the validator for testing purposes.
+set(SPIRV-Headers_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/spirv-headers CACHE STRING "")
+set(SPIRV_SKIP_TESTS ON CACHE BOOL ON)
+add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/spirv-tools)
diff --git a/tools/format b/tools/format
new file mode 100755
index 0000000..de1e706
--- /dev/null
+++ b/tools/format
@@ -0,0 +1,19 @@
+#!/bin/bash
+# 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.
+
+find src -name "*.h"  -exec clang-format -i {} \;
+find src -name "*.cc"  -exec clang-format -i {} \;
+find samples -name "*.h"  -exec clang-format -i {} \;
+find samples -name "*.cc"  -exec clang-format -i {} \;
diff --git a/tools/git-sync-deps b/tools/git-sync-deps
new file mode 100755
index 0000000..a562f26
--- /dev/null
+++ b/tools/git-sync-deps
@@ -0,0 +1,293 @@
+#!/usr/bin/env python
+# Copyright 2019 Google LLC
+#
+# 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.
+
+"""Parse a DEPS file and git checkout all of the dependencies.
+
+Args:
+  An optional list of deps_os values.
+
+  --with-swiftshader Checkout Swiftshader dependencies
+  --with-clspv       Checkout clspv dependencies
+  --with-dxc         Checkout dxc dependencies
+
+Environment Variables:
+  GIT_EXECUTABLE: path to "git" binary; if unset, will look for one of
+  ['git', 'git.exe', 'git.bat'] in your default path.
+
+  GIT_SYNC_DEPS_PATH: file to get the dependency list from; if unset,
+  will use the file ../DEPS relative to this script's directory.
+
+  GIT_SYNC_DEPS_QUIET: if set to non-empty string, suppress messages.
+
+Git Config:
+  To disable syncing of a single repository:
+      cd path/to/repository
+      git config sync-deps.disable true
+
+  To re-enable sync:
+      cd path/to/repository
+      git config --unset sync-deps.disable
+"""
+
+import os
+import re
+import subprocess
+import sys
+import threading
+from builtins import bytes
+
+with_clspv = False
+with_dxc = False
+with_swiftshader = False
+
+def git_executable():
+  """Find the git executable.
+
+  Returns:
+      A string suitable for passing to subprocess functions, or None.
+  """
+  envgit = os.environ.get('GIT_EXECUTABLE')
+  searchlist = ['git', 'git.exe', 'git.bat']
+  if envgit:
+    searchlist.insert(0, envgit)
+  with open(os.devnull, 'w') as devnull:
+    for git in searchlist:
+      try:
+        subprocess.call([git, '--version'], stdout=devnull)
+      except (OSError,):
+        continue
+      return git
+  return None
+
+
+DEFAULT_DEPS_PATH = os.path.normpath(
+  os.path.join(os.path.dirname(__file__), os.pardir, 'DEPS'))
+
+
+def usage(deps_file_path = None):
+  sys.stderr.write(
+    'Usage: run to grab dependencies, with optional platform support:\n')
+  sys.stderr.write('  %s %s' % (sys.executable, __file__))
+  if deps_file_path:
+    parsed_deps = parse_file_to_dict(deps_file_path)
+    if 'deps_os' in parsed_deps:
+      for deps_os in parsed_deps['deps_os']:
+        sys.stderr.write(' [%s]' % deps_os)
+  sys.stderr.write('\n\n')
+  sys.stderr.write(__doc__)
+
+
+def git_repository_sync_is_disabled(git, directory):
+  try:
+    disable = subprocess.check_output(
+      [git, 'config', 'sync-deps.disable'], cwd=directory)
+    return disable.lower().strip() in ['true', '1', 'yes', 'on']
+  except subprocess.CalledProcessError:
+    return False
+
+
+def is_git_toplevel(git, directory):
+  """Return true iff the directory is the top level of a Git repository.
+
+  Args:
+    git (string) the git executable
+
+    directory (string) the path into which the repository
+              is expected to be checked out.
+  """
+  try:
+    toplevel = subprocess.check_output(
+      [git, 'rev-parse', '--show-toplevel'], cwd=directory).strip()
+    return os.path.realpath(bytes(directory, 'utf8')) == os.path.realpath(toplevel)
+  except subprocess.CalledProcessError:
+    return False
+
+
+def status(directory, checkoutable):
+  def truncate(s, length):
+    return s if len(s) <= length else s[:(length - 3)] + '...'
+  dlen = 36
+  directory = truncate(directory, dlen)
+  checkoutable = truncate(checkoutable, 40)
+  sys.stdout.write('%-*s @ %s\n' % (dlen, directory, checkoutable))
+
+
+def git_checkout_to_directory(git, repo, checkoutable, directory, verbose):
+  """Checkout (and clone if needed) a Git repository.
+
+  Args:
+    git (string) the git executable
+
+    repo (string) the location of the repository, suitable
+         for passing to `git clone`.
+
+    checkoutable (string) a tag, branch, or commit, suitable for
+                 passing to `git checkout`
+
+    directory (string) the path into which the repository
+              should be checked out.
+
+    verbose (boolean)
+
+  Raises an exception if any calls to git fail.
+  """
+  if verbose:
+    status(directory, checkoutable)
+
+  if not os.path.isdir(directory):
+    subprocess.check_call(
+      [git, 'clone', '--quiet', repo, directory])
+
+  if not is_git_toplevel(git, directory):
+    # if the directory exists, but isn't a git repo, you will modify
+    # the parent repostory, which isn't what you want.
+    sys.stdout.write('%s\n  IS NOT TOP-LEVEL GIT DIRECTORY.\n' % directory)
+    return
+
+  # Check to see if this repo is disabled.  Quick return.
+  if git_repository_sync_is_disabled(git, directory):
+    sys.stdout.write('%s\n  SYNC IS DISABLED.\n' % directory)
+    return
+
+  with open(os.devnull, 'w') as devnull:
+    # If this fails, we will fetch before trying again.  Don't spam user
+    # with error infomation.
+    if 0 == subprocess.call([git, 'checkout', '--quiet', checkoutable],
+                            cwd=directory, stderr=devnull):
+      # if this succeeds, skip slow `git fetch`.
+      return
+
+  # If the repo has changed, always force use of the correct repo.
+  # If origin already points to repo, this is a quick no-op.
+  subprocess.check_call(
+      [git, 'remote', 'set-url', 'origin', repo], cwd=directory)
+
+  subprocess.check_call([git, 'fetch', '--quiet'], cwd=directory)
+
+  subprocess.check_call([git, 'checkout', '--quiet', checkoutable], cwd=directory)
+
+
+def parse_file_to_dict(path):
+  dictionary = {}
+  contents = open(path).read()
+  # Need to convert Var() to vars[], so that the DEPS is actually Python. Var()
+  # comes from Autoroller using gclient which has a slightly different DEPS
+  # format.
+  contents = re.sub(r"Var\((.*?)\)", r"vars[\1]", contents)
+  exec(contents, dictionary)
+  return dictionary
+
+
+def git_sync_deps(deps_file_path, command_line_os_requests, verbose):
+  """Grab dependencies, with optional platform support.
+
+  Args:
+    deps_file_path (string) Path to the DEPS file.
+
+    command_line_os_requests (list of strings) Can be empty list.
+        List of strings that should each be a key in the deps_os
+        dictionary in the DEPS file.
+
+  Raises git Exceptions.
+  """
+  git = git_executable()
+  assert git
+
+  deps_file_directory = os.path.dirname(deps_file_path)
+  deps_file = parse_file_to_dict(deps_file_path)
+  dependencies = deps_file['deps'].copy()
+  os_specific_dependencies = deps_file.get('deps_os', dict())
+  if 'all' in command_line_os_requests:
+    for value in os_specific_dependencies.values():
+      dependencies.update(value)
+  else:
+    for os_name in command_line_os_requests:
+      # Add OS-specific dependencies
+      if os_name in os_specific_dependencies:
+        dependencies.update(os_specific_dependencies[os_name])
+  for directory in dependencies:
+    for other_dir in dependencies:
+      if directory.startswith(other_dir + '/'):
+        raise Exception('%r is parent of %r' % (other_dir, directory))
+  list_of_arg_lists = []
+  for directory in sorted(dependencies):
+    if '@' in dependencies[directory]:
+      repo, checkoutable = dependencies[directory].split('@', 1)
+    else:
+      raise Exception("please specify commit or tag")
+
+    if not with_clspv and directory is 'third_party/clspv':
+      continue
+
+    if not with_clspv and directory is 'third_party/clspv-llvm':
+      continue
+
+    if not with_dxc and directory is 'third_party/dxc':
+      continue
+
+    if not with_swiftshader and directory is 'third_party/swiftshader':
+      continue
+
+    relative_directory = os.path.join(deps_file_directory, directory)
+
+    list_of_arg_lists.append(
+      (git, repo, checkoutable, relative_directory, verbose))
+
+  multithread(git_checkout_to_directory, list_of_arg_lists)
+
+  for directory in deps_file.get('recursedeps', []):
+    recursive_path = os.path.join(deps_file_directory, directory, 'DEPS')
+    git_sync_deps(recursive_path, command_line_os_requests, verbose)
+
+
+def multithread(function, list_of_arg_lists):
+  # for args in list_of_arg_lists:
+  #   function(*args)
+  # return
+  threads = []
+  for args in list_of_arg_lists:
+    thread = threading.Thread(None, function, None, args)
+    thread.start()
+    threads.append(thread)
+  for thread in threads:
+    thread.join()
+
+
+def main(argv):
+  deps_file_path = os.environ.get('GIT_SYNC_DEPS_PATH', DEFAULT_DEPS_PATH)
+  verbose = not bool(os.environ.get('GIT_SYNC_DEPS_QUIET', False))
+
+  if '--help' in argv or '-h' in argv:
+    usage(deps_file_path)
+    return 1
+
+  if '--with-clspv' in argv:
+    with_clspv = True
+
+  if '--with-dxc' in argv:
+    with_dxc = True
+
+  if '--with-swiftshader' in argv:
+    with_swiftshader = True
+
+  git_sync_deps(deps_file_path, argv, verbose)
+  # subprocess.check_call(
+  #     [sys.executable,
+  #      os.path.join(os.path.dirname(deps_file_path), 'bin', 'fetch-gn')])
+  return 0
+
+
+if __name__ == '__main__':
+  exit(main(sys.argv[1:]))
diff --git a/tools/lint b/tools/lint
new file mode 100755
index 0000000..0afb012
--- /dev/null
+++ b/tools/lint
@@ -0,0 +1,19 @@
+#!/bin/bash
+# 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.
+
+set -e  # fail on error
+
+./third_party/cpplint/cpplint/cpplint.py `find src -type f`
+./third_party/cpplint/cpplint/cpplint.py `find samples -type f`
diff --git a/tools/roll-all b/tools/roll-all
new file mode 100755
index 0000000..799e4e3
--- /dev/null
+++ b/tools/roll-all
@@ -0,0 +1,66 @@
+#!/usr/bin/env python
+# 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.
+
+"""Parse a DEPS file and rolls all of the dependencies.
+"""
+
+import os
+import re
+import subprocess
+import sys
+
+
+DEFAULT_DEPS_PATH = os.path.normpath(
+  os.path.join(os.path.dirname(__file__), os.pardir, 'DEPS'))
+
+
+def parse_file_to_dict(path):
+  dictionary = {}
+  contents = open(path).read()
+  # Need to convert Var() to vars[], so that the DEPS is actually Python. Var()
+  # comes from Autoroller using gclient which has a slightly different DEPS
+  # format.
+  contents = re.sub(r"Var\((.*?)\)", r"vars[\1]", contents)
+  exec(contents, dictionary)
+  return dictionary
+
+
+def roll_all_deps(deps_file_path):
+  deps_file_directory = os.path.dirname(deps_file_path)
+  deps_file = parse_file_to_dict(deps_file_path)
+
+  dependencies = deps_file['deps'].copy()
+
+  list_of_deps = ['roll-dep', '--ignore-dirty-tree']
+  for directory in sorted(deps_file['deps']):
+    # cpplint uses gh-pages as the main branch, not master which doesn't work
+    # with roll-dep
+    if directory == "third_party/cpplint":
+      continue
+
+    relative_directory = os.path.join(deps_file_directory, directory)
+    list_of_deps.append(relative_directory)
+
+  subprocess.check_call(list_of_deps)
+
+
+def main(argv):
+  deps_file_path = DEFAULT_DEPS_PATH
+
+  roll_all_deps(deps_file_path)
+
+
+if __name__ == '__main__':
+  exit(main(sys.argv[1:]))