You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tvm.apache.org by GitBox <gi...@apache.org> on 2020/07/07 17:47:00 UTC

[GitHub] [incubator-tvm] weberlo commented on a change in pull request #5932: [Frontend][Relay] Add Parser 2.0

weberlo commented on a change in pull request #5932:
URL: https://github.com/apache/incubator-tvm/pull/5932#discussion_r451005507



##########
File path: src/parser/parser.cc
##########
@@ -0,0 +1,1384 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/*!
+ * \file parser.cc
+ * \brief A parser for TVM IR.
+ */
+#include <tvm/ir/module.h>
+#include <tvm/node/reflection.h>
+#include <tvm/relay/adt.h>
+#include <tvm/relay/expr.h>
+#include <tvm/relay/function.h>
+#include <tvm/runtime/object.h>
+#include <tvm/runtime/registry.h>
+
+#include <fstream>
+
+#include "./diagnostic.h"
+#include "./op_table.h"
+#include "./tokenizer.h"
+
+namespace tvm {
+namespace parser {
+
+using namespace relay;
+using Expr = relay::Expr;
+
+/*! \brief A wrapper structure for capturing the result of parsing
+ * a global definition *before* we add it to the IRModule.
+ *
+ * This enables the parser to parse everything in one pass before
+ * constructing the IRModule.
+ */
+struct GlobalFunc {
+  GlobalVar global;
+  Function function;
+  GlobalFunc() : global(), function() {}
+  GlobalFunc(GlobalVar global, Function function) : global(global), function(function) {}
+  GlobalFunc(const GlobalFunc& gfunc) {
+    this->global = gfunc.global;
+    this->function = gfunc.function;
+  }
+};
+
+/*! \brief A wrapper structure for capturing all top-level definitions
+ * when parsing a module.
+ */
+struct Definitions {
+  /*! \brief The set of global functions. */
+  std::vector<GlobalFunc> funcs;
+  /*! \brief The set of type definitions. */
+  std::vector<TypeData> types;
+  // TODO(@jroesch): contain meta-table below
+};
+
+/*! \brief A structure representing the semantic versioning information
+ * for a Relay program.
+ */
+struct SemVer {
+  int major;
+  int minor;
+  int patch;
+};
+
+/*! \brief A reference to a "meta-expression".
+ *
+ * In the text format we allow referencing metadata which
+ * uses a compact serialization that proceeds the main
+ * program body.
+ *
+ * We can reference this table using an expression of
+ * the form `meta[Type][index]`.
+ *
+ * We must later resolve these references to actual in-memory
+ * AST nodes but this requires first parsing the full program
+ * then expanding these temporary AST nodes into their corresponding
+ * nodes.
+ *
+ * For example the nth large constant will be pretty-printed as meta[relay.Constant][n]
+ * with its compact binary serialization residing in the metadata section at the end
+ * of the program.
+ */
+class MetaRefExprNode : public TempExprNode {
+ public:
+  /*! \brief The type key of the meta expression. */
+  std::string type_key;
+  /*! \brief The index into the type key's table. */
+  uint64_t node_index;
+
+  void VisitAttrs(tvm::AttrVisitor* v) {}
+
+  // TODO(@jroesch): we probably will need to manually
+  // expand these with a pass.
+  Expr Realize() const final { return Expr(); }
+
+  static constexpr const char* _type_key = "relay.MetaRefExpr";
+  TVM_DECLARE_FINAL_OBJECT_INFO(MetaRefExprNode, TempExprNode);
+};
+
+class MetaRefExpr : public TempExpr {
+ public:
+  /*!
+   * \brief The constructor for MetaRefExpr
+   * \param type_key The type key of the object in the meta section.
+   * \param kind The index into that subfield.
+   */
+  TVM_DLL MetaRefExpr(std::string type_key, uint64_t node_index);
+
+  TVM_DEFINE_OBJECT_REF_METHODS(MetaRefExpr, TempExpr, MetaRefExprNode);
+};
+
+MetaRefExpr::MetaRefExpr(std::string type_key, uint64_t node_index) {
+  auto rnode = make_object<MetaRefExprNode>();
+  rnode->type_key = type_key;
+  rnode->node_index = node_index;
+  data_ = std::move(rnode);
+}
+
+/*! \brief A simple wrapper around a mapping from raw string names
+ * to a TVM variable, type variable or other binder type.
+ */
+template <typename T>
+struct Scope {
+  /*! \brief The internal map. */
+  std::unordered_map<std::string, T> name_map;
+};
+
+/*! \brief A stack of scopes.
+ *
+ * In order to properly handle scoping we must maintain a scope of stacks.

Review comment:
       ```suggestion
    * In order to properly handle scoping we must maintain a stack of scopes.
   ```

##########
File path: src/parser/parser.cc
##########
@@ -0,0 +1,1384 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/*!
+ * \file parser.cc
+ * \brief A parser for TVM IR.
+ */
+#include <tvm/ir/module.h>
+#include <tvm/node/reflection.h>
+#include <tvm/relay/adt.h>
+#include <tvm/relay/expr.h>
+#include <tvm/relay/function.h>
+#include <tvm/runtime/object.h>
+#include <tvm/runtime/registry.h>
+
+#include <fstream>
+
+#include "./diagnostic.h"
+#include "./op_table.h"
+#include "./tokenizer.h"
+
+namespace tvm {
+namespace parser {
+
+using namespace relay;
+using Expr = relay::Expr;
+
+/*! \brief A wrapper structure for capturing the result of parsing
+ * a global definition *before* we add it to the IRModule.
+ *
+ * This enables the parser to parse everything in one pass before
+ * constructing the IRModule.
+ */
+struct GlobalFunc {
+  GlobalVar global;
+  Function function;
+  GlobalFunc() : global(), function() {}
+  GlobalFunc(GlobalVar global, Function function) : global(global), function(function) {}
+  GlobalFunc(const GlobalFunc& gfunc) {
+    this->global = gfunc.global;
+    this->function = gfunc.function;
+  }
+};
+
+/*! \brief A wrapper structure for capturing all top-level definitions
+ * when parsing a module.
+ */
+struct Definitions {
+  /*! \brief The set of global functions. */
+  std::vector<GlobalFunc> funcs;
+  /*! \brief The set of type definitions. */
+  std::vector<TypeData> types;
+  // TODO(@jroesch): contain meta-table below
+};
+
+/*! \brief A structure representing the semantic versioning information
+ * for a Relay program.
+ */
+struct SemVer {
+  int major;
+  int minor;
+  int patch;
+};
+
+/*! \brief A reference to a "meta-expression".
+ *
+ * In the text format we allow referencing metadata which
+ * uses a compact serialization that proceeds the main
+ * program body.
+ *
+ * We can reference this table using an expression of
+ * the form `meta[Type][index]`.
+ *
+ * We must later resolve these references to actual in-memory
+ * AST nodes but this requires first parsing the full program
+ * then expanding these temporary AST nodes into their corresponding
+ * nodes.
+ *
+ * For example the nth large constant will be pretty-printed as meta[relay.Constant][n]
+ * with its compact binary serialization residing in the metadata section at the end
+ * of the program.
+ */
+class MetaRefExprNode : public TempExprNode {
+ public:
+  /*! \brief The type key of the meta expression. */
+  std::string type_key;
+  /*! \brief The index into the type key's table. */
+  uint64_t node_index;
+
+  void VisitAttrs(tvm::AttrVisitor* v) {}
+
+  // TODO(@jroesch): we probably will need to manually
+  // expand these with a pass.
+  Expr Realize() const final { return Expr(); }
+
+  static constexpr const char* _type_key = "relay.MetaRefExpr";
+  TVM_DECLARE_FINAL_OBJECT_INFO(MetaRefExprNode, TempExprNode);
+};
+
+class MetaRefExpr : public TempExpr {
+ public:
+  /*!
+   * \brief The constructor for MetaRefExpr
+   * \param type_key The type key of the object in the meta section.
+   * \param kind The index into that subfield.
+   */
+  TVM_DLL MetaRefExpr(std::string type_key, uint64_t node_index);
+
+  TVM_DEFINE_OBJECT_REF_METHODS(MetaRefExpr, TempExpr, MetaRefExprNode);
+};
+
+MetaRefExpr::MetaRefExpr(std::string type_key, uint64_t node_index) {
+  auto rnode = make_object<MetaRefExprNode>();
+  rnode->type_key = type_key;
+  rnode->node_index = node_index;
+  data_ = std::move(rnode);
+}
+
+/*! \brief A simple wrapper around a mapping from raw string names
+ * to a TVM variable, type variable or other binder type.
+ */
+template <typename T>
+struct Scope {
+  /*! \brief The internal map. */
+  std::unordered_map<std::string, T> name_map;
+};
+
+/*! \brief A stack of scopes.
+ *
+ * In order to properly handle scoping we must maintain a scope of stacks.
+ *
+ * A stack allows users to write programs which contain repeated variable
+ * names and to properly handle both nested scopes and removal of variables
+ * when they go out of scope.
+ *
+ * This is the classic approach to lexical scoping.
+ */
+template <typename T>
+class ScopeStack {
+ private:
+  std::vector<Scope<T>> scope_stack;
+
+ public:
+  /*! \brief Adds a variable binding to the current scope. */
+  void Add(const std::string& name, const T& value) {
+    if (!this->scope_stack.size()) {
+      LOG(FATAL) << "internal issue";
+    }
+    this->scope_stack.back().name_map.insert({name, value});
+  }
+
+  /*! \brief Looks up a variable name in the scope stack returning the matching variable
+   * in most recent scope. */
+  T Lookup(const std::string& name) {
+    for (auto scope = this->scope_stack.rbegin(); scope != this->scope_stack.rend(); ++scope) {
+      auto it = scope->name_map.find(name);
+      if (it != scope->name_map.end()) {
+        return it->second;
+      }
+    }
+    return T();
+  }
+
+  /*! \brief Adds a fresh scope. */
+  void PushStack() { this->scope_stack.push_back(Scope<T>()); }
+
+  /*! \brief Removes the most recent scope. */
+  void PopStack() { this->scope_stack.pop_back(); }
+};
+
+/*! \brief A table of interning strings as global function and type names. */
+template <typename T>
+struct InternTable {
+  /*! \brief The internal table mapping strings to a unique allocation. */
+  std::unordered_map<std::string, T> table;
+
+  /*! \brief Add the unique allocation. */
+  void Add(const std::string& name, const T& t) {
+    auto it = table.find(name);
+    if (it != table.end()) {
+      LOG(FATAL) << "duplicate name";
+    } else {
+      table.insert({name, t});
+    }
+  }
+
+  /*! \brief Return the unique allocation. */
+  Optional<T> Get(const std::string& name) {
+    auto it = table.find(name);
+    if (it != table.end()) {
+      return Optional<T>(it->second);
+    } else {
+      return Optional<T>();
+    }
+  }
+};
+
+/*! \brief The parser class is the main interface to the parser.
+ * the parser is not currently exposed beyond this .cc file.
+ *
+ * The parser is initialized with a diagnostic context, an
+ * operator table, and a token stream.
+ *
+ * The rest of the internal state is used to map the human readable
+ * form to in-memory IR representation.
+ *
+ * The main entry point to the parser are a set of parsing methods
+ * such as `ParseModule` and `ParseExpr`.
+ *
+ * As with traditional recursive descent parsers the parsing methods
+ * are factored recursively just as one would do with a formal language
+ * grammar.
+ *
+ * You can view a recursive descent parser as a human friendly way to specify
+ * a state machine, and thus this factoring is necessary as the 'state' of this
+ * machine is the combination of the current parsing method and the next token.
+ *
+ * Parsing proceeds by matching a token and then dispatching to the appropriate
+ * method to parse the next tokens in the stream.
+ *
+ * For example if we are parsing a type and encounter a "Tensor" token we switch
+ * into a mode for parsing `[`, a shape, a comma, a data type and then a ']'.
+ *
+ * Certain matches like this are unambiguous and proceed in a straight line fashion
+ * once the initial token is found. Other parsing is more complex and requires some
+ * tricks to correctly parse.
+ *
+ * For example when we find a '(' in an expression context, it may be part of
+ * a tuple, the arguments to a call, or a parenthesized expression. The below code
+ * disambiguate these cases by factoring expression parsing into a series of methods
+ * which encode the parsing context the and thus how to interpret the parenthesis.
+ *
+ * For more information one should be able to read the code in order starting with
+ * `ParseModule` or `ParseExpr`.
+ */
+class Parser {
+ public:
+  /*! \brief The version that the parser is parsing. */
+  SemVer version;
+
+  /*! \brief The diagnostic context used for error reporting. */
+  DiagnosticContext diag_ctx;
+
+  /*! \brief The current position in the token stream. */
+  int pos;
+
+  /*! \brief The token stream for the parser. */
+  std::vector<Token> tokens;
+
+  /*! \brief The configured operator table. */
+  OperatorTable op_table;
+
+  /*! \brief Configure the whitespace mode, right now we ignore all whitespace. */
+  bool ignore_whitespace;
+
+  /*! \brief A global mapping for GlobalVar. */
+  InternTable<GlobalVar> global_names;
+
+  /*! \brief A global mapping for type definitions. */
+  InternTable<GlobalTypeVar> type_names;
+
+  /*! \brief A global mapping for constructor names. */
+  InternTable<Constructor> ctors;
+
+  /*! \brief A mapping from graph variable to expression, i.e., `%0 = expr`. */
+  std::unordered_map<int, Expr> graph_ctx;
+
+  /*! \brief The set of type scopes used for generics. */
+  ScopeStack<TypeVar> type_scopes;
+
+  /*! \brief The set of expression scopes used for lexical scope. */
+  ScopeStack<Var> expr_scopes;
+
+  Parser(std::vector<Token> tokens, OperatorTable op_table, Source source)
+      : diag_ctx(source), pos(0), tokens(tokens), op_table(op_table), ignore_whitespace(true) {}
+
+  /*! \brief Examine the next token in the stream, the current parser is configured to be
+   * whitespace insensitive so we will skip all whitespace or comment tokens. */
+  Token Peek() {
+    // For now we ignore all whitespace tokens and comments.
+    // We can tweak this behavior later to enable white space sensitivity in the parser.
+    while (pos < tokens.size() && ignore_whitespace &&
+           (tokens.at(pos)->token_type == TokenType::Whitespace ||
+            tokens.at(pos)->token_type == TokenType::Newline ||
+            tokens.at(pos)->token_type == TokenType::LineComment ||
+            tokens.at(pos)->token_type == TokenType::Comment)) {
+      pos++;
+    }
+
+    if (pos < tokens.size()) {
+      return Token(this->tokens.at(pos));
+    } else {
+      return Token::Null();
+    }
+  }
+
+  /*! \brief Lookahead by N tokens.
+   * \param n The number of tokens to lookahead.
+   * \return The Nth token.
+   */
+  Token Lookahead(int n) {
+    CHECK_GE(n, 1) << "lookahead is only valid when n >= 1";
+
+    // We intend to skip n - 1 tokens, then return the nth.
+    auto old_pos = pos;
+    for (int i = 0; i < n - 1; i++) {
+      Peek();
+      pos++;
+    }
+
+    auto tok = Peek();
+    pos = old_pos;
+    return tok;
+  }
+
+  /*! \brief Consume a token, this method is the lowest level way to consume a token
+   * and will not ignore white space or look ahead in anyway.
+   *
+   * /param token_type The token type to match.
+   */
+  void Consume(const TokenType& token_type) {
+    if (tokens[pos]->token_type != token_type) {
+      std::string message =
+          "expected a " + Pretty(token_type) + " found " + Pretty(Peek()->token_type);
+      this->diag_ctx.Emit({tokens[pos]->line, tokens[pos]->column, message});
+      this->diag_ctx.Render(std::cout);
+    }
+    pos++;
+  }
+
+  /*! Match a token in the stream, this will first invoke Peek, ignoring tokens such
+   * as whitespace or comments returning the first meaningful token.
+   *
+   * We then try and consume the requested token, this will trigger an error if the
+   * current token does not match the token_type.
+   */
+  Token Match(const TokenType& token_type) {
+    auto tok = Peek();
+    Consume(token_type);
+    return tok;
+  }
+
+  /*! Conditionally consume a token when it matches, this will never trigger an error
+   * as we guard against consuming the token before we do.
+   *
+   * Useful for matching optional tokens, effectively looksahead by one.
+   */
+  bool WhenMatch(const TokenType& token_type) {
+    if (Peek()->token_type == token_type) {
+      Consume(token_type);
+      return true;
+    } else {
+      return false;
+    }
+  }
+
+  /* \brief Add a graph binding to the parsing context
+   *
+   * For example if we parse %0 = add(...), map 0 -> add(...), etc.
+   */
+  void AddGraphBinding(const Token& token, const Expr& expr) {
+    auto graph_no = token.ToNumber();
+    this->graph_ctx.insert({graph_no, expr});
+  }
+
+  /* \brief Lookup a previously bound graph variable.
+   *
+   * Note: we take tokens in all lookup methods so that we
+   * that we can do error reporting based on token location.
+   */
+  Expr LookupGraphBinding(const Token& token) {
+    auto graph_no = token.ToNumber();
+    return this->graph_ctx.at(graph_no);
+  }
+
+  /*! \brief Bind a local variable in the expression scope.
+   *
+   * "x" -> Var("x"), these are needed to map from the raw string names
+   * to unique variable nodes.
+   */
+  Var BindVar(const std::string& name, const relay::Type& type_annotation) {
+    auto var = Var(name, type_annotation);
+    this->expr_scopes.Add(name, var);
+    return var;
+  }
+
+  /*! \brief Bind a type variable in the type scope.
+   *
+   * "A" -> TypeVar("A", ...), these are needed to map from raw string names
+   * to unique type variable nodes.
+   */
+  TypeVar BindTypeVar(const std::string& name, const TypeKind type_kind) {
+    auto type_var = TypeVar(name, type_kind);
+    this->type_scopes.Add(name, type_var);
+    return type_var;
+  }
+
+  /*! \brief Lookup a variable in the expression scope.
+   *
+   * Note: all lookup methods take tokens intentionally for error reporting information.
+   */
+  Var LookupLocal(const Token& local) {
+    auto var = this->expr_scopes.Lookup(local.ToString());
+    if (!var.defined()) {
+      diag_ctx.Emit(
+          {local->line, local->column, "this local variable has not been previously declared"});
+    }
+    return var;
+  }
+
+  /*! \brief Lookup a variable in the type scope.
+   *
+   * Note: all lookup methods take tokens intentionally for error reporting information.
+   */
+  TypeVar LookupTypeVar(const Token& ident) {
+    auto var = this->type_scopes.Lookup(ident.ToString());
+    if (!var.defined()) {
+      diag_ctx.Emit(
+          {ident->line, ident->column,
+           "this type variable has not been previously declared anywhere, perhaps a typo?"});
+    }
+    return var;
+  }
+
+  /*! \brief Add an expression scope to the scope stack. */
+  void PushScope() { this->expr_scopes.PushStack(); }
+
+  /*! \brief Remove N expression scopes from the scope stack. */
+  void PopScopes(int n) {
+    for (int i = 0; i < n; i++) {
+      this->expr_scopes.PopStack();
+    }
+  }
+
+  /*! \brief Add an type scope to the scope stack. */
+  void PushTypeScope() { this->type_scopes.PushStack(); }
+
+  /*! \brief Remove N type scopes from the scope stack. */
+  void PopTypeScopes(int n) {
+    for (int i = 0; i < n; i++) {
+      this->type_scopes.PopStack();
+    }
+  }
+
+  /*! \brief Convert a numeric token to an NDArray for embedding into the Relay program. */
+  NDArray NumberToNDArray(const Token& token) {
+    if (token->token_type == TokenType::Integer) {
+      DLContext ctx({.device_type = DLDeviceType::kDLCPU, .device_id = 0});
+      auto dtype = String2DLDataType("int32");
+      auto data = NDArray::Empty({}, dtype, ctx);
+      auto array = reinterpret_cast<int32_t*>(data->data);
+      // revisit this, literal node issue.
+      int64_t value = Downcast<tvm::Integer>(token->data);
+      array[0] = (int32_t)value;
+      return data;
+    } else if (token->token_type == TokenType::Float) {
+      DLContext ctx({.device_type = DLDeviceType::kDLCPU, .device_id = 0});
+      auto dtype = String2DLDataType("float32");
+      auto data = NDArray::Empty({}, dtype, ctx);
+      auto array = reinterpret_cast<float*>(data->data);
+      // revisit this, literal node issue.
+      float value = Downcast<tvm::FloatImm>(token->data)->value;
+      array[0] = value;
+      return data;
+    } else {
+      LOG(FATAL) << "internal error: should only call this function on numeric tokens";
+      return NDArray();
+    }
+  }
+
+  /*! \brief Convert a boolean value to an NDArray for embedding into the Relay program. */
+  NDArray BooleanToNDarray(bool value) {
+    DLContext ctx({.device_type = DLDeviceType::kDLCPU, .device_id = 0});
+    auto dtype = String2DLDataType("bool");
+    auto data = NDArray::Empty({}, dtype, ctx);
+    auto array = reinterpret_cast<bool*>(data->data);
+    array[0] = value;
+    return data;
+  }
+
+  [[noreturn]] void ParseError(const Token& token, const std::string& msg) {
+    throw std::runtime_error(msg);
+  }
+
+  /*! \brief A parsing helper for a bracketed expression <start> <parser> <stop>. */
+  template <typename R>
+  R Bracket(TokenType open, TokenType close, std::function<R()> parser) {
+    Match(open);
+    R result = parser();
+    Match(close);
+    return result;
+  }
+
+  /*! \brief Parse `(` parser() `)`. */
+  template <typename R>
+  R Parens(std::function<R()> parser) {
+    return Bracket(TokenType::OpenParen, TokenType::CloseParen, parser);
+  }
+
+  /*! \brief Parse `{` parser() `}`. */
+  template <typename R>
+  R Block(std::function<R()> parser) {
+    return Bracket(TokenType::LCurly, TokenType::RCurly, parser);
+  }
+
+  /*! \brief Parses a sequence beginning with a start token, seperated by a seperator token, and
+   * ending with a stop token.
+   *
+   * The simple form being <start> (<parse()> <seperator>)* <stop>.
+   *
+   * This also provides a fourth argument which is allowed to run when the sequence which matches
+   * the inner sequence can not proceed.
+   *
+   * This is useful for parsing things like attributes which don't match the standard expression
+   * parsers but are contained within the stop token.
+   */
+  template <typename T>
+  Array<T> ParseSequence(TokenType start, TokenType sep, TokenType stop, std::function<T()> parse,
+                         std::function<void()> before_stop = nullptr) {
+    Match(start);
+    if (WhenMatch(stop)) {
+      return Array<T>();
+    } else {
+      auto data = parse();
+      Array<T> elements = {data};
+
+      // parse '(' expr ')'
+      // if we are at the end invoke leftover parser
+      if (Peek()->token_type == stop && before_stop) {
+        before_stop();
+      }
+      if (WhenMatch(stop)) {
+        return elements;
+        // parse '( expr ',' * ')'
+      } else if (WhenMatch(sep)) {
+        // if we are at the end invoke leftover parser
+        if (Peek()->token_type == stop && before_stop) {
+          before_stop();
+        }
+        while (true) {
+          if (WhenMatch(stop)) {
+            break;
+          } else {
+            auto data = parse();
+            WhenMatch(sep);
+            elements.push_back(data);
+          }
+        }
+        return elements;
+      } else {
+        LOG(FATAL) << "issue";
+        return Array<T>(nullptr);
+      }
+    }
+  }
+
+  /*! \brief Parse a full IRModule. */
+  IRModule ParseModule() {
+    // Parse the semver header at the top of the module.
+    this->version = ParseSemVer();
+    // Parse the definitions.
+    auto defs = ParseDefinitions();
+    // Parse the metadata section at the end.
+    auto metadata = ParseMetadata();
+    Match(TokenType::EndOfFile);
+    Map<tvm::GlobalVar, BaseFunc> funcs;
+    Map<tvm::GlobalTypeVar, TypeData> types;
+
+    for (auto type_def : defs.types) {
+      types.Set(type_def->header, type_def);
+    }
+
+    auto mod = IRModule({}, types);
+
+    for (auto func : defs.funcs) {
+      mod->Add(func.global, func.function);
+    }
+
+    return mod;
+  }
+
+  /*! \brief Parse the semantic versioning header. */
+  SemVer ParseSemVer() {
+    // TODO(@jroesch): convert semver to module level attribute.
+    auto id = Peek();
+    if (id->token_type == TokenType::Identifier && id.ToString() == "v0") {
+      auto id = Match(TokenType::Identifier);
+      Consume(TokenType::Period);
+      // CHECK_EQ(minor_and_patch)

Review comment:
       remove?

##########
File path: src/parser/parser.cc
##########
@@ -0,0 +1,1384 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/*!
+ * \file parser.cc
+ * \brief A parser for TVM IR.
+ */
+#include <tvm/ir/module.h>
+#include <tvm/node/reflection.h>
+#include <tvm/relay/adt.h>
+#include <tvm/relay/expr.h>
+#include <tvm/relay/function.h>
+#include <tvm/runtime/object.h>
+#include <tvm/runtime/registry.h>
+
+#include <fstream>
+
+#include "./diagnostic.h"
+#include "./op_table.h"
+#include "./tokenizer.h"
+
+namespace tvm {
+namespace parser {
+
+using namespace relay;
+using Expr = relay::Expr;
+
+/*! \brief A wrapper structure for capturing the result of parsing
+ * a global definition *before* we add it to the IRModule.
+ *
+ * This enables the parser to parse everything in one pass before
+ * constructing the IRModule.
+ */
+struct GlobalFunc {
+  GlobalVar global;
+  Function function;
+  GlobalFunc() : global(), function() {}
+  GlobalFunc(GlobalVar global, Function function) : global(global), function(function) {}
+  GlobalFunc(const GlobalFunc& gfunc) {
+    this->global = gfunc.global;
+    this->function = gfunc.function;
+  }
+};
+
+/*! \brief A wrapper structure for capturing all top-level definitions
+ * when parsing a module.
+ */
+struct Definitions {
+  /*! \brief The set of global functions. */
+  std::vector<GlobalFunc> funcs;
+  /*! \brief The set of type definitions. */
+  std::vector<TypeData> types;
+  // TODO(@jroesch): contain meta-table below
+};
+
+/*! \brief A structure representing the semantic versioning information
+ * for a Relay program.
+ */
+struct SemVer {
+  int major;
+  int minor;
+  int patch;
+};
+
+/*! \brief A reference to a "meta-expression".
+ *
+ * In the text format we allow referencing metadata which
+ * uses a compact serialization that proceeds the main
+ * program body.
+ *
+ * We can reference this table using an expression of
+ * the form `meta[Type][index]`.
+ *
+ * We must later resolve these references to actual in-memory
+ * AST nodes but this requires first parsing the full program
+ * then expanding these temporary AST nodes into their corresponding
+ * nodes.
+ *
+ * For example the nth large constant will be pretty-printed as meta[relay.Constant][n]
+ * with its compact binary serialization residing in the metadata section at the end
+ * of the program.
+ */
+class MetaRefExprNode : public TempExprNode {
+ public:
+  /*! \brief The type key of the meta expression. */
+  std::string type_key;
+  /*! \brief The index into the type key's table. */
+  uint64_t node_index;
+
+  void VisitAttrs(tvm::AttrVisitor* v) {}
+
+  // TODO(@jroesch): we probably will need to manually
+  // expand these with a pass.
+  Expr Realize() const final { return Expr(); }
+
+  static constexpr const char* _type_key = "relay.MetaRefExpr";
+  TVM_DECLARE_FINAL_OBJECT_INFO(MetaRefExprNode, TempExprNode);
+};
+
+class MetaRefExpr : public TempExpr {
+ public:
+  /*!
+   * \brief The constructor for MetaRefExpr
+   * \param type_key The type key of the object in the meta section.
+   * \param kind The index into that subfield.
+   */
+  TVM_DLL MetaRefExpr(std::string type_key, uint64_t node_index);
+
+  TVM_DEFINE_OBJECT_REF_METHODS(MetaRefExpr, TempExpr, MetaRefExprNode);
+};
+
+MetaRefExpr::MetaRefExpr(std::string type_key, uint64_t node_index) {
+  auto rnode = make_object<MetaRefExprNode>();
+  rnode->type_key = type_key;
+  rnode->node_index = node_index;
+  data_ = std::move(rnode);
+}
+
+/*! \brief A simple wrapper around a mapping from raw string names
+ * to a TVM variable, type variable or other binder type.
+ */
+template <typename T>
+struct Scope {
+  /*! \brief The internal map. */
+  std::unordered_map<std::string, T> name_map;
+};
+
+/*! \brief A stack of scopes.
+ *
+ * In order to properly handle scoping we must maintain a scope of stacks.
+ *
+ * A stack allows users to write programs which contain repeated variable
+ * names and to properly handle both nested scopes and removal of variables
+ * when they go out of scope.
+ *
+ * This is the classic approach to lexical scoping.
+ */
+template <typename T>
+class ScopeStack {
+ private:
+  std::vector<Scope<T>> scope_stack;
+
+ public:
+  /*! \brief Adds a variable binding to the current scope. */
+  void Add(const std::string& name, const T& value) {
+    if (!this->scope_stack.size()) {
+      LOG(FATAL) << "internal issue";
+    }
+    this->scope_stack.back().name_map.insert({name, value});
+  }
+
+  /*! \brief Looks up a variable name in the scope stack returning the matching variable
+   * in most recent scope. */
+  T Lookup(const std::string& name) {
+    for (auto scope = this->scope_stack.rbegin(); scope != this->scope_stack.rend(); ++scope) {
+      auto it = scope->name_map.find(name);
+      if (it != scope->name_map.end()) {
+        return it->second;
+      }
+    }
+    return T();
+  }
+
+  /*! \brief Adds a fresh scope. */
+  void PushStack() { this->scope_stack.push_back(Scope<T>()); }
+
+  /*! \brief Removes the most recent scope. */
+  void PopStack() { this->scope_stack.pop_back(); }
+};
+
+/*! \brief A table of interning strings as global function and type names. */
+template <typename T>
+struct InternTable {
+  /*! \brief The internal table mapping strings to a unique allocation. */
+  std::unordered_map<std::string, T> table;
+
+  /*! \brief Add the unique allocation. */
+  void Add(const std::string& name, const T& t) {
+    auto it = table.find(name);
+    if (it != table.end()) {
+      LOG(FATAL) << "duplicate name";
+    } else {
+      table.insert({name, t});
+    }
+  }
+
+  /*! \brief Return the unique allocation. */
+  Optional<T> Get(const std::string& name) {
+    auto it = table.find(name);
+    if (it != table.end()) {
+      return Optional<T>(it->second);
+    } else {
+      return Optional<T>();
+    }
+  }
+};
+
+/*! \brief The parser class is the main interface to the parser.
+ * the parser is not currently exposed beyond this .cc file.
+ *
+ * The parser is initialized with a diagnostic context, an
+ * operator table, and a token stream.
+ *
+ * The rest of the internal state is used to map the human readable
+ * form to in-memory IR representation.
+ *
+ * The main entry point to the parser are a set of parsing methods
+ * such as `ParseModule` and `ParseExpr`.
+ *
+ * As with traditional recursive descent parsers the parsing methods
+ * are factored recursively just as one would do with a formal language
+ * grammar.
+ *
+ * You can view a recursive descent parser as a human friendly way to specify
+ * a state machine, and thus this factoring is necessary as the 'state' of this
+ * machine is the combination of the current parsing method and the next token.
+ *
+ * Parsing proceeds by matching a token and then dispatching to the appropriate
+ * method to parse the next tokens in the stream.
+ *
+ * For example if we are parsing a type and encounter a "Tensor" token we switch
+ * into a mode for parsing `[`, a shape, a comma, a data type and then a ']'.
+ *
+ * Certain matches like this are unambiguous and proceed in a straight line fashion
+ * once the initial token is found. Other parsing is more complex and requires some
+ * tricks to correctly parse.
+ *
+ * For example when we find a '(' in an expression context, it may be part of
+ * a tuple, the arguments to a call, or a parenthesized expression. The below code
+ * disambiguate these cases by factoring expression parsing into a series of methods
+ * which encode the parsing context the and thus how to interpret the parenthesis.

Review comment:
       ```suggestion
    * which encode the parsing context and thus how to interpret the parenthesis.
   ```

##########
File path: src/parser/parser.cc
##########
@@ -0,0 +1,1384 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/*!
+ * \file parser.cc
+ * \brief A parser for TVM IR.
+ */
+#include <tvm/ir/module.h>
+#include <tvm/node/reflection.h>
+#include <tvm/relay/adt.h>
+#include <tvm/relay/expr.h>
+#include <tvm/relay/function.h>
+#include <tvm/runtime/object.h>
+#include <tvm/runtime/registry.h>
+
+#include <fstream>
+
+#include "./diagnostic.h"
+#include "./op_table.h"
+#include "./tokenizer.h"
+
+namespace tvm {
+namespace parser {
+
+using namespace relay;
+using Expr = relay::Expr;
+
+/*! \brief A wrapper structure for capturing the result of parsing
+ * a global definition *before* we add it to the IRModule.
+ *
+ * This enables the parser to parse everything in one pass before
+ * constructing the IRModule.
+ */
+struct GlobalFunc {
+  GlobalVar global;
+  Function function;
+  GlobalFunc() : global(), function() {}
+  GlobalFunc(GlobalVar global, Function function) : global(global), function(function) {}
+  GlobalFunc(const GlobalFunc& gfunc) {
+    this->global = gfunc.global;
+    this->function = gfunc.function;
+  }
+};
+
+/*! \brief A wrapper structure for capturing all top-level definitions
+ * when parsing a module.
+ */
+struct Definitions {
+  /*! \brief The set of global functions. */
+  std::vector<GlobalFunc> funcs;
+  /*! \brief The set of type definitions. */
+  std::vector<TypeData> types;
+  // TODO(@jroesch): contain meta-table below
+};
+
+/*! \brief A structure representing the semantic versioning information
+ * for a Relay program.
+ */
+struct SemVer {
+  int major;
+  int minor;
+  int patch;
+};
+
+/*! \brief A reference to a "meta-expression".
+ *
+ * In the text format we allow referencing metadata which
+ * uses a compact serialization that proceeds the main
+ * program body.
+ *
+ * We can reference this table using an expression of
+ * the form `meta[Type][index]`.
+ *
+ * We must later resolve these references to actual in-memory
+ * AST nodes but this requires first parsing the full program
+ * then expanding these temporary AST nodes into their corresponding
+ * nodes.
+ *
+ * For example the nth large constant will be pretty-printed as meta[relay.Constant][n]
+ * with its compact binary serialization residing in the metadata section at the end
+ * of the program.
+ */
+class MetaRefExprNode : public TempExprNode {
+ public:
+  /*! \brief The type key of the meta expression. */
+  std::string type_key;
+  /*! \brief The index into the type key's table. */
+  uint64_t node_index;
+
+  void VisitAttrs(tvm::AttrVisitor* v) {}
+
+  // TODO(@jroesch): we probably will need to manually
+  // expand these with a pass.
+  Expr Realize() const final { return Expr(); }
+
+  static constexpr const char* _type_key = "relay.MetaRefExpr";
+  TVM_DECLARE_FINAL_OBJECT_INFO(MetaRefExprNode, TempExprNode);
+};
+
+class MetaRefExpr : public TempExpr {
+ public:
+  /*!
+   * \brief The constructor for MetaRefExpr
+   * \param type_key The type key of the object in the meta section.
+   * \param kind The index into that subfield.
+   */
+  TVM_DLL MetaRefExpr(std::string type_key, uint64_t node_index);
+
+  TVM_DEFINE_OBJECT_REF_METHODS(MetaRefExpr, TempExpr, MetaRefExprNode);
+};
+
+MetaRefExpr::MetaRefExpr(std::string type_key, uint64_t node_index) {
+  auto rnode = make_object<MetaRefExprNode>();
+  rnode->type_key = type_key;
+  rnode->node_index = node_index;
+  data_ = std::move(rnode);
+}
+
+/*! \brief A simple wrapper around a mapping from raw string names
+ * to a TVM variable, type variable or other binder type.
+ */
+template <typename T>
+struct Scope {
+  /*! \brief The internal map. */
+  std::unordered_map<std::string, T> name_map;
+};
+
+/*! \brief A stack of scopes.
+ *
+ * In order to properly handle scoping we must maintain a scope of stacks.
+ *
+ * A stack allows users to write programs which contain repeated variable
+ * names and to properly handle both nested scopes and removal of variables
+ * when they go out of scope.
+ *
+ * This is the classic approach to lexical scoping.
+ */
+template <typename T>
+class ScopeStack {
+ private:
+  std::vector<Scope<T>> scope_stack;
+
+ public:
+  /*! \brief Adds a variable binding to the current scope. */
+  void Add(const std::string& name, const T& value) {
+    if (!this->scope_stack.size()) {
+      LOG(FATAL) << "internal issue";
+    }
+    this->scope_stack.back().name_map.insert({name, value});
+  }
+
+  /*! \brief Looks up a variable name in the scope stack returning the matching variable
+   * in most recent scope. */
+  T Lookup(const std::string& name) {
+    for (auto scope = this->scope_stack.rbegin(); scope != this->scope_stack.rend(); ++scope) {
+      auto it = scope->name_map.find(name);
+      if (it != scope->name_map.end()) {
+        return it->second;
+      }
+    }
+    return T();
+  }
+
+  /*! \brief Adds a fresh scope. */
+  void PushStack() { this->scope_stack.push_back(Scope<T>()); }
+
+  /*! \brief Removes the most recent scope. */
+  void PopStack() { this->scope_stack.pop_back(); }
+};
+
+/*! \brief A table of interning strings as global function and type names. */
+template <typename T>
+struct InternTable {
+  /*! \brief The internal table mapping strings to a unique allocation. */
+  std::unordered_map<std::string, T> table;
+
+  /*! \brief Add the unique allocation. */
+  void Add(const std::string& name, const T& t) {
+    auto it = table.find(name);
+    if (it != table.end()) {
+      LOG(FATAL) << "duplicate name";
+    } else {
+      table.insert({name, t});
+    }
+  }
+
+  /*! \brief Return the unique allocation. */
+  Optional<T> Get(const std::string& name) {
+    auto it = table.find(name);
+    if (it != table.end()) {
+      return Optional<T>(it->second);
+    } else {
+      return Optional<T>();
+    }
+  }
+};
+
+/*! \brief The parser class is the main interface to the parser.
+ * the parser is not currently exposed beyond this .cc file.
+ *
+ * The parser is initialized with a diagnostic context, an
+ * operator table, and a token stream.
+ *
+ * The rest of the internal state is used to map the human readable
+ * form to in-memory IR representation.
+ *
+ * The main entry point to the parser are a set of parsing methods
+ * such as `ParseModule` and `ParseExpr`.
+ *
+ * As with traditional recursive descent parsers the parsing methods
+ * are factored recursively just as one would do with a formal language
+ * grammar.
+ *
+ * You can view a recursive descent parser as a human friendly way to specify
+ * a state machine, and thus this factoring is necessary as the 'state' of this
+ * machine is the combination of the current parsing method and the next token.
+ *
+ * Parsing proceeds by matching a token and then dispatching to the appropriate
+ * method to parse the next tokens in the stream.
+ *
+ * For example if we are parsing a type and encounter a "Tensor" token we switch
+ * into a mode for parsing `[`, a shape, a comma, a data type and then a ']'.

Review comment:
       ```suggestion
    * into a mode for parsing `[`, a shape, a comma, a data type and then a `]`.
   ```

##########
File path: src/parser/parser.cc
##########
@@ -0,0 +1,1384 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/*!
+ * \file parser.cc
+ * \brief A parser for TVM IR.
+ */
+#include <tvm/ir/module.h>
+#include <tvm/node/reflection.h>
+#include <tvm/relay/adt.h>
+#include <tvm/relay/expr.h>
+#include <tvm/relay/function.h>
+#include <tvm/runtime/object.h>
+#include <tvm/runtime/registry.h>
+
+#include <fstream>
+
+#include "./diagnostic.h"
+#include "./op_table.h"
+#include "./tokenizer.h"
+
+namespace tvm {
+namespace parser {
+
+using namespace relay;
+using Expr = relay::Expr;
+
+/*! \brief A wrapper structure for capturing the result of parsing
+ * a global definition *before* we add it to the IRModule.
+ *
+ * This enables the parser to parse everything in one pass before
+ * constructing the IRModule.
+ */
+struct GlobalFunc {
+  GlobalVar global;
+  Function function;
+  GlobalFunc() : global(), function() {}
+  GlobalFunc(GlobalVar global, Function function) : global(global), function(function) {}
+  GlobalFunc(const GlobalFunc& gfunc) {
+    this->global = gfunc.global;
+    this->function = gfunc.function;
+  }
+};
+
+/*! \brief A wrapper structure for capturing all top-level definitions
+ * when parsing a module.
+ */
+struct Definitions {
+  /*! \brief The set of global functions. */
+  std::vector<GlobalFunc> funcs;
+  /*! \brief The set of type definitions. */
+  std::vector<TypeData> types;
+  // TODO(@jroesch): contain meta-table below
+};
+
+/*! \brief A structure representing the semantic versioning information
+ * for a Relay program.
+ */
+struct SemVer {
+  int major;
+  int minor;
+  int patch;
+};
+
+/*! \brief A reference to a "meta-expression".
+ *
+ * In the text format we allow referencing metadata which
+ * uses a compact serialization that proceeds the main
+ * program body.
+ *
+ * We can reference this table using an expression of
+ * the form `meta[Type][index]`.
+ *
+ * We must later resolve these references to actual in-memory
+ * AST nodes but this requires first parsing the full program
+ * then expanding these temporary AST nodes into their corresponding
+ * nodes.
+ *
+ * For example the nth large constant will be pretty-printed as meta[relay.Constant][n]
+ * with its compact binary serialization residing in the metadata section at the end
+ * of the program.
+ */
+class MetaRefExprNode : public TempExprNode {
+ public:
+  /*! \brief The type key of the meta expression. */
+  std::string type_key;
+  /*! \brief The index into the type key's table. */
+  uint64_t node_index;
+
+  void VisitAttrs(tvm::AttrVisitor* v) {}
+
+  // TODO(@jroesch): we probably will need to manually
+  // expand these with a pass.
+  Expr Realize() const final { return Expr(); }
+
+  static constexpr const char* _type_key = "relay.MetaRefExpr";
+  TVM_DECLARE_FINAL_OBJECT_INFO(MetaRefExprNode, TempExprNode);
+};
+
+class MetaRefExpr : public TempExpr {
+ public:
+  /*!
+   * \brief The constructor for MetaRefExpr
+   * \param type_key The type key of the object in the meta section.
+   * \param kind The index into that subfield.
+   */
+  TVM_DLL MetaRefExpr(std::string type_key, uint64_t node_index);
+
+  TVM_DEFINE_OBJECT_REF_METHODS(MetaRefExpr, TempExpr, MetaRefExprNode);
+};
+
+MetaRefExpr::MetaRefExpr(std::string type_key, uint64_t node_index) {
+  auto rnode = make_object<MetaRefExprNode>();
+  rnode->type_key = type_key;
+  rnode->node_index = node_index;
+  data_ = std::move(rnode);
+}
+
+/*! \brief A simple wrapper around a mapping from raw string names
+ * to a TVM variable, type variable or other binder type.
+ */
+template <typename T>
+struct Scope {
+  /*! \brief The internal map. */
+  std::unordered_map<std::string, T> name_map;
+};
+
+/*! \brief A stack of scopes.
+ *
+ * In order to properly handle scoping we must maintain a scope of stacks.
+ *
+ * A stack allows users to write programs which contain repeated variable
+ * names and to properly handle both nested scopes and removal of variables
+ * when they go out of scope.
+ *
+ * This is the classic approach to lexical scoping.
+ */
+template <typename T>
+class ScopeStack {
+ private:
+  std::vector<Scope<T>> scope_stack;
+
+ public:
+  /*! \brief Adds a variable binding to the current scope. */
+  void Add(const std::string& name, const T& value) {
+    if (!this->scope_stack.size()) {
+      LOG(FATAL) << "internal issue";
+    }
+    this->scope_stack.back().name_map.insert({name, value});
+  }
+
+  /*! \brief Looks up a variable name in the scope stack returning the matching variable
+   * in most recent scope. */
+  T Lookup(const std::string& name) {
+    for (auto scope = this->scope_stack.rbegin(); scope != this->scope_stack.rend(); ++scope) {
+      auto it = scope->name_map.find(name);
+      if (it != scope->name_map.end()) {
+        return it->second;
+      }
+    }
+    return T();
+  }
+
+  /*! \brief Adds a fresh scope. */
+  void PushStack() { this->scope_stack.push_back(Scope<T>()); }
+
+  /*! \brief Removes the most recent scope. */
+  void PopStack() { this->scope_stack.pop_back(); }
+};
+
+/*! \brief A table of interning strings as global function and type names. */
+template <typename T>
+struct InternTable {
+  /*! \brief The internal table mapping strings to a unique allocation. */
+  std::unordered_map<std::string, T> table;
+
+  /*! \brief Add the unique allocation. */
+  void Add(const std::string& name, const T& t) {
+    auto it = table.find(name);
+    if (it != table.end()) {
+      LOG(FATAL) << "duplicate name";
+    } else {
+      table.insert({name, t});
+    }
+  }
+
+  /*! \brief Return the unique allocation. */
+  Optional<T> Get(const std::string& name) {
+    auto it = table.find(name);
+    if (it != table.end()) {
+      return Optional<T>(it->second);
+    } else {
+      return Optional<T>();
+    }
+  }
+};
+
+/*! \brief The parser class is the main interface to the parser.
+ * the parser is not currently exposed beyond this .cc file.
+ *
+ * The parser is initialized with a diagnostic context, an
+ * operator table, and a token stream.
+ *
+ * The rest of the internal state is used to map the human readable
+ * form to in-memory IR representation.
+ *
+ * The main entry point to the parser are a set of parsing methods
+ * such as `ParseModule` and `ParseExpr`.
+ *
+ * As with traditional recursive descent parsers the parsing methods
+ * are factored recursively just as one would do with a formal language
+ * grammar.
+ *
+ * You can view a recursive descent parser as a human friendly way to specify
+ * a state machine, and thus this factoring is necessary as the 'state' of this
+ * machine is the combination of the current parsing method and the next token.
+ *
+ * Parsing proceeds by matching a token and then dispatching to the appropriate
+ * method to parse the next tokens in the stream.
+ *
+ * For example if we are parsing a type and encounter a "Tensor" token we switch
+ * into a mode for parsing `[`, a shape, a comma, a data type and then a ']'.
+ *
+ * Certain matches like this are unambiguous and proceed in a straight line fashion
+ * once the initial token is found. Other parsing is more complex and requires some
+ * tricks to correctly parse.
+ *
+ * For example when we find a '(' in an expression context, it may be part of
+ * a tuple, the arguments to a call, or a parenthesized expression. The below code
+ * disambiguate these cases by factoring expression parsing into a series of methods
+ * which encode the parsing context the and thus how to interpret the parenthesis.
+ *
+ * For more information one should be able to read the code in order starting with
+ * `ParseModule` or `ParseExpr`.
+ */
+class Parser {
+ public:
+  /*! \brief The version that the parser is parsing. */
+  SemVer version;
+
+  /*! \brief The diagnostic context used for error reporting. */
+  DiagnosticContext diag_ctx;
+
+  /*! \brief The current position in the token stream. */
+  int pos;
+
+  /*! \brief The token stream for the parser. */
+  std::vector<Token> tokens;
+
+  /*! \brief The configured operator table. */
+  OperatorTable op_table;
+
+  /*! \brief Configure the whitespace mode, right now we ignore all whitespace. */
+  bool ignore_whitespace;
+
+  /*! \brief A global mapping for GlobalVar. */
+  InternTable<GlobalVar> global_names;
+
+  /*! \brief A global mapping for type definitions. */
+  InternTable<GlobalTypeVar> type_names;
+
+  /*! \brief A global mapping for constructor names. */
+  InternTable<Constructor> ctors;
+
+  /*! \brief A mapping from graph variable to expression, i.e., `%0 = expr`. */
+  std::unordered_map<int, Expr> graph_ctx;
+
+  /*! \brief The set of type scopes used for generics. */
+  ScopeStack<TypeVar> type_scopes;
+
+  /*! \brief The set of expression scopes used for lexical scope. */
+  ScopeStack<Var> expr_scopes;
+
+  Parser(std::vector<Token> tokens, OperatorTable op_table, Source source)
+      : diag_ctx(source), pos(0), tokens(tokens), op_table(op_table), ignore_whitespace(true) {}
+
+  /*! \brief Examine the next token in the stream, the current parser is configured to be
+   * whitespace insensitive so we will skip all whitespace or comment tokens. */
+  Token Peek() {
+    // For now we ignore all whitespace tokens and comments.
+    // We can tweak this behavior later to enable white space sensitivity in the parser.
+    while (pos < tokens.size() && ignore_whitespace &&
+           (tokens.at(pos)->token_type == TokenType::Whitespace ||
+            tokens.at(pos)->token_type == TokenType::Newline ||
+            tokens.at(pos)->token_type == TokenType::LineComment ||
+            tokens.at(pos)->token_type == TokenType::Comment)) {
+      pos++;
+    }
+
+    if (pos < tokens.size()) {
+      return Token(this->tokens.at(pos));
+    } else {
+      return Token::Null();
+    }
+  }
+
+  /*! \brief Lookahead by N tokens.
+   * \param n The number of tokens to lookahead.
+   * \return The Nth token.
+   */
+  Token Lookahead(int n) {
+    CHECK_GE(n, 1) << "lookahead is only valid when n >= 1";
+
+    // We intend to skip n - 1 tokens, then return the nth.
+    auto old_pos = pos;
+    for (int i = 0; i < n - 1; i++) {
+      Peek();
+      pos++;
+    }
+
+    auto tok = Peek();
+    pos = old_pos;
+    return tok;
+  }
+
+  /*! \brief Consume a token, this method is the lowest level way to consume a token
+   * and will not ignore white space or look ahead in anyway.
+   *
+   * /param token_type The token type to match.
+   */
+  void Consume(const TokenType& token_type) {
+    if (tokens[pos]->token_type != token_type) {
+      std::string message =
+          "expected a " + Pretty(token_type) + " found " + Pretty(Peek()->token_type);
+      this->diag_ctx.Emit({tokens[pos]->line, tokens[pos]->column, message});
+      this->diag_ctx.Render(std::cout);
+    }
+    pos++;
+  }
+
+  /*! Match a token in the stream, this will first invoke Peek, ignoring tokens such
+   * as whitespace or comments returning the first meaningful token.
+   *
+   * We then try and consume the requested token, this will trigger an error if the
+   * current token does not match the token_type.
+   */
+  Token Match(const TokenType& token_type) {
+    auto tok = Peek();
+    Consume(token_type);
+    return tok;
+  }
+
+  /*! Conditionally consume a token when it matches, this will never trigger an error
+   * as we guard against consuming the token before we do.
+   *
+   * Useful for matching optional tokens, effectively looksahead by one.
+   */
+  bool WhenMatch(const TokenType& token_type) {
+    if (Peek()->token_type == token_type) {
+      Consume(token_type);
+      return true;
+    } else {
+      return false;
+    }
+  }
+
+  /* \brief Add a graph binding to the parsing context
+   *
+   * For example if we parse %0 = add(...), map 0 -> add(...), etc.
+   */
+  void AddGraphBinding(const Token& token, const Expr& expr) {
+    auto graph_no = token.ToNumber();
+    this->graph_ctx.insert({graph_no, expr});
+  }
+
+  /* \brief Lookup a previously bound graph variable.
+   *
+   * Note: we take tokens in all lookup methods so that we
+   * that we can do error reporting based on token location.
+   */
+  Expr LookupGraphBinding(const Token& token) {
+    auto graph_no = token.ToNumber();
+    return this->graph_ctx.at(graph_no);
+  }
+
+  /*! \brief Bind a local variable in the expression scope.
+   *
+   * "x" -> Var("x"), these are needed to map from the raw string names
+   * to unique variable nodes.
+   */
+  Var BindVar(const std::string& name, const relay::Type& type_annotation) {
+    auto var = Var(name, type_annotation);
+    this->expr_scopes.Add(name, var);
+    return var;
+  }
+
+  /*! \brief Bind a type variable in the type scope.
+   *
+   * "A" -> TypeVar("A", ...), these are needed to map from raw string names
+   * to unique type variable nodes.
+   */
+  TypeVar BindTypeVar(const std::string& name, const TypeKind type_kind) {
+    auto type_var = TypeVar(name, type_kind);
+    this->type_scopes.Add(name, type_var);
+    return type_var;
+  }
+
+  /*! \brief Lookup a variable in the expression scope.
+   *
+   * Note: all lookup methods take tokens intentionally for error reporting information.
+   */
+  Var LookupLocal(const Token& local) {
+    auto var = this->expr_scopes.Lookup(local.ToString());
+    if (!var.defined()) {
+      diag_ctx.Emit(
+          {local->line, local->column, "this local variable has not been previously declared"});
+    }
+    return var;
+  }
+
+  /*! \brief Lookup a variable in the type scope.
+   *
+   * Note: all lookup methods take tokens intentionally for error reporting information.
+   */
+  TypeVar LookupTypeVar(const Token& ident) {
+    auto var = this->type_scopes.Lookup(ident.ToString());
+    if (!var.defined()) {
+      diag_ctx.Emit(
+          {ident->line, ident->column,
+           "this type variable has not been previously declared anywhere, perhaps a typo?"});
+    }
+    return var;
+  }
+
+  /*! \brief Add an expression scope to the scope stack. */
+  void PushScope() { this->expr_scopes.PushStack(); }
+
+  /*! \brief Remove N expression scopes from the scope stack. */
+  void PopScopes(int n) {
+    for (int i = 0; i < n; i++) {
+      this->expr_scopes.PopStack();
+    }
+  }
+
+  /*! \brief Add an type scope to the scope stack. */
+  void PushTypeScope() { this->type_scopes.PushStack(); }
+
+  /*! \brief Remove N type scopes from the scope stack. */
+  void PopTypeScopes(int n) {
+    for (int i = 0; i < n; i++) {
+      this->type_scopes.PopStack();
+    }
+  }
+
+  /*! \brief Convert a numeric token to an NDArray for embedding into the Relay program. */
+  NDArray NumberToNDArray(const Token& token) {
+    if (token->token_type == TokenType::Integer) {
+      DLContext ctx({.device_type = DLDeviceType::kDLCPU, .device_id = 0});
+      auto dtype = String2DLDataType("int32");
+      auto data = NDArray::Empty({}, dtype, ctx);
+      auto array = reinterpret_cast<int32_t*>(data->data);
+      // revisit this, literal node issue.
+      int64_t value = Downcast<tvm::Integer>(token->data);
+      array[0] = (int32_t)value;
+      return data;
+    } else if (token->token_type == TokenType::Float) {
+      DLContext ctx({.device_type = DLDeviceType::kDLCPU, .device_id = 0});
+      auto dtype = String2DLDataType("float32");
+      auto data = NDArray::Empty({}, dtype, ctx);
+      auto array = reinterpret_cast<float*>(data->data);
+      // revisit this, literal node issue.
+      float value = Downcast<tvm::FloatImm>(token->data)->value;
+      array[0] = value;
+      return data;
+    } else {
+      LOG(FATAL) << "internal error: should only call this function on numeric tokens";
+      return NDArray();
+    }
+  }
+
+  /*! \brief Convert a boolean value to an NDArray for embedding into the Relay program. */
+  NDArray BooleanToNDarray(bool value) {
+    DLContext ctx({.device_type = DLDeviceType::kDLCPU, .device_id = 0});
+    auto dtype = String2DLDataType("bool");
+    auto data = NDArray::Empty({}, dtype, ctx);
+    auto array = reinterpret_cast<bool*>(data->data);
+    array[0] = value;
+    return data;
+  }

Review comment:
       does it make sense to refactor this?

##########
File path: src/parser/parser.cc
##########
@@ -0,0 +1,1384 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/*!
+ * \file parser.cc
+ * \brief A parser for TVM IR.
+ */
+#include <tvm/ir/module.h>
+#include <tvm/node/reflection.h>
+#include <tvm/relay/adt.h>
+#include <tvm/relay/expr.h>
+#include <tvm/relay/function.h>
+#include <tvm/runtime/object.h>
+#include <tvm/runtime/registry.h>
+
+#include <fstream>
+
+#include "./diagnostic.h"
+#include "./op_table.h"
+#include "./tokenizer.h"
+
+namespace tvm {
+namespace parser {
+
+using namespace relay;
+using Expr = relay::Expr;
+
+/*! \brief A wrapper structure for capturing the result of parsing
+ * a global definition *before* we add it to the IRModule.
+ *
+ * This enables the parser to parse everything in one pass before
+ * constructing the IRModule.
+ */
+struct GlobalFunc {
+  GlobalVar global;
+  Function function;
+  GlobalFunc() : global(), function() {}
+  GlobalFunc(GlobalVar global, Function function) : global(global), function(function) {}
+  GlobalFunc(const GlobalFunc& gfunc) {
+    this->global = gfunc.global;
+    this->function = gfunc.function;
+  }
+};
+
+/*! \brief A wrapper structure for capturing all top-level definitions
+ * when parsing a module.
+ */
+struct Definitions {
+  /*! \brief The set of global functions. */
+  std::vector<GlobalFunc> funcs;
+  /*! \brief The set of type definitions. */
+  std::vector<TypeData> types;
+  // TODO(@jroesch): contain meta-table below
+};
+
+/*! \brief A structure representing the semantic versioning information
+ * for a Relay program.
+ */
+struct SemVer {
+  int major;
+  int minor;
+  int patch;
+};
+
+/*! \brief A reference to a "meta-expression".
+ *
+ * In the text format we allow referencing metadata which
+ * uses a compact serialization that proceeds the main
+ * program body.
+ *
+ * We can reference this table using an expression of
+ * the form `meta[Type][index]`.
+ *
+ * We must later resolve these references to actual in-memory
+ * AST nodes but this requires first parsing the full program
+ * then expanding these temporary AST nodes into their corresponding
+ * nodes.
+ *
+ * For example the nth large constant will be pretty-printed as meta[relay.Constant][n]
+ * with its compact binary serialization residing in the metadata section at the end
+ * of the program.
+ */
+class MetaRefExprNode : public TempExprNode {
+ public:
+  /*! \brief The type key of the meta expression. */
+  std::string type_key;
+  /*! \brief The index into the type key's table. */
+  uint64_t node_index;
+
+  void VisitAttrs(tvm::AttrVisitor* v) {}
+
+  // TODO(@jroesch): we probably will need to manually
+  // expand these with a pass.
+  Expr Realize() const final { return Expr(); }
+
+  static constexpr const char* _type_key = "relay.MetaRefExpr";
+  TVM_DECLARE_FINAL_OBJECT_INFO(MetaRefExprNode, TempExprNode);
+};
+
+class MetaRefExpr : public TempExpr {
+ public:
+  /*!
+   * \brief The constructor for MetaRefExpr
+   * \param type_key The type key of the object in the meta section.
+   * \param kind The index into that subfield.
+   */
+  TVM_DLL MetaRefExpr(std::string type_key, uint64_t node_index);
+
+  TVM_DEFINE_OBJECT_REF_METHODS(MetaRefExpr, TempExpr, MetaRefExprNode);
+};
+
+MetaRefExpr::MetaRefExpr(std::string type_key, uint64_t node_index) {
+  auto rnode = make_object<MetaRefExprNode>();
+  rnode->type_key = type_key;
+  rnode->node_index = node_index;
+  data_ = std::move(rnode);
+}
+
+/*! \brief A simple wrapper around a mapping from raw string names
+ * to a TVM variable, type variable or other binder type.
+ */
+template <typename T>
+struct Scope {
+  /*! \brief The internal map. */
+  std::unordered_map<std::string, T> name_map;
+};
+
+/*! \brief A stack of scopes.
+ *
+ * In order to properly handle scoping we must maintain a scope of stacks.
+ *
+ * A stack allows users to write programs which contain repeated variable
+ * names and to properly handle both nested scopes and removal of variables
+ * when they go out of scope.
+ *
+ * This is the classic approach to lexical scoping.
+ */
+template <typename T>
+class ScopeStack {
+ private:
+  std::vector<Scope<T>> scope_stack;
+
+ public:
+  /*! \brief Adds a variable binding to the current scope. */
+  void Add(const std::string& name, const T& value) {
+    if (!this->scope_stack.size()) {
+      LOG(FATAL) << "internal issue";
+    }
+    this->scope_stack.back().name_map.insert({name, value});
+  }
+
+  /*! \brief Looks up a variable name in the scope stack returning the matching variable
+   * in most recent scope. */
+  T Lookup(const std::string& name) {
+    for (auto scope = this->scope_stack.rbegin(); scope != this->scope_stack.rend(); ++scope) {
+      auto it = scope->name_map.find(name);
+      if (it != scope->name_map.end()) {
+        return it->second;
+      }
+    }
+    return T();
+  }
+
+  /*! \brief Adds a fresh scope. */
+  void PushStack() { this->scope_stack.push_back(Scope<T>()); }
+
+  /*! \brief Removes the most recent scope. */
+  void PopStack() { this->scope_stack.pop_back(); }
+};
+
+/*! \brief A table of interning strings as global function and type names. */
+template <typename T>
+struct InternTable {
+  /*! \brief The internal table mapping strings to a unique allocation. */
+  std::unordered_map<std::string, T> table;
+
+  /*! \brief Add the unique allocation. */
+  void Add(const std::string& name, const T& t) {
+    auto it = table.find(name);
+    if (it != table.end()) {
+      LOG(FATAL) << "duplicate name";
+    } else {
+      table.insert({name, t});
+    }
+  }
+
+  /*! \brief Return the unique allocation. */
+  Optional<T> Get(const std::string& name) {
+    auto it = table.find(name);
+    if (it != table.end()) {
+      return Optional<T>(it->second);
+    } else {
+      return Optional<T>();
+    }
+  }
+};
+
+/*! \brief The parser class is the main interface to the parser.
+ * the parser is not currently exposed beyond this .cc file.
+ *
+ * The parser is initialized with a diagnostic context, an
+ * operator table, and a token stream.
+ *
+ * The rest of the internal state is used to map the human readable
+ * form to in-memory IR representation.
+ *
+ * The main entry point to the parser are a set of parsing methods
+ * such as `ParseModule` and `ParseExpr`.
+ *
+ * As with traditional recursive descent parsers the parsing methods
+ * are factored recursively just as one would do with a formal language
+ * grammar.
+ *
+ * You can view a recursive descent parser as a human friendly way to specify
+ * a state machine, and thus this factoring is necessary as the 'state' of this
+ * machine is the combination of the current parsing method and the next token.
+ *
+ * Parsing proceeds by matching a token and then dispatching to the appropriate
+ * method to parse the next tokens in the stream.
+ *
+ * For example if we are parsing a type and encounter a "Tensor" token we switch
+ * into a mode for parsing `[`, a shape, a comma, a data type and then a ']'.
+ *
+ * Certain matches like this are unambiguous and proceed in a straight line fashion
+ * once the initial token is found. Other parsing is more complex and requires some
+ * tricks to correctly parse.
+ *
+ * For example when we find a '(' in an expression context, it may be part of
+ * a tuple, the arguments to a call, or a parenthesized expression. The below code
+ * disambiguate these cases by factoring expression parsing into a series of methods
+ * which encode the parsing context the and thus how to interpret the parenthesis.
+ *
+ * For more information one should be able to read the code in order starting with
+ * `ParseModule` or `ParseExpr`.
+ */
+class Parser {
+ public:
+  /*! \brief The version that the parser is parsing. */
+  SemVer version;
+
+  /*! \brief The diagnostic context used for error reporting. */
+  DiagnosticContext diag_ctx;
+
+  /*! \brief The current position in the token stream. */
+  int pos;
+
+  /*! \brief The token stream for the parser. */
+  std::vector<Token> tokens;
+
+  /*! \brief The configured operator table. */
+  OperatorTable op_table;
+
+  /*! \brief Configure the whitespace mode, right now we ignore all whitespace. */
+  bool ignore_whitespace;
+
+  /*! \brief A global mapping for GlobalVar. */
+  InternTable<GlobalVar> global_names;
+
+  /*! \brief A global mapping for type definitions. */
+  InternTable<GlobalTypeVar> type_names;
+
+  /*! \brief A global mapping for constructor names. */
+  InternTable<Constructor> ctors;
+
+  /*! \brief A mapping from graph variable to expression, i.e., `%0 = expr`. */
+  std::unordered_map<int, Expr> graph_ctx;
+
+  /*! \brief The set of type scopes used for generics. */
+  ScopeStack<TypeVar> type_scopes;
+
+  /*! \brief The set of expression scopes used for lexical scope. */
+  ScopeStack<Var> expr_scopes;
+
+  Parser(std::vector<Token> tokens, OperatorTable op_table, Source source)
+      : diag_ctx(source), pos(0), tokens(tokens), op_table(op_table), ignore_whitespace(true) {}
+
+  /*! \brief Examine the next token in the stream, the current parser is configured to be
+   * whitespace insensitive so we will skip all whitespace or comment tokens. */
+  Token Peek() {
+    // For now we ignore all whitespace tokens and comments.
+    // We can tweak this behavior later to enable white space sensitivity in the parser.
+    while (pos < tokens.size() && ignore_whitespace &&
+           (tokens.at(pos)->token_type == TokenType::Whitespace ||
+            tokens.at(pos)->token_type == TokenType::Newline ||
+            tokens.at(pos)->token_type == TokenType::LineComment ||
+            tokens.at(pos)->token_type == TokenType::Comment)) {
+      pos++;
+    }
+
+    if (pos < tokens.size()) {
+      return Token(this->tokens.at(pos));
+    } else {
+      return Token::Null();
+    }
+  }
+
+  /*! \brief Lookahead by N tokens.
+   * \param n The number of tokens to lookahead.
+   * \return The Nth token.
+   */
+  Token Lookahead(int n) {
+    CHECK_GE(n, 1) << "lookahead is only valid when n >= 1";
+
+    // We intend to skip n - 1 tokens, then return the nth.
+    auto old_pos = pos;
+    for (int i = 0; i < n - 1; i++) {
+      Peek();
+      pos++;
+    }
+
+    auto tok = Peek();
+    pos = old_pos;
+    return tok;
+  }
+
+  /*! \brief Consume a token, this method is the lowest level way to consume a token
+   * and will not ignore white space or look ahead in anyway.
+   *
+   * /param token_type The token type to match.
+   */
+  void Consume(const TokenType& token_type) {
+    if (tokens[pos]->token_type != token_type) {
+      std::string message =
+          "expected a " + Pretty(token_type) + " found " + Pretty(Peek()->token_type);
+      this->diag_ctx.Emit({tokens[pos]->line, tokens[pos]->column, message});
+      this->diag_ctx.Render(std::cout);
+    }
+    pos++;
+  }
+
+  /*! Match a token in the stream, this will first invoke Peek, ignoring tokens such
+   * as whitespace or comments returning the first meaningful token.
+   *
+   * We then try and consume the requested token, this will trigger an error if the
+   * current token does not match the token_type.
+   */
+  Token Match(const TokenType& token_type) {
+    auto tok = Peek();
+    Consume(token_type);
+    return tok;
+  }
+
+  /*! Conditionally consume a token when it matches, this will never trigger an error
+   * as we guard against consuming the token before we do.
+   *
+   * Useful for matching optional tokens, effectively looksahead by one.
+   */
+  bool WhenMatch(const TokenType& token_type) {
+    if (Peek()->token_type == token_type) {
+      Consume(token_type);
+      return true;
+    } else {
+      return false;
+    }
+  }
+
+  /* \brief Add a graph binding to the parsing context
+   *
+   * For example if we parse %0 = add(...), map 0 -> add(...), etc.
+   */
+  void AddGraphBinding(const Token& token, const Expr& expr) {
+    auto graph_no = token.ToNumber();
+    this->graph_ctx.insert({graph_no, expr});
+  }
+
+  /* \brief Lookup a previously bound graph variable.
+   *
+   * Note: we take tokens in all lookup methods so that we
+   * that we can do error reporting based on token location.
+   */
+  Expr LookupGraphBinding(const Token& token) {
+    auto graph_no = token.ToNumber();
+    return this->graph_ctx.at(graph_no);
+  }
+
+  /*! \brief Bind a local variable in the expression scope.
+   *
+   * "x" -> Var("x"), these are needed to map from the raw string names
+   * to unique variable nodes.
+   */
+  Var BindVar(const std::string& name, const relay::Type& type_annotation) {
+    auto var = Var(name, type_annotation);
+    this->expr_scopes.Add(name, var);
+    return var;
+  }
+
+  /*! \brief Bind a type variable in the type scope.
+   *
+   * "A" -> TypeVar("A", ...), these are needed to map from raw string names
+   * to unique type variable nodes.
+   */
+  TypeVar BindTypeVar(const std::string& name, const TypeKind type_kind) {
+    auto type_var = TypeVar(name, type_kind);
+    this->type_scopes.Add(name, type_var);
+    return type_var;
+  }
+
+  /*! \brief Lookup a variable in the expression scope.
+   *
+   * Note: all lookup methods take tokens intentionally for error reporting information.
+   */
+  Var LookupLocal(const Token& local) {
+    auto var = this->expr_scopes.Lookup(local.ToString());
+    if (!var.defined()) {
+      diag_ctx.Emit(
+          {local->line, local->column, "this local variable has not been previously declared"});
+    }
+    return var;
+  }
+
+  /*! \brief Lookup a variable in the type scope.
+   *
+   * Note: all lookup methods take tokens intentionally for error reporting information.
+   */
+  TypeVar LookupTypeVar(const Token& ident) {
+    auto var = this->type_scopes.Lookup(ident.ToString());
+    if (!var.defined()) {
+      diag_ctx.Emit(
+          {ident->line, ident->column,
+           "this type variable has not been previously declared anywhere, perhaps a typo?"});
+    }
+    return var;
+  }
+
+  /*! \brief Add an expression scope to the scope stack. */
+  void PushScope() { this->expr_scopes.PushStack(); }
+
+  /*! \brief Remove N expression scopes from the scope stack. */
+  void PopScopes(int n) {
+    for (int i = 0; i < n; i++) {
+      this->expr_scopes.PopStack();
+    }
+  }
+
+  /*! \brief Add an type scope to the scope stack. */
+  void PushTypeScope() { this->type_scopes.PushStack(); }
+
+  /*! \brief Remove N type scopes from the scope stack. */
+  void PopTypeScopes(int n) {
+    for (int i = 0; i < n; i++) {
+      this->type_scopes.PopStack();
+    }
+  }
+
+  /*! \brief Convert a numeric token to an NDArray for embedding into the Relay program. */
+  NDArray NumberToNDArray(const Token& token) {
+    if (token->token_type == TokenType::Integer) {
+      DLContext ctx({.device_type = DLDeviceType::kDLCPU, .device_id = 0});
+      auto dtype = String2DLDataType("int32");
+      auto data = NDArray::Empty({}, dtype, ctx);
+      auto array = reinterpret_cast<int32_t*>(data->data);
+      // revisit this, literal node issue.
+      int64_t value = Downcast<tvm::Integer>(token->data);
+      array[0] = (int32_t)value;
+      return data;
+    } else if (token->token_type == TokenType::Float) {
+      DLContext ctx({.device_type = DLDeviceType::kDLCPU, .device_id = 0});
+      auto dtype = String2DLDataType("float32");
+      auto data = NDArray::Empty({}, dtype, ctx);
+      auto array = reinterpret_cast<float*>(data->data);
+      // revisit this, literal node issue.
+      float value = Downcast<tvm::FloatImm>(token->data)->value;
+      array[0] = value;
+      return data;
+    } else {
+      LOG(FATAL) << "internal error: should only call this function on numeric tokens";
+      return NDArray();
+    }
+  }
+
+  /*! \brief Convert a boolean value to an NDArray for embedding into the Relay program. */
+  NDArray BooleanToNDarray(bool value) {
+    DLContext ctx({.device_type = DLDeviceType::kDLCPU, .device_id = 0});
+    auto dtype = String2DLDataType("bool");
+    auto data = NDArray::Empty({}, dtype, ctx);
+    auto array = reinterpret_cast<bool*>(data->data);
+    array[0] = value;
+    return data;
+  }
+
+  [[noreturn]] void ParseError(const Token& token, const std::string& msg) {
+    throw std::runtime_error(msg);
+  }
+
+  /*! \brief A parsing helper for a bracketed expression <start> <parser> <stop>. */
+  template <typename R>
+  R Bracket(TokenType open, TokenType close, std::function<R()> parser) {
+    Match(open);
+    R result = parser();
+    Match(close);
+    return result;
+  }
+
+  /*! \brief Parse `(` parser() `)`. */
+  template <typename R>
+  R Parens(std::function<R()> parser) {
+    return Bracket(TokenType::OpenParen, TokenType::CloseParen, parser);
+  }
+
+  /*! \brief Parse `{` parser() `}`. */
+  template <typename R>
+  R Block(std::function<R()> parser) {
+    return Bracket(TokenType::LCurly, TokenType::RCurly, parser);
+  }
+
+  /*! \brief Parses a sequence beginning with a start token, seperated by a seperator token, and
+   * ending with a stop token.
+   *
+   * The simple form being <start> (<parse()> <seperator>)* <stop>.
+   *
+   * This also provides a fourth argument which is allowed to run when the sequence which matches
+   * the inner sequence can not proceed.
+   *
+   * This is useful for parsing things like attributes which don't match the standard expression
+   * parsers but are contained within the stop token.
+   */
+  template <typename T>
+  Array<T> ParseSequence(TokenType start, TokenType sep, TokenType stop, std::function<T()> parse,
+                         std::function<void()> before_stop = nullptr) {
+    Match(start);
+    if (WhenMatch(stop)) {
+      return Array<T>();
+    } else {
+      auto data = parse();
+      Array<T> elements = {data};
+
+      // parse '(' expr ')'
+      // if we are at the end invoke leftover parser
+      if (Peek()->token_type == stop && before_stop) {
+        before_stop();
+      }
+      if (WhenMatch(stop)) {
+        return elements;
+        // parse '( expr ',' * ')'
+      } else if (WhenMatch(sep)) {
+        // if we are at the end invoke leftover parser
+        if (Peek()->token_type == stop && before_stop) {
+          before_stop();
+        }
+        while (true) {
+          if (WhenMatch(stop)) {
+            break;
+          } else {
+            auto data = parse();
+            WhenMatch(sep);
+            elements.push_back(data);
+          }
+        }
+        return elements;
+      } else {
+        LOG(FATAL) << "issue";
+        return Array<T>(nullptr);
+      }
+    }
+  }
+
+  /*! \brief Parse a full IRModule. */
+  IRModule ParseModule() {
+    // Parse the semver header at the top of the module.
+    this->version = ParseSemVer();
+    // Parse the definitions.
+    auto defs = ParseDefinitions();
+    // Parse the metadata section at the end.
+    auto metadata = ParseMetadata();
+    Match(TokenType::EndOfFile);
+    Map<tvm::GlobalVar, BaseFunc> funcs;
+    Map<tvm::GlobalTypeVar, TypeData> types;
+
+    for (auto type_def : defs.types) {
+      types.Set(type_def->header, type_def);
+    }
+
+    auto mod = IRModule({}, types);
+
+    for (auto func : defs.funcs) {
+      mod->Add(func.global, func.function);
+    }
+
+    return mod;
+  }
+
+  /*! \brief Parse the semantic versioning header. */
+  SemVer ParseSemVer() {
+    // TODO(@jroesch): convert semver to module level attribute.
+    auto id = Peek();
+    if (id->token_type == TokenType::Identifier && id.ToString() == "v0") {
+      auto id = Match(TokenType::Identifier);
+      Consume(TokenType::Period);
+      // CHECK_EQ(minor_and_patch)
+      Consume(TokenType::Float);
+    }
+    // For now we only support current version.
+    return SemVer{.major = 0, .minor = 0, .patch = 4};
+  }

Review comment:
       even if we only support the current version, we should still validate the given version matches that, right?

##########
File path: src/parser/parser.cc
##########
@@ -0,0 +1,1384 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/*!
+ * \file parser.cc
+ * \brief A parser for TVM IR.
+ */
+#include <tvm/ir/module.h>
+#include <tvm/node/reflection.h>
+#include <tvm/relay/adt.h>
+#include <tvm/relay/expr.h>
+#include <tvm/relay/function.h>
+#include <tvm/runtime/object.h>
+#include <tvm/runtime/registry.h>
+
+#include <fstream>
+
+#include "./diagnostic.h"
+#include "./op_table.h"
+#include "./tokenizer.h"
+
+namespace tvm {
+namespace parser {
+
+using namespace relay;
+using Expr = relay::Expr;
+
+/*! \brief A wrapper structure for capturing the result of parsing
+ * a global definition *before* we add it to the IRModule.
+ *
+ * This enables the parser to parse everything in one pass before
+ * constructing the IRModule.
+ */
+struct GlobalFunc {
+  GlobalVar global;
+  Function function;
+  GlobalFunc() : global(), function() {}
+  GlobalFunc(GlobalVar global, Function function) : global(global), function(function) {}
+  GlobalFunc(const GlobalFunc& gfunc) {
+    this->global = gfunc.global;
+    this->function = gfunc.function;
+  }
+};
+
+/*! \brief A wrapper structure for capturing all top-level definitions
+ * when parsing a module.
+ */
+struct Definitions {
+  /*! \brief The set of global functions. */
+  std::vector<GlobalFunc> funcs;
+  /*! \brief The set of type definitions. */
+  std::vector<TypeData> types;
+  // TODO(@jroesch): contain meta-table below
+};
+
+/*! \brief A structure representing the semantic versioning information
+ * for a Relay program.
+ */
+struct SemVer {
+  int major;
+  int minor;
+  int patch;
+};
+
+/*! \brief A reference to a "meta-expression".
+ *
+ * In the text format we allow referencing metadata which
+ * uses a compact serialization that proceeds the main
+ * program body.
+ *
+ * We can reference this table using an expression of
+ * the form `meta[Type][index]`.
+ *
+ * We must later resolve these references to actual in-memory
+ * AST nodes but this requires first parsing the full program
+ * then expanding these temporary AST nodes into their corresponding
+ * nodes.
+ *
+ * For example the nth large constant will be pretty-printed as meta[relay.Constant][n]
+ * with its compact binary serialization residing in the metadata section at the end
+ * of the program.
+ */
+class MetaRefExprNode : public TempExprNode {
+ public:
+  /*! \brief The type key of the meta expression. */
+  std::string type_key;
+  /*! \brief The index into the type key's table. */
+  uint64_t node_index;
+
+  void VisitAttrs(tvm::AttrVisitor* v) {}
+
+  // TODO(@jroesch): we probably will need to manually
+  // expand these with a pass.
+  Expr Realize() const final { return Expr(); }
+
+  static constexpr const char* _type_key = "relay.MetaRefExpr";
+  TVM_DECLARE_FINAL_OBJECT_INFO(MetaRefExprNode, TempExprNode);
+};
+
+class MetaRefExpr : public TempExpr {
+ public:
+  /*!
+   * \brief The constructor for MetaRefExpr
+   * \param type_key The type key of the object in the meta section.
+   * \param kind The index into that subfield.
+   */
+  TVM_DLL MetaRefExpr(std::string type_key, uint64_t node_index);
+
+  TVM_DEFINE_OBJECT_REF_METHODS(MetaRefExpr, TempExpr, MetaRefExprNode);
+};
+
+MetaRefExpr::MetaRefExpr(std::string type_key, uint64_t node_index) {
+  auto rnode = make_object<MetaRefExprNode>();
+  rnode->type_key = type_key;
+  rnode->node_index = node_index;
+  data_ = std::move(rnode);
+}
+
+/*! \brief A simple wrapper around a mapping from raw string names
+ * to a TVM variable, type variable or other binder type.
+ */
+template <typename T>
+struct Scope {
+  /*! \brief The internal map. */
+  std::unordered_map<std::string, T> name_map;
+};
+
+/*! \brief A stack of scopes.
+ *
+ * In order to properly handle scoping we must maintain a scope of stacks.
+ *
+ * A stack allows users to write programs which contain repeated variable
+ * names and to properly handle both nested scopes and removal of variables
+ * when they go out of scope.
+ *
+ * This is the classic approach to lexical scoping.
+ */
+template <typename T>
+class ScopeStack {
+ private:
+  std::vector<Scope<T>> scope_stack;
+
+ public:
+  /*! \brief Adds a variable binding to the current scope. */
+  void Add(const std::string& name, const T& value) {
+    if (!this->scope_stack.size()) {
+      LOG(FATAL) << "internal issue";
+    }
+    this->scope_stack.back().name_map.insert({name, value});
+  }
+
+  /*! \brief Looks up a variable name in the scope stack returning the matching variable
+   * in most recent scope. */
+  T Lookup(const std::string& name) {
+    for (auto scope = this->scope_stack.rbegin(); scope != this->scope_stack.rend(); ++scope) {
+      auto it = scope->name_map.find(name);
+      if (it != scope->name_map.end()) {
+        return it->second;
+      }
+    }
+    return T();
+  }
+
+  /*! \brief Adds a fresh scope. */
+  void PushStack() { this->scope_stack.push_back(Scope<T>()); }
+
+  /*! \brief Removes the most recent scope. */
+  void PopStack() { this->scope_stack.pop_back(); }
+};
+
+/*! \brief A table of interning strings as global function and type names. */
+template <typename T>
+struct InternTable {
+  /*! \brief The internal table mapping strings to a unique allocation. */
+  std::unordered_map<std::string, T> table;
+
+  /*! \brief Add the unique allocation. */
+  void Add(const std::string& name, const T& t) {
+    auto it = table.find(name);
+    if (it != table.end()) {
+      LOG(FATAL) << "duplicate name";
+    } else {
+      table.insert({name, t});
+    }
+  }
+
+  /*! \brief Return the unique allocation. */
+  Optional<T> Get(const std::string& name) {
+    auto it = table.find(name);
+    if (it != table.end()) {
+      return Optional<T>(it->second);
+    } else {
+      return Optional<T>();
+    }
+  }
+};
+
+/*! \brief The parser class is the main interface to the parser.
+ * the parser is not currently exposed beyond this .cc file.
+ *
+ * The parser is initialized with a diagnostic context, an
+ * operator table, and a token stream.
+ *
+ * The rest of the internal state is used to map the human readable
+ * form to in-memory IR representation.
+ *
+ * The main entry point to the parser are a set of parsing methods
+ * such as `ParseModule` and `ParseExpr`.
+ *
+ * As with traditional recursive descent parsers the parsing methods
+ * are factored recursively just as one would do with a formal language
+ * grammar.
+ *
+ * You can view a recursive descent parser as a human friendly way to specify
+ * a state machine, and thus this factoring is necessary as the 'state' of this
+ * machine is the combination of the current parsing method and the next token.
+ *
+ * Parsing proceeds by matching a token and then dispatching to the appropriate
+ * method to parse the next tokens in the stream.
+ *
+ * For example if we are parsing a type and encounter a "Tensor" token we switch
+ * into a mode for parsing `[`, a shape, a comma, a data type and then a ']'.
+ *
+ * Certain matches like this are unambiguous and proceed in a straight line fashion
+ * once the initial token is found. Other parsing is more complex and requires some
+ * tricks to correctly parse.
+ *
+ * For example when we find a '(' in an expression context, it may be part of
+ * a tuple, the arguments to a call, or a parenthesized expression. The below code
+ * disambiguate these cases by factoring expression parsing into a series of methods
+ * which encode the parsing context the and thus how to interpret the parenthesis.
+ *
+ * For more information one should be able to read the code in order starting with
+ * `ParseModule` or `ParseExpr`.
+ */
+class Parser {
+ public:
+  /*! \brief The version that the parser is parsing. */
+  SemVer version;
+
+  /*! \brief The diagnostic context used for error reporting. */
+  DiagnosticContext diag_ctx;
+
+  /*! \brief The current position in the token stream. */
+  int pos;
+
+  /*! \brief The token stream for the parser. */
+  std::vector<Token> tokens;
+
+  /*! \brief The configured operator table. */
+  OperatorTable op_table;
+
+  /*! \brief Configure the whitespace mode, right now we ignore all whitespace. */
+  bool ignore_whitespace;
+
+  /*! \brief A global mapping for GlobalVar. */
+  InternTable<GlobalVar> global_names;
+
+  /*! \brief A global mapping for type definitions. */
+  InternTable<GlobalTypeVar> type_names;
+
+  /*! \brief A global mapping for constructor names. */
+  InternTable<Constructor> ctors;
+
+  /*! \brief A mapping from graph variable to expression, i.e., `%0 = expr`. */
+  std::unordered_map<int, Expr> graph_ctx;
+
+  /*! \brief The set of type scopes used for generics. */
+  ScopeStack<TypeVar> type_scopes;
+
+  /*! \brief The set of expression scopes used for lexical scope. */
+  ScopeStack<Var> expr_scopes;
+
+  Parser(std::vector<Token> tokens, OperatorTable op_table, Source source)
+      : diag_ctx(source), pos(0), tokens(tokens), op_table(op_table), ignore_whitespace(true) {}
+
+  /*! \brief Examine the next token in the stream, the current parser is configured to be
+   * whitespace insensitive so we will skip all whitespace or comment tokens. */
+  Token Peek() {
+    // For now we ignore all whitespace tokens and comments.
+    // We can tweak this behavior later to enable white space sensitivity in the parser.
+    while (pos < tokens.size() && ignore_whitespace &&
+           (tokens.at(pos)->token_type == TokenType::Whitespace ||
+            tokens.at(pos)->token_type == TokenType::Newline ||
+            tokens.at(pos)->token_type == TokenType::LineComment ||
+            tokens.at(pos)->token_type == TokenType::Comment)) {
+      pos++;
+    }
+
+    if (pos < tokens.size()) {
+      return Token(this->tokens.at(pos));
+    } else {
+      return Token::Null();
+    }
+  }
+
+  /*! \brief Lookahead by N tokens.
+   * \param n The number of tokens to lookahead.
+   * \return The Nth token.
+   */
+  Token Lookahead(int n) {
+    CHECK_GE(n, 1) << "lookahead is only valid when n >= 1";
+
+    // We intend to skip n - 1 tokens, then return the nth.
+    auto old_pos = pos;
+    for (int i = 0; i < n - 1; i++) {
+      Peek();
+      pos++;
+    }
+
+    auto tok = Peek();
+    pos = old_pos;
+    return tok;
+  }
+
+  /*! \brief Consume a token, this method is the lowest level way to consume a token
+   * and will not ignore white space or look ahead in anyway.
+   *
+   * /param token_type The token type to match.
+   */
+  void Consume(const TokenType& token_type) {
+    if (tokens[pos]->token_type != token_type) {
+      std::string message =
+          "expected a " + Pretty(token_type) + " found " + Pretty(Peek()->token_type);
+      this->diag_ctx.Emit({tokens[pos]->line, tokens[pos]->column, message});
+      this->diag_ctx.Render(std::cout);
+    }
+    pos++;
+  }
+
+  /*! Match a token in the stream, this will first invoke Peek, ignoring tokens such
+   * as whitespace or comments returning the first meaningful token.
+   *
+   * We then try and consume the requested token, this will trigger an error if the
+   * current token does not match the token_type.
+   */
+  Token Match(const TokenType& token_type) {
+    auto tok = Peek();
+    Consume(token_type);
+    return tok;
+  }
+
+  /*! Conditionally consume a token when it matches, this will never trigger an error
+   * as we guard against consuming the token before we do.
+   *
+   * Useful for matching optional tokens, effectively looksahead by one.
+   */
+  bool WhenMatch(const TokenType& token_type) {
+    if (Peek()->token_type == token_type) {
+      Consume(token_type);
+      return true;
+    } else {
+      return false;
+    }
+  }
+
+  /* \brief Add a graph binding to the parsing context
+   *
+   * For example if we parse %0 = add(...), map 0 -> add(...), etc.
+   */
+  void AddGraphBinding(const Token& token, const Expr& expr) {
+    auto graph_no = token.ToNumber();
+    this->graph_ctx.insert({graph_no, expr});
+  }
+
+  /* \brief Lookup a previously bound graph variable.
+   *
+   * Note: we take tokens in all lookup methods so that we
+   * that we can do error reporting based on token location.
+   */
+  Expr LookupGraphBinding(const Token& token) {
+    auto graph_no = token.ToNumber();
+    return this->graph_ctx.at(graph_no);
+  }
+
+  /*! \brief Bind a local variable in the expression scope.
+   *
+   * "x" -> Var("x"), these are needed to map from the raw string names
+   * to unique variable nodes.
+   */
+  Var BindVar(const std::string& name, const relay::Type& type_annotation) {
+    auto var = Var(name, type_annotation);
+    this->expr_scopes.Add(name, var);
+    return var;
+  }
+
+  /*! \brief Bind a type variable in the type scope.
+   *
+   * "A" -> TypeVar("A", ...), these are needed to map from raw string names
+   * to unique type variable nodes.
+   */
+  TypeVar BindTypeVar(const std::string& name, const TypeKind type_kind) {
+    auto type_var = TypeVar(name, type_kind);
+    this->type_scopes.Add(name, type_var);
+    return type_var;
+  }
+
+  /*! \brief Lookup a variable in the expression scope.
+   *
+   * Note: all lookup methods take tokens intentionally for error reporting information.
+   */
+  Var LookupLocal(const Token& local) {
+    auto var = this->expr_scopes.Lookup(local.ToString());
+    if (!var.defined()) {
+      diag_ctx.Emit(
+          {local->line, local->column, "this local variable has not been previously declared"});
+    }
+    return var;
+  }
+
+  /*! \brief Lookup a variable in the type scope.
+   *
+   * Note: all lookup methods take tokens intentionally for error reporting information.
+   */
+  TypeVar LookupTypeVar(const Token& ident) {
+    auto var = this->type_scopes.Lookup(ident.ToString());
+    if (!var.defined()) {
+      diag_ctx.Emit(
+          {ident->line, ident->column,
+           "this type variable has not been previously declared anywhere, perhaps a typo?"});
+    }
+    return var;
+  }
+
+  /*! \brief Add an expression scope to the scope stack. */
+  void PushScope() { this->expr_scopes.PushStack(); }
+
+  /*! \brief Remove N expression scopes from the scope stack. */
+  void PopScopes(int n) {
+    for (int i = 0; i < n; i++) {
+      this->expr_scopes.PopStack();
+    }
+  }
+
+  /*! \brief Add an type scope to the scope stack. */
+  void PushTypeScope() { this->type_scopes.PushStack(); }
+
+  /*! \brief Remove N type scopes from the scope stack. */
+  void PopTypeScopes(int n) {
+    for (int i = 0; i < n; i++) {
+      this->type_scopes.PopStack();
+    }
+  }
+
+  /*! \brief Convert a numeric token to an NDArray for embedding into the Relay program. */
+  NDArray NumberToNDArray(const Token& token) {
+    if (token->token_type == TokenType::Integer) {
+      DLContext ctx({.device_type = DLDeviceType::kDLCPU, .device_id = 0});
+      auto dtype = String2DLDataType("int32");
+      auto data = NDArray::Empty({}, dtype, ctx);
+      auto array = reinterpret_cast<int32_t*>(data->data);
+      // revisit this, literal node issue.
+      int64_t value = Downcast<tvm::Integer>(token->data);
+      array[0] = (int32_t)value;
+      return data;
+    } else if (token->token_type == TokenType::Float) {
+      DLContext ctx({.device_type = DLDeviceType::kDLCPU, .device_id = 0});
+      auto dtype = String2DLDataType("float32");
+      auto data = NDArray::Empty({}, dtype, ctx);
+      auto array = reinterpret_cast<float*>(data->data);
+      // revisit this, literal node issue.
+      float value = Downcast<tvm::FloatImm>(token->data)->value;
+      array[0] = value;
+      return data;
+    } else {
+      LOG(FATAL) << "internal error: should only call this function on numeric tokens";
+      return NDArray();
+    }
+  }
+
+  /*! \brief Convert a boolean value to an NDArray for embedding into the Relay program. */
+  NDArray BooleanToNDarray(bool value) {
+    DLContext ctx({.device_type = DLDeviceType::kDLCPU, .device_id = 0});
+    auto dtype = String2DLDataType("bool");
+    auto data = NDArray::Empty({}, dtype, ctx);
+    auto array = reinterpret_cast<bool*>(data->data);
+    array[0] = value;
+    return data;
+  }
+
+  [[noreturn]] void ParseError(const Token& token, const std::string& msg) {
+    throw std::runtime_error(msg);
+  }
+
+  /*! \brief A parsing helper for a bracketed expression <start> <parser> <stop>. */
+  template <typename R>
+  R Bracket(TokenType open, TokenType close, std::function<R()> parser) {
+    Match(open);
+    R result = parser();
+    Match(close);
+    return result;
+  }
+
+  /*! \brief Parse `(` parser() `)`. */
+  template <typename R>
+  R Parens(std::function<R()> parser) {
+    return Bracket(TokenType::OpenParen, TokenType::CloseParen, parser);
+  }
+
+  /*! \brief Parse `{` parser() `}`. */
+  template <typename R>
+  R Block(std::function<R()> parser) {
+    return Bracket(TokenType::LCurly, TokenType::RCurly, parser);
+  }
+
+  /*! \brief Parses a sequence beginning with a start token, seperated by a seperator token, and
+   * ending with a stop token.
+   *
+   * The simple form being <start> (<parse()> <seperator>)* <stop>.
+   *
+   * This also provides a fourth argument which is allowed to run when the sequence which matches
+   * the inner sequence can not proceed.
+   *
+   * This is useful for parsing things like attributes which don't match the standard expression
+   * parsers but are contained within the stop token.
+   */
+  template <typename T>
+  Array<T> ParseSequence(TokenType start, TokenType sep, TokenType stop, std::function<T()> parse,
+                         std::function<void()> before_stop = nullptr) {
+    Match(start);
+    if (WhenMatch(stop)) {
+      return Array<T>();
+    } else {
+      auto data = parse();
+      Array<T> elements = {data};
+
+      // parse '(' expr ')'
+      // if we are at the end invoke leftover parser
+      if (Peek()->token_type == stop && before_stop) {
+        before_stop();
+      }
+      if (WhenMatch(stop)) {
+        return elements;
+        // parse '( expr ',' * ')'
+      } else if (WhenMatch(sep)) {
+        // if we are at the end invoke leftover parser
+        if (Peek()->token_type == stop && before_stop) {
+          before_stop();
+        }
+        while (true) {
+          if (WhenMatch(stop)) {
+            break;
+          } else {
+            auto data = parse();
+            WhenMatch(sep);
+            elements.push_back(data);
+          }
+        }
+        return elements;
+      } else {
+        LOG(FATAL) << "issue";
+        return Array<T>(nullptr);
+      }
+    }
+  }
+
+  /*! \brief Parse a full IRModule. */
+  IRModule ParseModule() {
+    // Parse the semver header at the top of the module.
+    this->version = ParseSemVer();
+    // Parse the definitions.
+    auto defs = ParseDefinitions();
+    // Parse the metadata section at the end.
+    auto metadata = ParseMetadata();
+    Match(TokenType::EndOfFile);
+    Map<tvm::GlobalVar, BaseFunc> funcs;
+    Map<tvm::GlobalTypeVar, TypeData> types;
+
+    for (auto type_def : defs.types) {
+      types.Set(type_def->header, type_def);
+    }
+
+    auto mod = IRModule({}, types);
+
+    for (auto func : defs.funcs) {
+      mod->Add(func.global, func.function);
+    }
+
+    return mod;
+  }
+
+  /*! \brief Parse the semantic versioning header. */
+  SemVer ParseSemVer() {
+    // TODO(@jroesch): convert semver to module level attribute.
+    auto id = Peek();
+    if (id->token_type == TokenType::Identifier && id.ToString() == "v0") {
+      auto id = Match(TokenType::Identifier);
+      Consume(TokenType::Period);
+      // CHECK_EQ(minor_and_patch)
+      Consume(TokenType::Float);
+    }
+    // For now we only support current version.
+    return SemVer{.major = 0, .minor = 0, .patch = 4};
+  }
+
+  /*! \brief Parse zero or more Relay definitions. */
+  Definitions ParseDefinitions() {
+    Definitions defs;
+
+    while (true) {
+      auto next = Peek();
+      switch (next->token_type) {
+        case TokenType::Defn: {
+          Consume(TokenType::Defn);
+          auto global_name = Match(TokenType::Global).ToString();
+          auto global = GlobalVar(global_name);
+          global_names.Add(global_name, global);
+          auto func = ParseFunctionDef();
+          defs.funcs.push_back(GlobalFunc(global, func));
+          continue;
+        }
+        case TokenType::TypeDef: {
+          defs.types.push_back(ParseTypeDef());
+          continue;
+        }
+        case TokenType::Extern: {
+          Consume(TokenType::Extern);
+          // TODO(@jroesch): add some validation here?
+          defs.types.push_back(ParseTypeDef());

Review comment:
       i'm not sure if it _can_ be validated, since it's opaque. unless you mean something else

##########
File path: src/parser/parser.cc
##########
@@ -0,0 +1,1384 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/*!
+ * \file parser.cc
+ * \brief A parser for TVM IR.
+ */
+#include <tvm/ir/module.h>
+#include <tvm/node/reflection.h>
+#include <tvm/relay/adt.h>
+#include <tvm/relay/expr.h>
+#include <tvm/relay/function.h>
+#include <tvm/runtime/object.h>
+#include <tvm/runtime/registry.h>
+
+#include <fstream>
+
+#include "./diagnostic.h"
+#include "./op_table.h"
+#include "./tokenizer.h"
+
+namespace tvm {
+namespace parser {
+
+using namespace relay;
+using Expr = relay::Expr;
+
+/*! \brief A wrapper structure for capturing the result of parsing
+ * a global definition *before* we add it to the IRModule.
+ *
+ * This enables the parser to parse everything in one pass before
+ * constructing the IRModule.
+ */
+struct GlobalFunc {
+  GlobalVar global;
+  Function function;
+  GlobalFunc() : global(), function() {}
+  GlobalFunc(GlobalVar global, Function function) : global(global), function(function) {}
+  GlobalFunc(const GlobalFunc& gfunc) {
+    this->global = gfunc.global;
+    this->function = gfunc.function;
+  }
+};
+
+/*! \brief A wrapper structure for capturing all top-level definitions
+ * when parsing a module.
+ */
+struct Definitions {
+  /*! \brief The set of global functions. */
+  std::vector<GlobalFunc> funcs;
+  /*! \brief The set of type definitions. */
+  std::vector<TypeData> types;
+  // TODO(@jroesch): contain meta-table below
+};
+
+/*! \brief A structure representing the semantic versioning information
+ * for a Relay program.
+ */
+struct SemVer {
+  int major;
+  int minor;
+  int patch;
+};
+
+/*! \brief A reference to a "meta-expression".
+ *
+ * In the text format we allow referencing metadata which
+ * uses a compact serialization that proceeds the main
+ * program body.
+ *
+ * We can reference this table using an expression of
+ * the form `meta[Type][index]`.
+ *
+ * We must later resolve these references to actual in-memory
+ * AST nodes but this requires first parsing the full program
+ * then expanding these temporary AST nodes into their corresponding
+ * nodes.
+ *
+ * For example the nth large constant will be pretty-printed as meta[relay.Constant][n]
+ * with its compact binary serialization residing in the metadata section at the end
+ * of the program.
+ */
+class MetaRefExprNode : public TempExprNode {
+ public:
+  /*! \brief The type key of the meta expression. */
+  std::string type_key;
+  /*! \brief The index into the type key's table. */
+  uint64_t node_index;
+
+  void VisitAttrs(tvm::AttrVisitor* v) {}
+
+  // TODO(@jroesch): we probably will need to manually
+  // expand these with a pass.
+  Expr Realize() const final { return Expr(); }
+
+  static constexpr const char* _type_key = "relay.MetaRefExpr";
+  TVM_DECLARE_FINAL_OBJECT_INFO(MetaRefExprNode, TempExprNode);
+};
+
+class MetaRefExpr : public TempExpr {
+ public:
+  /*!
+   * \brief The constructor for MetaRefExpr
+   * \param type_key The type key of the object in the meta section.
+   * \param kind The index into that subfield.
+   */
+  TVM_DLL MetaRefExpr(std::string type_key, uint64_t node_index);
+
+  TVM_DEFINE_OBJECT_REF_METHODS(MetaRefExpr, TempExpr, MetaRefExprNode);
+};
+
+MetaRefExpr::MetaRefExpr(std::string type_key, uint64_t node_index) {
+  auto rnode = make_object<MetaRefExprNode>();
+  rnode->type_key = type_key;
+  rnode->node_index = node_index;
+  data_ = std::move(rnode);
+}
+
+/*! \brief A simple wrapper around a mapping from raw string names
+ * to a TVM variable, type variable or other binder type.
+ */
+template <typename T>
+struct Scope {
+  /*! \brief The internal map. */
+  std::unordered_map<std::string, T> name_map;
+};
+
+/*! \brief A stack of scopes.
+ *
+ * In order to properly handle scoping we must maintain a scope of stacks.
+ *
+ * A stack allows users to write programs which contain repeated variable
+ * names and to properly handle both nested scopes and removal of variables
+ * when they go out of scope.
+ *
+ * This is the classic approach to lexical scoping.
+ */
+template <typename T>
+class ScopeStack {
+ private:
+  std::vector<Scope<T>> scope_stack;
+
+ public:
+  /*! \brief Adds a variable binding to the current scope. */
+  void Add(const std::string& name, const T& value) {
+    if (!this->scope_stack.size()) {
+      LOG(FATAL) << "internal issue";
+    }
+    this->scope_stack.back().name_map.insert({name, value});
+  }
+
+  /*! \brief Looks up a variable name in the scope stack returning the matching variable
+   * in most recent scope. */
+  T Lookup(const std::string& name) {
+    for (auto scope = this->scope_stack.rbegin(); scope != this->scope_stack.rend(); ++scope) {
+      auto it = scope->name_map.find(name);
+      if (it != scope->name_map.end()) {
+        return it->second;
+      }
+    }
+    return T();
+  }
+
+  /*! \brief Adds a fresh scope. */
+  void PushStack() { this->scope_stack.push_back(Scope<T>()); }
+
+  /*! \brief Removes the most recent scope. */
+  void PopStack() { this->scope_stack.pop_back(); }
+};
+
+/*! \brief A table of interning strings as global function and type names. */
+template <typename T>
+struct InternTable {
+  /*! \brief The internal table mapping strings to a unique allocation. */
+  std::unordered_map<std::string, T> table;
+
+  /*! \brief Add the unique allocation. */
+  void Add(const std::string& name, const T& t) {
+    auto it = table.find(name);
+    if (it != table.end()) {
+      LOG(FATAL) << "duplicate name";
+    } else {
+      table.insert({name, t});
+    }
+  }
+
+  /*! \brief Return the unique allocation. */
+  Optional<T> Get(const std::string& name) {
+    auto it = table.find(name);
+    if (it != table.end()) {
+      return Optional<T>(it->second);
+    } else {
+      return Optional<T>();
+    }
+  }
+};
+
+/*! \brief The parser class is the main interface to the parser.
+ * the parser is not currently exposed beyond this .cc file.
+ *
+ * The parser is initialized with a diagnostic context, an
+ * operator table, and a token stream.
+ *
+ * The rest of the internal state is used to map the human readable
+ * form to in-memory IR representation.
+ *
+ * The main entry point to the parser are a set of parsing methods
+ * such as `ParseModule` and `ParseExpr`.
+ *
+ * As with traditional recursive descent parsers the parsing methods
+ * are factored recursively just as one would do with a formal language
+ * grammar.
+ *
+ * You can view a recursive descent parser as a human friendly way to specify
+ * a state machine, and thus this factoring is necessary as the 'state' of this
+ * machine is the combination of the current parsing method and the next token.
+ *
+ * Parsing proceeds by matching a token and then dispatching to the appropriate
+ * method to parse the next tokens in the stream.
+ *
+ * For example if we are parsing a type and encounter a "Tensor" token we switch
+ * into a mode for parsing `[`, a shape, a comma, a data type and then a ']'.
+ *
+ * Certain matches like this are unambiguous and proceed in a straight line fashion
+ * once the initial token is found. Other parsing is more complex and requires some
+ * tricks to correctly parse.
+ *
+ * For example when we find a '(' in an expression context, it may be part of
+ * a tuple, the arguments to a call, or a parenthesized expression. The below code
+ * disambiguate these cases by factoring expression parsing into a series of methods
+ * which encode the parsing context the and thus how to interpret the parenthesis.
+ *
+ * For more information one should be able to read the code in order starting with
+ * `ParseModule` or `ParseExpr`.
+ */
+class Parser {
+ public:
+  /*! \brief The version that the parser is parsing. */
+  SemVer version;
+
+  /*! \brief The diagnostic context used for error reporting. */
+  DiagnosticContext diag_ctx;
+
+  /*! \brief The current position in the token stream. */
+  int pos;
+
+  /*! \brief The token stream for the parser. */
+  std::vector<Token> tokens;
+
+  /*! \brief The configured operator table. */
+  OperatorTable op_table;
+
+  /*! \brief Configure the whitespace mode, right now we ignore all whitespace. */
+  bool ignore_whitespace;
+
+  /*! \brief A global mapping for GlobalVar. */
+  InternTable<GlobalVar> global_names;
+
+  /*! \brief A global mapping for type definitions. */
+  InternTable<GlobalTypeVar> type_names;
+
+  /*! \brief A global mapping for constructor names. */
+  InternTable<Constructor> ctors;
+
+  /*! \brief A mapping from graph variable to expression, i.e., `%0 = expr`. */
+  std::unordered_map<int, Expr> graph_ctx;
+
+  /*! \brief The set of type scopes used for generics. */
+  ScopeStack<TypeVar> type_scopes;
+
+  /*! \brief The set of expression scopes used for lexical scope. */
+  ScopeStack<Var> expr_scopes;
+
+  Parser(std::vector<Token> tokens, OperatorTable op_table, Source source)
+      : diag_ctx(source), pos(0), tokens(tokens), op_table(op_table), ignore_whitespace(true) {}
+
+  /*! \brief Examine the next token in the stream, the current parser is configured to be
+   * whitespace insensitive so we will skip all whitespace or comment tokens. */
+  Token Peek() {
+    // For now we ignore all whitespace tokens and comments.
+    // We can tweak this behavior later to enable white space sensitivity in the parser.
+    while (pos < tokens.size() && ignore_whitespace &&
+           (tokens.at(pos)->token_type == TokenType::Whitespace ||
+            tokens.at(pos)->token_type == TokenType::Newline ||
+            tokens.at(pos)->token_type == TokenType::LineComment ||
+            tokens.at(pos)->token_type == TokenType::Comment)) {
+      pos++;
+    }
+
+    if (pos < tokens.size()) {
+      return Token(this->tokens.at(pos));
+    } else {
+      return Token::Null();
+    }
+  }
+
+  /*! \brief Lookahead by N tokens.
+   * \param n The number of tokens to lookahead.
+   * \return The Nth token.
+   */
+  Token Lookahead(int n) {
+    CHECK_GE(n, 1) << "lookahead is only valid when n >= 1";
+
+    // We intend to skip n - 1 tokens, then return the nth.
+    auto old_pos = pos;
+    for (int i = 0; i < n - 1; i++) {
+      Peek();
+      pos++;
+    }
+
+    auto tok = Peek();
+    pos = old_pos;
+    return tok;
+  }
+
+  /*! \brief Consume a token, this method is the lowest level way to consume a token
+   * and will not ignore white space or look ahead in anyway.
+   *
+   * /param token_type The token type to match.
+   */
+  void Consume(const TokenType& token_type) {
+    if (tokens[pos]->token_type != token_type) {
+      std::string message =
+          "expected a " + Pretty(token_type) + " found " + Pretty(Peek()->token_type);
+      this->diag_ctx.Emit({tokens[pos]->line, tokens[pos]->column, message});
+      this->diag_ctx.Render(std::cout);
+    }
+    pos++;
+  }
+
+  /*! Match a token in the stream, this will first invoke Peek, ignoring tokens such
+   * as whitespace or comments returning the first meaningful token.
+   *
+   * We then try and consume the requested token, this will trigger an error if the
+   * current token does not match the token_type.
+   */
+  Token Match(const TokenType& token_type) {
+    auto tok = Peek();
+    Consume(token_type);
+    return tok;
+  }
+
+  /*! Conditionally consume a token when it matches, this will never trigger an error
+   * as we guard against consuming the token before we do.
+   *
+   * Useful for matching optional tokens, effectively looksahead by one.

Review comment:
       ```suggestion
      * Useful for matching optional tokens, effectively looks ahead by one.
   ```




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org