wsgl parser: support multiple error messages

Use synchronization tokens to ensure the parser can resynchronize on error.

Bug: tint:282
Change-Id: I8bb033f8a723eb8f2bc029e1ffc8350174c964e2
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/32284
Commit-Queue: dan sinclair <dsinclair@chromium.org>
Reviewed-by: dan sinclair <dsinclair@chromium.org>
diff --git a/BUILD.gn b/BUILD.gn
index d98b686..21e2077 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -943,6 +943,7 @@
     "src/reader/wgsl/parser_impl_elseif_stmt_test.cc",
     "src/reader/wgsl/parser_impl_equality_expression_test.cc",
     "src/reader/wgsl/parser_impl_error_msg_test.cc",
+    "src/reader/wgsl/parser_impl_error_resync_test.cc",
     "src/reader/wgsl/parser_impl_exclusive_or_expression_test.cc",
     "src/reader/wgsl/parser_impl_for_stmt_test.cc",
     "src/reader/wgsl/parser_impl_function_decl_test.cc",
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 65ef68d..93daa84 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -479,6 +479,7 @@
     reader/wgsl/parser_impl_elseif_stmt_test.cc
     reader/wgsl/parser_impl_equality_expression_test.cc
     reader/wgsl/parser_impl_error_msg_test.cc
+    reader/wgsl/parser_impl_error_resync_test.cc
     reader/wgsl/parser_impl_exclusive_or_expression_test.cc
     reader/wgsl/parser_impl_for_stmt_test.cc
     reader/wgsl/parser_impl_function_decl_test.cc
diff --git a/src/reader/wgsl/parser_impl.cc b/src/reader/wgsl/parser_impl.cc
index 036a35f..537c05c 100644
--- a/src/reader/wgsl/parser_impl.cc
+++ b/src/reader/wgsl/parser_impl.cc
@@ -83,7 +83,11 @@
 /// Controls the maximum number of times we'll call into the const_expr function
 /// from itself. This is to guard against stack overflow when there is an
 /// excessive number of type constructors inside the const_expr.
-uint32_t kMaxConstExprDepth = 128;
+constexpr uint32_t kMaxConstExprDepth = 128;
+
+/// The maximum number of tokens to look ahead to try and sync the
+/// parser on error.
+constexpr size_t const kMaxResynchronizeLookahead = 32;
 
 ast::Builtin ident_to_builtin(const std::string& str) {
   if (str == "position") {
@@ -122,6 +126,38 @@
          t.IsOffset();
 }
 
+/// Enter-exit counters for block token types.
+/// Used by sync_to() to skip over closing block tokens that were opened during
+/// the forward scan.
+struct BlockCounters {
+  int attrs = 0;    // [[ ]]
+  int brace = 0;    // {   }
+  int bracket = 0;  // [   ]
+  int paren = 0;    // (   )
+
+  /// @return the current enter-exit depth for the given block token type. If
+  /// |t| is not a block token type, then 0 is always returned.
+  int consume(const Token& t) {
+    if (t.Is(Token::Type::kAttrLeft))
+      return attrs++;
+    if (t.Is(Token::Type::kAttrRight))
+      return attrs--;
+    if (t.Is(Token::Type::kBraceLeft))
+      return brace++;
+    if (t.Is(Token::Type::kBraceRight))
+      return brace--;
+    if (t.Is(Token::Type::kBracketLeft))
+      return bracket++;
+    if (t.Is(Token::Type::kBracketRight))
+      return bracket--;
+    if (t.Is(Token::Type::kParenLeft))
+      return paren++;
+    if (t.Is(Token::Type::kParenRight))
+      return paren--;
+    return 0;
+  }
+};
+
 }  // namespace
 
 ParserImpl::ParserImpl(Context* ctx, Source::File const* file)
@@ -198,10 +234,8 @@
 // translation_unit
 //  : global_decl* EOF
 void ParserImpl::translation_unit() {
-  while (!peek().IsEof()) {
-    auto decl = expect_global_decl();
-    if (decl.errored)
-      break;
+  while (!peek().IsEof() && synchronized_) {
+    expect_global_decl();
   }
 
   assert(module_.IsValid());
@@ -218,75 +252,87 @@
   if (match(Token::Type::kSemicolon) || match(Token::Type::kEOF))
     return true;
 
+  bool errored = false;
+
   auto decos = decoration_list();
-
-  // FUDGE - Abort early if we enter with an error state to avoid accumulating
-  // multiple error messages.
-  // TODO(ben-clayton) - remove this once resynchronization is implemented.
-  if (has_error())
+  if (decos.errored)
+    errored = true;
+  if (!synchronized_)
     return Failure::kErrored;
 
-  auto gv = global_variable_decl(decos.value);
-  if (gv.errored)
-    return Failure::kErrored;
-  if (gv.matched) {
-    if (!expect("variable declaration", Token::Type::kSemicolon))
+  auto decl = sync(Token::Type::kSemicolon, [&]() -> Maybe<bool> {
+    auto gv = global_variable_decl(decos.value);
+    if (gv.errored)
+      return Failure::kErrored;
+    if (gv.matched) {
+      if (!expect("variable declaration", Token::Type::kSemicolon))
+        return Failure::kErrored;
+
+      module_.AddGlobalVariable(std::move(gv.value));
+      return true;
+    }
+
+    auto gc = global_constant_decl();
+    if (gc.errored)
       return Failure::kErrored;
 
-    module_.AddGlobalVariable(std::move(gv.value));
-    return true;
-  }
+    if (gc.matched) {
+      if (!expect("constant declaration", Token::Type::kSemicolon))
+        return Failure::kErrored;
 
-  auto gc = global_constant_decl();
-  if (gc.errored) {
-    return Failure::kErrored;
-  }
-  if (gc.matched) {
-    if (!expect("constant declaration", Token::Type::kSemicolon))
+      module_.AddGlobalVariable(std::move(gc.value));
+      return true;
+    }
+
+    auto ta = type_alias();
+    if (ta.errored)
       return Failure::kErrored;
 
-    module_.AddGlobalVariable(std::move(gc.value));
-    return true;
-  }
+    if (ta.matched) {
+      if (!expect("type alias", Token::Type::kSemicolon))
+        return Failure::kErrored;
 
-  auto ta = type_alias();
-  if (ta.errored)
-    return Failure::kErrored;
+      module_.AddConstructedType(ta.value);
+      return true;
+    }
 
-  if (ta.matched) {
-    if (!expect("type alias", Token::Type::kSemicolon))
+    auto str = struct_decl(decos.value);
+    if (str.errored)
       return Failure::kErrored;
 
-    module_.AddConstructedType(ta.value);
+    if (str.matched) {
+      if (!expect("struct declaration", Token::Type::kSemicolon))
+        return Failure::kErrored;
+
+      auto* type = ctx_.type_mgr().Get(std::move(str.value));
+      register_constructed(type->AsStruct()->name(), type);
+      module_.AddConstructedType(type);
+      return true;
+    }
+
+    return Failure::kNoMatch;
+  });
+
+  if (decl.errored)
+    errored = true;
+  if (decl.matched)
     return true;
-  }
-
-  auto str = struct_decl(decos.value);
-  if (str.errored)
-    return Failure::kErrored;
-
-  if (str.matched) {
-    if (!expect("struct declaration", Token::Type::kSemicolon))
-      return Failure::kErrored;
-
-    auto* type = ctx_.type_mgr().Get(std::move(str.value));
-    register_constructed(type->AsStruct()->name(), type);
-    module_.AddConstructedType(type);
-    return true;
-  }
 
   auto func = function_decl(decos.value);
   if (func.errored)
-    return Failure::kErrored;
+    errored = true;
   if (func.matched) {
     module_.AddFunction(std::move(func.value));
     return true;
   }
 
+  if (errored)
+    return Failure::kErrored;
+
   if (decos.value.size() > 0) {
-    add_error(peek(), "expected declaration after decorations");
+    add_error(next(), "expected declaration after decorations");
   } else {
-    add_error(peek(), "unexpected token");
+    add_error(next(), "unexpected token");
   }
   return Failure::kErrored;
 }
@@ -1027,10 +1073,6 @@
   if (!match(Token::Type::kStruct))
     return Failure::kNoMatch;
 
-  auto struct_decos = cast_decorations<ast::StructDecoration>(decos);
-  if (struct_decos.errored)
-    return Failure::kErrored;
-
   auto name = expect_ident("struct declaration");
   if (name.errored)
     return Failure::kErrored;
@@ -1039,6 +1081,10 @@
   if (body.errored)
     return Failure::kErrored;
 
+  auto struct_decos = cast_decorations<ast::StructDecoration>(decos);
+  if (struct_decos.errored)
+    return Failure::kErrored;
+
   return std::make_unique<ast::type::StructType>(
       name.value,
       std::make_unique<ast::Struct>(source, std::move(struct_decos.value),
@@ -1050,20 +1096,32 @@
 Expect<ast::StructMemberList> ParserImpl::expect_struct_body_decl() {
   return expect_brace_block(
       "struct declaration", [&]() -> Expect<ast::StructMemberList> {
+        bool errored = false;
+
         ast::StructMemberList members;
 
-        while (!peek().IsBraceRight() && !peek().IsEof()) {
-          auto decos = decoration_list();
-          if (decos.errored)
-            return Failure::kErrored;
+        while (synchronized_ && !peek().IsBraceRight() && !peek().IsEof()) {
+          auto member =
+              sync(Token::Type::kSemicolon,
+                   [&]() -> Expect<std::unique_ptr<ast::StructMember>> {
+                     auto decos = decoration_list();
+                     if (decos.errored)
+                       errored = true;
+                     if (!synchronized_)
+                       return Failure::kErrored;
+                     return expect_struct_member(decos.value);
+                   });
 
-          auto mem = expect_struct_member(decos.value);
-          if (mem.errored)
-            return Failure::kErrored;
-
-          members.push_back(std::move(mem.value));
+          if (member.errored) {
+            errored = true;
+          } else {
+            members.push_back(std::move(member.value));
+          }
         }
 
+        if (errored)
+          return Failure::kErrored;
+
         return members;
       });
 }
@@ -1072,20 +1130,6 @@
 //   : struct_member_decoration_decl+ variable_ident_decl SEMICOLON
 Expect<std::unique_ptr<ast::StructMember>> ParserImpl::expect_struct_member(
     ast::DecorationList& decos) {
-  // FUDGE - Abort early if we enter with an error state to avoid accumulating
-  // multiple error messages. This is a work around for the unit tests that
-  // call:
-  //   auto decos = p->decoration_list();
-  //   auto m = p->expect_struct_member(decos);
-  // ... and expect a single error message due to bad decorations.
-  // While expect_struct_body_decl() aborts after checking for decoration parse
-  // errors (and so these tests do not currently reflect full-parse behaviour),
-  // they do test the long-term desired behavior where the parser can
-  // resynchronize at the ']]'.
-  // TODO(ben-clayton) - remove this once resynchronization is implemented.
-  if (has_error())
-    return Failure::kErrored;
-
   auto decl = expect_variable_ident_decl("struct member");
   if (decl.errored)
     return Failure::kErrored;
@@ -1106,19 +1150,34 @@
 Maybe<std::unique_ptr<ast::Function>> ParserImpl::function_decl(
     ast::DecorationList& decos) {
   auto f = function_header();
-  if (f.errored)
+  if (f.errored) {
+    if (sync_to(Token::Type::kBraceLeft, /* consume: */ false)) {
+      // There were errors in the function header, but the parser has managed to
+      // resynchronize with the opening brace. As there's no outer
+      // synchronization token for function declarations, attempt to parse the
+      // function body. The AST isn't used as we've already errored, but this
+      // catches any errors inside the body, and can help keep the parser in
+      // sync.
+      expect_body_stmt();
+    }
     return Failure::kErrored;
+  }
   if (!f.matched)
     return Failure::kNoMatch;
 
+  bool errored = false;
+
   auto func_decos = cast_decorations<ast::FunctionDecoration>(decos);
   if (func_decos.errored)
-    return Failure::kErrored;
+    errored = true;
 
   f->set_decorations(std::move(func_decos.value));
 
   auto body = expect_body_stmt();
   if (body.errored)
+    errored = true;
+
+  if (errored)
     return Failure::kErrored;
 
   f->set_body(std::move(body.value));
@@ -1143,23 +1202,34 @@
     return Failure::kNoMatch;
 
   const char* use = "function declaration";
+  bool errored = false;
 
   auto name = expect_ident(use);
-  if (name.errored)
-    return Failure::kErrored;
+  if (name.errored) {
+    errored = true;
+    if (!sync_to(Token::Type::kParenLeft, /* consume: */ false))
+      return Failure::kErrored;
+  }
 
   auto params = expect_paren_block(use, [&] { return expect_param_list(); });
-  if (params.errored)
-    return Failure::kErrored;
+  if (params.errored) {
+    errored = true;
+    if (!synchronized_)
+      return Failure::kErrored;
+  }
 
   if (!expect(use, Token::Type::kArrow))
     return Failure::kErrored;
 
   auto type = function_type_decl();
-  if (type.errored)
-    return Failure::kErrored;
-  if (!type.matched)
+  if (type.errored) {
+    errored = true;
+  } else if (!type.matched) {
     return add_error(peek(), "unable to determine function return type");
+  }
+
+  if (errored)
+    return Failure::kErrored;
 
   return std::make_unique<ast::Function>(source, name.value,
                                          std::move(params.value), type.value);
@@ -1252,18 +1322,23 @@
 // statements
 //   : statement*
 Expect<std::unique_ptr<ast::BlockStatement>> ParserImpl::expect_statements() {
+  bool errored = false;
   auto ret = std::make_unique<ast::BlockStatement>();
 
-  for (;;) {
+  while (synchronized_) {
     auto stmt = statement();
-    if (stmt.errored)
-      return Failure::kErrored;
-    if (!stmt.matched)
+    if (stmt.errored) {
+      errored = true;
+    } else if (stmt.matched) {
+      ret->append(std::move(stmt.value));
+    } else {
       break;
-
-    ret->append(std::move(stmt.value));
+    }
   }
 
+  if (errored)
+    return Failure::kErrored;
+
   return ret;
 }
 
@@ -1287,9 +1362,10 @@
     // Skip empty statements
   }
 
-  // Non-block statments all end in a semi-colon.
-  // TODO(bclayton): We can use this property to synchronize on error.
-  auto stmt = non_block_statement();
+  // Non-block statments that error can resynchronize on semicolon.
+  auto stmt =
+      sync(Token::Type::kSemicolon, [&] { return non_block_statement(); });
+
   if (stmt.errored)
     return Failure::kErrored;
   if (stmt.matched)
@@ -1401,6 +1477,7 @@
   auto expr = logical_or_expression();
   if (expr.errored)
     return Failure::kErrored;
+
   // TODO(bclayton): Check matched?
   return std::make_unique<ast::ReturnStatement>(source, std::move(expr.value));
 }
@@ -1540,16 +1617,20 @@
 
   auto body = expect_brace_block("switch statement",
                                  [&]() -> Expect<ast::CaseStatementList> {
+                                   bool errored = false;
                                    ast::CaseStatementList list;
-                                   for (;;) {
+                                   while (synchronized_) {
                                      auto stmt = switch_body();
-                                     if (stmt.errored)
-                                       return Failure::kErrored;
+                                     if (stmt.errored) {
+                                       errored = true;
+                                       continue;
+                                     }
                                      if (!stmt.matched)
                                        break;
-
                                      list.push_back(std::move(stmt.value));
                                    }
+                                   if (errored)
+                                     return Failure::kErrored;
                                    return list;
                                  });
 
@@ -1630,11 +1711,8 @@
 Maybe<std::unique_ptr<ast::BlockStatement>> ParserImpl::case_body() {
   auto ret = std::make_unique<ast::BlockStatement>();
   for (;;) {
-    auto t = peek();
-    if (t.IsFallthrough()) {
-      auto source = t.source();
-      next();  // Consume the peek
-
+    Source source;
+    if (match(Token::Type::kFallthrough, &source)) {
       if (!expect("fallthrough statement", Token::Type::kSemicolon))
         return Failure::kErrored;
 
@@ -2571,19 +2649,23 @@
 }
 
 Maybe<ast::DecorationList> ParserImpl::decoration_list() {
+  bool errored = false;
   bool matched = false;
   ast::DecorationList decos;
 
-  while (true) {
+  while (synchronized_) {
     auto list = decoration_bracketed_list(decos);
     if (list.errored)
-      return Failure::kErrored;
+      errored = true;
     if (!list.matched)
       break;
 
     matched = true;
   }
 
+  if (errored)
+    return Failure::kErrored;
+
   if (!matched)
     return Failure::kNoMatch;
 
@@ -2591,6 +2673,8 @@
 }
 
 Maybe<bool> ParserImpl::decoration_bracketed_list(ast::DecorationList& decos) {
+  const char* use = "decoration list";
+
   if (!match(Token::Type::kAttrLeft)) {
     return Failure::kNoMatch;
   }
@@ -2599,30 +2683,37 @@
   if (match(Token::Type::kAttrRight, &source))
     return add_error(source, "empty decoration list");
 
-  while (true) {
-    auto deco = expect_decoration();
-    if (deco.errored)
-      return Failure::kErrored;
+  return sync(Token::Type::kAttrRight, [&]() -> Expect<bool> {
+    bool errored = false;
 
-    decos.emplace_back(std::move(deco.value));
+    while (synchronized_) {
+      auto deco = expect_decoration();
+      if (deco.errored)
+        errored = true;
+      decos.emplace_back(std::move(deco.value));
 
-    if (match(Token::Type::kComma)) {
-      continue;
+      if (match(Token::Type::kComma))
+        continue;
+
+      if (is_decoration(peek())) {
+        // We have two decorations in a bracket without a separating comma.
+        // e.g. [[location(1) set(2)]]
+        //                    ^^^ expected comma
+        expect(use, Token::Type::kComma);
+        return Failure::kErrored;
+      }
+
+      break;
     }
 
-    if (is_decoration(peek())) {
-      // We have two decorations in a bracket without a separating comma.
-      // e.g. [[location(1) set(2)]]
-      //                    ^^^ expected comma
-      expect("decoration list", Token::Type::kComma);
+    if (errored)
       return Failure::kErrored;
-    }
 
-    if (!expect("decoration list", Token::Type::kAttrRight))
+    if (!expect(use, Token::Type::kAttrRight))
       return Failure::kErrored;
 
     return true;
-  }
+  });
 }
 
 Expect<std::unique_ptr<ast::Decoration>> ParserImpl::expect_decoration() {
@@ -2791,25 +2882,29 @@
 }
 
 bool ParserImpl::expect(const std::string& use, Token::Type tok) {
-  auto t = next();
-  if (!t.Is(tok)) {
-    std::stringstream err;
-    err << "expected '" << Token::TypeToName(tok) << "'";
-    if (!use.empty()) {
-      err << " for " << use;
-    }
-    add_error(t, err.str());
-    return false;
+  auto t = peek();
+  if (t.Is(tok)) {
+    next();
+    synchronized_ = true;
+    return true;
   }
-  return true;
+
+  std::stringstream err;
+  err << "expected '" << Token::TypeToName(tok) << "'";
+  if (!use.empty()) {
+    err << " for " << use;
+  }
+  add_error(t, err.str());
+  synchronized_ = false;
+  return false;
 }
 
 Expect<int32_t> ParserImpl::expect_sint(const std::string& use) {
-  auto t = next();
-
+  auto t = peek();
   if (!t.IsSintLiteral())
     return add_error(t.source(), "expected signed integer literal", use);
 
+  next();
   return {t.to_i32(), t.source()};
 }
 
@@ -2837,11 +2932,14 @@
 }
 
 Expect<std::string> ParserImpl::expect_ident(const std::string& use) {
-  auto t = next();
-  if (!t.IsIdentifier())
-    return add_error(t.source(), "expected identifier", use);
-
-  return {t.to_str(), t.source()};
+  auto t = peek();
+  if (t.IsIdentifier()) {
+    synchronized_ = true;
+    next();
+    return {t.to_str(), t.source()};
+  }
+  synchronized_ = false;
+  return add_error(t.source(), "expected identifier", use);
 }
 
 template <typename F, typename T>
@@ -2852,14 +2950,18 @@
   if (!expect(use, start)) {
     return Failure::kErrored;
   }
-  auto res = body();
-  if (res.errored) {
-    return Failure::kErrored;
-  }
-  if (!expect(use, end)) {
-    return Failure::kErrored;
-  }
-  return res;
+
+  return sync(end, [&]() -> T {
+    auto res = body();
+
+    if (res.errored)
+      return Failure::kErrored;
+
+    if (!expect(use, end))
+      return Failure::kErrored;
+
+    return res;
+  });
 }
 
 template <typename F, typename T>
@@ -2880,6 +2982,64 @@
                       std::forward<F>(body));
 }
 
+template <typename F, typename T>
+T ParserImpl::sync(Token::Type tok, F&& body) {
+  sync_tokens_.push_back(tok);
+
+  auto result = body();
+
+  assert(sync_tokens_.back() == tok);
+  sync_tokens_.pop_back();
+
+  if (result.errored) {
+    sync_to(tok, /* consume: */ true);
+  }
+
+  return result;
+}
+
+bool ParserImpl::sync_to(Token::Type tok, bool consume) {
+  // Clear the synchronized state - gets set to true again on success.
+  synchronized_ = false;
+
+  BlockCounters counters;
+
+  for (size_t i = 0; i < kMaxResynchronizeLookahead; i++) {
+    auto t = peek(i);
+    if (counters.consume(t) > 0)
+      continue;  // Nested block
+    if (!t.Is(tok) && !is_sync_token(t))
+      continue;  // Not a synchronization point
+
+    // Synchronization point found.
+
+    // Skip any tokens we don't understand, bringing us to just before the
+    // resync point.
+    while (i-- > 0) {
+      next();
+    }
+
+    // Is this synchronization token |tok|?
+    if (t.Is(tok)) {
+      if (consume)
+        next();
+      synchronized_ = true;
+      return true;
+    }
+    break;
+  }
+
+  return false;
+}
+
+bool ParserImpl::is_sync_token(const Token& t) const {
+  for (auto r : sync_tokens_) {
+    if (t.Is(r))
+      return true;
+  }
+  return false;
+}
+
 }  // namespace wgsl
 }  // namespace reader
 }  // namespace tint
diff --git a/src/reader/wgsl/parser_impl.h b/src/reader/wgsl/parser_impl.h
index 0f5b152..0e7dd45 100644
--- a/src/reader/wgsl/parser_impl.h
+++ b/src/reader/wgsl/parser_impl.h
@@ -606,31 +606,35 @@
   /// pointer, regardless of success or error
   bool match(Token::Type tok, Source* source = nullptr);
   /// Errors if the next token is not equal to |tok|.
-  /// Always consumes the next token.
+  /// Consumes the next token on match.
+  /// expect() also updates |synchronized_|, setting it to `true` if the next
+  /// token is equal to |tok|, otherwise `false`.
   /// @param use a description of what was being parsed if an error was raised.
   /// @param tok the token to test against
   /// @returns true if the next token equals |tok|.
   bool expect(const std::string& use, Token::Type tok);
   /// Parses a signed integer from the next token in the stream, erroring if the
   /// next token is not a signed integer.
-  /// Always consumes the next token.
+  /// Consumes the next token on match.
   /// @param use a description of what was being parsed if an error was raised
   /// @returns the parsed integer.
   Expect<int32_t> expect_sint(const std::string& use);
   /// Parses a signed integer from the next token in the stream, erroring if
   /// the next token is not a signed integer or is negative.
-  /// Always consumes the next token.
+  /// Consumes the next token if it is a signed integer (not necessarily
+  /// negative).
   /// @param use a description of what was being parsed if an error was raised
   /// @returns the parsed integer.
   Expect<uint32_t> expect_positive_sint(const std::string& use);
   /// Parses a non-zero signed integer from the next token in the stream,
   /// erroring if the next token is not a signed integer or is less than 1.
-  /// Always consumes the next token.
+  /// Consumes the next token if it is a signed integer (not necessarily
+  /// >= 1).
   /// @param use a description of what was being parsed if an error was raised
   /// @returns the parsed integer.
   Expect<uint32_t> expect_nonzero_positive_sint(const std::string& use);
   /// Errors if the next token is not an identifier.
-  /// Always consumes the next token.
+  /// Consumes the next token on match.
   /// @param use a description of what was being parsed if an error was raised
   /// @returns the parsed identifier.
   Expect<std::string> expect_ident(const std::string& use);
@@ -684,6 +688,45 @@
   template <typename F, typename T = ReturnType<F>>
   T expect_lt_gt_block(const std::string& use, F&& body);
 
+  /// sync() calls the function |func|, and attempts to resynchronize the parser
+  /// to the next found resynchronization token if |func| fails.
+  /// If the next found resynchronization token is |tok|, then sync will also
+  /// consume |tok|.
+  ///
+  /// sync() will transiently add |tok| to the parser's stack of synchronization
+  /// tokens for the duration of the call to |func|. Once |func| returns, |tok|
+  /// is removed from the stack of resynchronization tokens. sync calls may be
+  /// nested, and so the number of resynchronization tokens is equal to the
+  /// number of |sync| calls in the current stack frame.
+  ///
+  /// sync() updates |synchronized_|, setting it to `true` if the next
+  /// resynchronization token found was |tok|, otherwise `false`.
+  ///
+  /// @param tok the token to attempt to synchronize the parser to if |func|
+  /// fails.
+  /// @param func a function or lambda with the signature: `Expect<Result>()` or
+  /// `Maybe<Result>()`.
+  /// @return the value returned by |func|.
+  template <typename F, typename T = ReturnType<F>>
+  T sync(Token::Type tok, F&& func);
+  /// sync_to() attempts to resynchronize the parser to the next found
+  /// resynchronization token or |tok| (whichever comes first).
+  ///
+  /// Synchronization tokens are transiently defined by calls to |sync|.
+  ///
+  /// sync_to() updates |synchronized_|, setting it to `true` if a
+  /// resynchronization token was found and it was |tok|, otherwise `false`.
+  ///
+  /// @param tok the token to attempt to synchronize the parser to.
+  /// @param consume if true and the next found resynchronization token is
+  /// |tok| then sync_to() will also consume |tok|.
+  /// @return the state of |synchronized_|.
+  /// @see sync().
+  bool sync_to(Token::Type tok, bool consume);
+  /// @return true if |t| is in the stack of resynchronization tokens.
+  /// @see sync().
+  bool is_sync_token(const Token& t) const;
+
   /// Downcasts all the decorations in |list| to the type |T|, raising a parser
   /// error if any of the decorations aren't of the type |T|.
   template <typename T>
@@ -712,6 +755,8 @@
   diag::List diags_;
   std::unique_ptr<Lexer> lexer_;
   std::deque<Token> token_queue_;
+  bool synchronized_ = true;
+  std::vector<Token::Type> sync_tokens_;
   std::unordered_map<std::string, ast::type::Type*> registered_constructs_;
   ast::Module module_;
 };
diff --git a/src/reader/wgsl/parser_impl_error_resync_test.cc b/src/reader/wgsl/parser_impl_error_resync_test.cc
new file mode 100644
index 0000000..3427a59
--- /dev/null
+++ b/src/reader/wgsl/parser_impl_error_resync_test.cc
@@ -0,0 +1,186 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "gtest/gtest.h"
+#include "src/reader/wgsl/parser_impl.h"
+#include "src/reader/wgsl/parser_impl_test_helper.h"
+
+namespace tint {
+namespace reader {
+namespace wgsl {
+namespace {
+
+class ParserImplErrorResyncTest : public ParserImplTest {};
+
+#define EXPECT(SOURCE, EXPECTED)                                     \
+  do {                                                               \
+    std::string source = SOURCE;                                     \
+    std::string expected = EXPECTED;                                 \
+    auto* p = parser(source);                                        \
+    EXPECT_EQ(false, p->Parse());                                    \
+    EXPECT_EQ(true, p->diagnostics().contains_errors());             \
+    EXPECT_EQ(expected, diag::Formatter().format(p->diagnostics())); \
+  } while (false)
+
+TEST_F(ParserImplErrorResyncTest, BadFunctionDecls) {
+  EXPECT(R"(
+fn .() -> . {}
+fn x(.) . {}
+[[.,.]] fn -> {}
+fn good() -> void {}
+)",
+         "test.wgsl:2:4 error: expected identifier for function declaration\n"
+         "fn .() -> . {}\n"
+         "   ^\n"
+         "\n"
+         "test.wgsl:2:11 error: unable to determine function return type\n"
+         "fn .() -> . {}\n"
+         "          ^\n"
+         "\n"
+         "test.wgsl:3:6 error: expected ')' for function declaration\n"
+         "fn x(.) . {}\n"
+         "     ^\n"
+         "\n"
+         "test.wgsl:3:9 error: expected '->' for function declaration\n"
+         "fn x(.) . {}\n"
+         "        ^\n"
+         "\n"
+         "test.wgsl:4:3 error: expected decoration\n"
+         "[[.,.]] fn -> {}\n"
+         "  ^\n"
+         "\n"
+         "test.wgsl:4:5 error: expected decoration\n"
+         "[[.,.]] fn -> {}\n"
+         "    ^\n"
+         "\n"
+         "test.wgsl:4:12 error: expected identifier for function declaration\n"
+         "[[.,.]] fn -> {}\n"
+         "           ^^\n");
+}
+
+TEST_F(ParserImplErrorResyncTest, AssignmentStatement) {
+  EXPECT(R"(
+fn f() -> void {
+  blah blah blah blah;
+  good = 1;
+  blah blah blah blah;
+  x = .;
+  good = 1;
+}
+)",
+         "test.wgsl:3:8 error: expected '=' for assignment\n"
+         "  blah blah blah blah;\n"
+         "       ^^^^\n"
+         "\n"
+         "test.wgsl:5:8 error: expected '=' for assignment\n"
+         "  blah blah blah blah;\n"
+         "       ^^^^\n"
+         "\n"
+         "test.wgsl:6:7 error: unable to parse right side of assignment\n"
+         "  x = .;\n"
+         "      ^\n");
+}
+
+TEST_F(ParserImplErrorResyncTest, DiscardStatement) {
+  EXPECT(R"(
+fn f() -> void {
+  discard blah blah blah;
+  a = 1;
+  discard blah blah blah;
+}
+)",
+         "test.wgsl:3:11 error: expected ';' for discard statement\n"
+         "  discard blah blah blah;\n"
+         "          ^^^^\n"
+         "\n"
+         "test.wgsl:5:11 error: expected ';' for discard statement\n"
+         "  discard blah blah blah;\n"
+         "          ^^^^\n");
+}
+
+TEST_F(ParserImplErrorResyncTest, StructMembers) {
+  EXPECT(R"(
+struct S {
+    blah blah blah;
+    a : i32;
+    blah blah blah;
+    b : i32;
+    [[block]] x : i32;
+    c : i32;
+}
+)",
+         "test.wgsl:3:10 error: expected ':' for struct member\n"
+         "    blah blah blah;\n"
+         "         ^^^^\n"
+         "\n"
+         "test.wgsl:5:10 error: expected ':' for struct member\n"
+         "    blah blah blah;\n"
+         "         ^^^^\n"
+         "\n"
+         "test.wgsl:7:7 error: struct decoration type cannot be used for "
+         "struct member\n"
+         "    [[block]] x : i32;\n"
+         "      ^^^^^\n");
+}
+
+// Check that the forward scan in resynchronize() stop at nested sync points.
+// In this test the inner resynchronize() is looking for a terminating ';', and
+// the outer resynchronize() is looking for a terminating '}' for the function
+// scope.
+TEST_F(ParserImplErrorResyncTest, NestedSyncPoints) {
+  EXPECT(R"(
+fn f() -> void {
+  x = 1;
+  discard
+}
+struct S { blah };
+)",
+         "test.wgsl:5:1 error: expected ';' for discard statement\n"
+         "}\n"
+         "^\n"
+         "\n"
+         "test.wgsl:6:17 error: expected ':' for struct member\n"
+         "struct S { blah };\n"
+         "                ^\n");
+}
+
+TEST_F(ParserImplErrorResyncTest, BracketCounting) {
+  EXPECT(R"(
+[[woof[[[[]]]]]]
+fn f(x(((())))) -> void {
+  meow = {{{}}}
+}
+struct S { blah };
+)",
+         "test.wgsl:2:3 error: expected decoration\n"
+         "[[woof[[[[]]]]]]\n"
+         "  ^^^^\n"
+         "\n"
+         "test.wgsl:3:7 error: expected ':' for parameter\n"
+         "fn f(x(((())))) -> void {\n"
+         "      ^\n"
+         "\n"
+         "test.wgsl:4:10 error: unable to parse right side of assignment\n"
+         "  meow = {{{}}}\n"
+         "         ^\n"
+         "\n"
+         "test.wgsl:6:17 error: expected ':' for struct member\n"
+         "struct S { blah };\n"
+         "                ^\n");
+}
+
+}  // namespace
+}  // namespace wgsl
+}  // namespace reader
+}  // namespace tint
diff --git a/src/reader/wgsl/parser_impl_global_variable_decl_test.cc b/src/reader/wgsl/parser_impl_global_variable_decl_test.cc
index 446d93f..d714e96 100644
--- a/src/reader/wgsl/parser_impl_global_variable_decl_test.cc
+++ b/src/reader/wgsl/parser_impl_global_variable_decl_test.cc
@@ -149,8 +149,8 @@
 
   auto e = p->global_variable_decl(decos.value);
   EXPECT_FALSE(e.errored);
-  EXPECT_FALSE(e.matched);
-  EXPECT_EQ(e.value, nullptr);
+  EXPECT_TRUE(e.matched);
+  EXPECT_NE(e.value, nullptr);
 
   EXPECT_TRUE(p->has_error());
   EXPECT_EQ(p->error(),
diff --git a/src/reader/wgsl/parser_impl_struct_decl_test.cc b/src/reader/wgsl/parser_impl_struct_decl_test.cc
index 37c5f06..59a91eb 100644
--- a/src/reader/wgsl/parser_impl_struct_decl_test.cc
+++ b/src/reader/wgsl/parser_impl_struct_decl_test.cc
@@ -165,8 +165,8 @@
 
   auto s = p->struct_decl(decos.value);
   EXPECT_FALSE(s.errored);
-  EXPECT_FALSE(s.matched);
-  EXPECT_EQ(s.value, nullptr);
+  EXPECT_TRUE(s.matched);
+  EXPECT_NE(s.value, nullptr);
 
   EXPECT_TRUE(p->has_error());
   EXPECT_EQ(p->error(), "1:9: expected ']]' for decoration list");
diff --git a/src/reader/wgsl/parser_impl_struct_member_test.cc b/src/reader/wgsl/parser_impl_struct_member_test.cc
index d8c3fe7..2ae818a 100644
--- a/src/reader/wgsl/parser_impl_struct_member_test.cc
+++ b/src/reader/wgsl/parser_impl_struct_member_test.cc
@@ -110,9 +110,10 @@
   EXPECT_FALSE(decos.matched);
 
   auto m = p->expect_struct_member(decos.value);
+  ASSERT_FALSE(m.errored);
+  ASSERT_NE(m.value, nullptr);
+
   ASSERT_TRUE(p->has_error());
-  ASSERT_TRUE(m.errored);
-  ASSERT_EQ(m.value, nullptr);
   EXPECT_EQ(p->error(),
             "1:10: expected signed integer literal for offset decoration");
 }