anchor.diff:  
     void test_comment_multi() {
       _assertComment(TokenType.MULTI_LINE_COMMENT, "/* comment */");
  +  }
  +
  +  void test_comment_multi_consecutive_2() {
  +    Token token = _scan("/* x */ /* y */ z");
  +    expect(token.type, TokenType.IDENTIFIER);
  +    expect(token.precedingComments, isNotNull);
  +    expect(token.precedingComments.value(), "/* x */");
  +    expect(token.precedingComments.previous, isNull);
  +    expect(token.precedingComments.next, isNotNull);
  +    expect(token.precedingComments.next.value(), "/* y */");
  +    expect(
  +        token.precedingComments.next.previous, same(token.precedingComments));
  +    expect(token.precedingComments.next.next, isNull);
  +  }
  +
  +  void test_comment_multi_consecutive_3() {
  +    Token token = _scan("/* x */ /* y */ /* z */ a");
  +    expect(token.type, TokenType.IDENTIFIER);
  +    expect(token.precedingComments, isNotNull);
  +    expect(token.precedingComments.value(), "/* x */");
  +    expect(token.precedingComments.previous, isNull);
  +    expect(token.precedingComments.next, isNotNull);
  +    expect(token.precedingComments.next.value(), "/* y */");
  +    expect(
  +        token.precedingComments.next.previous, same(token.precedingComments));
  +    expect(token.precedingComments.next.next, isNotNull);
  +    expect(token.precedingComments.next.next.value(), "/* z */");
  +    expect(token.precedingComments.next.next.previous,
  +        same(token.precedingComments.next));
  +    expect(token.precedingComments.next.next.next, isNull);
     }
   
     void test_comment_multi_lineEnds() {
  
positive.code:     defineReflectiveTests(CharSequenceReaderTest);
      defineReflectiveTests(KeywordStateTest);
      defineReflectiveTests(ScannerTest);
      defineReflectiveTests(TokenTypeTest);
    });
  }
  
negative.code: }
  
  @reflectiveTest
  class ScannerTest extends ScannerTestBase {
    @override
    Token _scanWithListener(String source, _ErrorListener listener,
        {bool genericMethodComments: false,
        bool lazyAssignmentOperators: false}) {
      Scanner scanner =
          new _TestScanner(new CharSequenceReader(source), listener);
      scanner.scanGenericMethodComments = genericMethodComments;
      scanner.scanLazyAssignmentOperators = lazyAssignmentOperators;
      return scanner.tokenize();
    }
  }
  
  @reflectiveTest
  class ScannerTest_Fasta extends ScannerTestBase {
    final _keywordMap = {
      "assert": Keyword.ASSERT,
      "break": Keyword.BREAK,
      "case": Keyword.CASE,
      "catch": Keyword.CATCH,
      "class": Keyword.CLASS,
      "const": Keyword.CONST,
      "continue": Keyword.CONTINUE,
      "default": Keyword.DEFAULT,
      "do": Keyword.DO,
      "else": Keyword.ELSE,
      "enum": Keyword.ENUM,
      "extends": Keyword.EXTENDS,
      "false": Keyword.FALSE,
      "final": Keyword.FINAL,
      "finally": Keyword.FINALLY,
      "for": Keyword.FOR,
      "if": Keyword.IF,
      "in": Keyword.IN,
      "new": Keyword.NEW,
      "null": Keyword.NULL,
      "rethrow": Keyword.RETHROW,
      "return": Keyword.RETURN,
      "super": Keyword.SUPER,
      "switch": Keyword.SWITCH,
      "this": Keyword.THIS,
      "throw": Keyword.THROW,
      "true": Keyword.TRUE,
      "try": Keyword.TRY,
      "var": Keyword.VAR,
      "void": Keyword.VOID,
      "while": Keyword.WHILE,
      "with": Keyword.WITH,
      "is": Keyword.IS,
      "abstract": Keyword.ABSTRACT,
      "as": Keyword.AS,
      "covariant": Keyword.COVARIANT,
      "dynamic": Keyword.DYNAMIC,
      "export": Keyword.EXPORT,
      "external": Keyword.EXTERNAL,
      "factory": Keyword.FACTORY,
      "get": Keyword.GET,
      "implements": Keyword.IMPLEMENTS,
      "import": Keyword.IMPORT,
      "library": Keyword.LIBRARY,
      "operator": Keyword.OPERATOR,
      "part": Keyword.PART,
      "set": Keyword.SET,
      "static": Keyword.STATIC,
      "typedef": Keyword.TYPEDEF,
      "deferred": Keyword.DEFERRED,
    };
  
    @override
    @failingTest
    void test_ampersand_ampersand_eq() {
      // TODO(paulberry,ahe): Fasta doesn't support `&&=` yet
      super.test_ampersand_ampersand_eq();
    }
  
    @override
    @failingTest
    void test_bar_bar_eq() {
      // TODO(paulberry,ahe): Fasta doesn't support `||=` yet
      super.test_bar_bar_eq();
    }
  
    @override
    @failingTest
    void test_comment_generic_method_type_assign() {
      // TODO(paulberry,ahe): Fasta doesn't support generic method comment syntax.
      super.test_comment_generic_method_type_assign();
    }
  
    @override
    @failingTest
    void test_comment_generic_method_type_list() {
      // TODO(paulberry,ahe): Fasta doesn't support generic method comment syntax.
      super.test_comment_generic_method_type_list();
    }
  
    @override
    @failingTest
    void test_comment_multi_unterminated() {
      // TODO(paulberry,ahe): see UnimplementedError("distinguish unterminated
      // errors")
      super.test_comment_multi_unterminated();
    }
  
    @override
    @failingTest
    void test_comment_single() {
      // TODO(paulberry,ahe): See TODO comment below in _translateTokenInfoKind().
      super.test_comment_single();
    }
  
    @override
    @failingTest
    void test_double_missingDigitInExponent() {
      // TODO(paulberry,ahe): see UnimplementedError("distinguish unterminated
      // errors")
      super.test_double_missingDigitInExponent();
    }
  
    @override
    @failingTest
    void test_hexidecimal_missingDigit() {
      // TODO(paulberry,ahe): see UnimplementedError("distinguish unterminated
      // errors")
      super.test_hexidecimal_missingDigit();
    }
  
    @override
    @failingTest
    void test_index() {
      // TODO(paulberry,ahe): "[]" should be parsed as a single token.
      super.test_index();
    }
  
    @override
    @failingTest
    void test_index_eq() {
      // TODO(paulberry,ahe): "[]=" should be parsed as a single token.
      super.test_index_eq();
    }
  
    @override
    @failingTest
    void test_scriptTag_withArgs() {
      // TODO(paulberry,ahe): script tags are needed by analyzer.
      super.test_scriptTag_withArgs();
    }
  
    @override
    @failingTest
    void test_scriptTag_withoutSpace() {
      // TODO(paulberry,ahe): script tags are needed by analyzer.
      super.test_scriptTag_withoutSpace();
    }
  
    @override
    @failingTest
    void test_scriptTag_withSpace() {
      // TODO(paulberry,ahe): script tags are needed by analyzer.
      super.test_scriptTag_withSpace();
    }
  
    @override
    @failingTest
    void test_string_multi_unterminated() {
      // TODO(paulberry,ahe): bad error recovery.
      super.test_string_multi_unterminated();
    }
  
    @override
    @failingTest
    void test_string_multi_unterminated_interpolation_block() {
      // TODO(paulberry,ahe): bad error recovery.
      super.test_string_multi_unterminated_interpolation_block();
    }
  
    @override
    @failingTest
    void test_string_multi_unterminated_interpolation_identifier() {
      // TODO(paulberry,ahe): bad error recovery.
      super.test_string_multi_unterminated_interpolation_identifier();
    }
  
    @override
    @failingTest
    void test_string_raw_multi_unterminated() {
      // TODO(paulberry,ahe): bad error recovery.
      super.test_string_raw_multi_unterminated();
    }
  
    @override
    @failingTest
    void test_string_raw_simple_unterminated_eof() {
      // TODO(paulberry,ahe): bad error recovery.
      super.test_string_raw_simple_unterminated_eof();
    }
  
    @override
    @failingTest
    void test_string_raw_simple_unterminated_eol() {
      // TODO(paulberry,ahe): bad error recovery.
      super.test_string_raw_simple_unterminated_eol();
    }
  
    @override
    @failingTest
    void test_string_simple_interpolation_missingIdentifier() {
      // TODO(paulberry,ahe): bad error recovery.
      super.test_string_simple_interpolation_missingIdentifier();
    }
  
    @override
    @failingTest
    void test_string_simple_interpolation_nonIdentifier() {
      // TODO(paulberry,ahe): bad error recovery.
      super.test_string_simple_interpolation_nonIdentifier();
    }
  
    @override
    @failingTest
    void test_string_simple_unterminated_eof() {
      // TODO(paulberry,ahe): bad error recovery.
      super.test_string_simple_unterminated_eof();
    }
  
    @override
    @failingTest
    void test_string_simple_unterminated_eol() {
      // TODO(paulberry,ahe): bad error recovery.
      super.test_string_simple_unterminated_eol();
    }
  
    @override
    @failingTest
    void test_string_simple_unterminated_interpolation_block() {
      // TODO(paulberry,ahe): bad error recovery.
      super.test_string_simple_unterminated_interpolation_block();
    }
  
    @override
    @failingTest
    void test_string_simple_unterminated_interpolation_identifier() {
      // TODO(paulberry,ahe): bad error recovery.
      super.test_string_simple_unterminated_interpolation_identifier();
    }
  
    @override
    Token _scanWithListener(String source, _ErrorListener listener,
        {bool genericMethodComments: false,
        bool lazyAssignmentOperators: false}) {
      if (genericMethodComments) {
        // Fasta doesn't support generic method comments.
        // TODO(paulberry): once the analyzer toolchain no longer needs generic
        // method comments, remove tests that exercise them.
        fail('No generic method comment support in Fasta');
      }
      // Note: Fasta always supports lazy assignment operators (`&&=` and `||=`),
      // so we can ignore the `lazyAssignmentOperators` flag.
      // TODO(paulberry): once lazyAssignmentOperators are fully supported by
      // Dart, remove this flag.
      var scanner = new fasta.StringScanner(source, includeComments: true);
      var token = scanner.tokenize();
      var analyzerTokenHead = new Token(null, 0);
      analyzerTokenHead.previous = analyzerTokenHead;
      var analyzerTokenTail = analyzerTokenHead;
      // TODO(paulberry,ahe): Fasta includes comments directly in the token
      // stream, rather than pointing to them via a "precedingComment" pointer, as
      // analyzer does.  This seems like it will complicate parsing and other
      // operations.
      CommentToken currentCommentHead;
      CommentToken currentCommentTail;
      while (true) {
        if (token is fasta.ErrorToken) {
          var error = _translateErrorToken(token, source.length);
          if (error != null) {
            listener.errors.add(error);
          }
        } else if (token is fasta.StringToken &&
            token.info.kind == fasta.COMMENT_TOKEN) {
          var translatedToken = _translateToken(token, null) as CommentToken;
          if (currentCommentHead == null) {
            currentCommentHead = currentCommentTail = translatedToken;
          } else {
            currentCommentTail.setNext(translatedToken);
            currentCommentTail = translatedToken;
          }
        } else {
          var translatedToken = _translateToken(token, currentCommentHead);
          translatedToken.setNext(translatedToken);
          currentCommentHead = currentCommentTail = null;
          analyzerTokenTail.setNext(translatedToken);
          translatedToken.previous = analyzerTokenTail;
          analyzerTokenTail = translatedToken;
        }
        if (token.isEof) {
          return analyzerTokenHead.next;
        }
        token = token.next;
      }
    }
  
    _TestError _translateErrorToken(fasta.ErrorToken token, int inputLength) {
      int charOffset = token.charOffset;
      // TODO(paulberry,ahe): why is endOffset sometimes null?
      int endOffset = token.endOffset ?? charOffset;
      _TestError _makeError(ScannerErrorCode errorCode, List<Object> arguments) {
        int errorLength = endOffset - charOffset;
        if (charOffset == inputLength) {
          // Analyzer never generates an error message past the end of the input,
          // since such an error would not be visible in an editor.
          // TODO(paulberry,ahe): would it make sense to replicate this behavior
          // in fasta, or move it elsewhere in analyzer?
          charOffset--;
        }
        if (errorLength == 0) {
          // Analyzer never generates an error message of length zero,
          // since such an error would not be visible in an editor.
          // TODO(paulberry,ahe): would it make sense to replicate this behavior
          // in fasta, or move it elsewhere in analyzer?
          errorLength = 1;
        }
        return new _TestError(charOffset, errorLength, errorCode, arguments);
      }
  
      if (token is fasta.UnterminatedToken) {
        // TODO(paulberry,ahe): How to tell what kind of error to
        // report?  It could be ScannerErrorCode.UNTERMINATED_STRING_LITERAL,
        // ScannerErrorCode.MISSING_HEX_DIGIT, or
        // ScannerErrorCode.UNTERMINATED_MULTI_LINE_COMMENT
        return _makeError(
            throw new UnimplementedError("distinguish unterminated errors"),
            null);
      } else if (token is fasta.UnmatchedToken) {
        return null;
      } else if (token is fasta.NonAsciiIdentifierToken) {
        return _makeError(ScannerErrorCode.ILLEGAL_CHARACTER, [token.character]);
      } else if (token is fasta.NonAsciiWhitespaceToken) {
        return _makeError(ScannerErrorCode.ILLEGAL_CHARACTER, [token.character]);
      }
      throw new UnimplementedError('${token.runtimeType}');
    }
  
    Keyword _translateKeyword(String syntax) =>
        _keywordMap[syntax] ?? (throw new UnimplementedError('$syntax'));
  
    Token _translateToken(fasta.Token token, CommentToken comment) {
      var type = _translateTokenInfoKind(token.info.kind);
      int offset = token.charOffset;
      Token makeStringToken(String value) {
        if (comment == null) {
          return new StringToken(type, value, offset);
        } else {
          return new StringTokenWithComment(type, value, offset, comment);
        }
      }
  
      Token makeKeywordToken(Keyword keyword) {
        if (comment == null) {
          return new KeywordToken(keyword, offset);
        } else {
          return new KeywordTokenWithComment(keyword, offset, comment);
        }
      }
  
      Token makeBeginToken() {
        if (comment == null) {
          return new BeginToken(type, offset);
        } else {
          return new BeginTokenWithComment(type, offset, comment);
        }
      }
  
      Token makeCommentToken() {
        return new CommentToken(type, token.value, offset);
      }
  
      if (token is fasta.StringToken) {
        if (token.info.kind == fasta.COMMENT_TOKEN) {
          return makeCommentToken();
        } else {
          return makeStringToken(token.value);
        }
      } else if (token is fasta.KeywordToken) {
        return makeKeywordToken(_translateKeyword(token.keyword.syntax));
      } else if (token is fasta.SymbolToken) {
        if (token is fasta.BeginGroupToken) {
          if (type == TokenType.LT) {
            return makeStringToken(token.value);
          } else {
            return makeBeginToken();
          }
        } else {
          return makeStringToken(token.value);
        }
      }
      throw new UnimplementedError('${token.runtimeType}');
    }
  
    TokenType _translateTokenInfoKind(int kind) {
      switch (kind) {
        case fasta.EOF_TOKEN:
          return TokenType.EOF;
        case fasta.KEYWORD_TOKEN:
          return TokenType.KEYWORD;
        case fasta.IDENTIFIER_TOKEN:
          return TokenType.IDENTIFIER;
        case fasta.BAD_INPUT_TOKEN:
          return TokenType.STRING;
        case fasta.DOUBLE_TOKEN:
          return TokenType.DOUBLE;
        case fasta.INT_TOKEN:
          return TokenType.INT;
        case fasta.HEXADECIMAL_TOKEN:
          return TokenType.HEXADECIMAL;
        case fasta.STRING_TOKEN:
          return TokenType.STRING;
        case fasta.AMPERSAND_TOKEN:
          return TokenType.AMPERSAND;
        case fasta.BACKPING_TOKEN:
          return TokenType.BACKPING;
        case fasta.BACKSLASH_TOKEN:
          return TokenType.BACKSLASH;
        case fasta.BANG_TOKEN:
          return TokenType.BANG;
        case fasta.BAR_TOKEN:
          return TokenType.BAR;
        case fasta.COLON_TOKEN:
          return TokenType.COLON;
        case fasta.COMMA_TOKEN:
          return TokenType.COMMA;
        case fasta.EQ_TOKEN:
          return TokenType.EQ;
        case fasta.GT_TOKEN:
          return TokenType.GT;
        case fasta.HASH_TOKEN:
          return TokenType.HASH;
        case fasta.OPEN_CURLY_BRACKET_TOKEN:
          return TokenType.OPEN_CURLY_BRACKET;
        case fasta.OPEN_SQUARE_BRACKET_TOKEN:
          return TokenType.OPEN_SQUARE_BRACKET;
        case fasta.OPEN_PAREN_TOKEN:
          return TokenType.OPEN_PAREN;
        case fasta.LT_TOKEN:
          return TokenType.LT;
        case fasta.MINUS_TOKEN:
          return TokenType.MINUS;
        case fasta.PERIOD_TOKEN:
          return TokenType.PERIOD;
        case fasta.PLUS_TOKEN:
          return TokenType.PLUS;
        case fasta.QUESTION_TOKEN:
          return TokenType.QUESTION;
        case fasta.AT_TOKEN:
          return TokenType.AT;
        case fasta.CLOSE_CURLY_BRACKET_TOKEN:
          return TokenType.CLOSE_CURLY_BRACKET;
        case fasta.CLOSE_SQUARE_BRACKET_TOKEN:
          return TokenType.CLOSE_SQUARE_BRACKET;
        case fasta.CLOSE_PAREN_TOKEN:
          return TokenType.CLOSE_PAREN;
        case fasta.SEMICOLON_TOKEN:
          return TokenType.SEMICOLON;
        case fasta.SLASH_TOKEN:
          return TokenType.SLASH;
        case fasta.TILDE_TOKEN:
          return TokenType.TILDE;
        case fasta.STAR_TOKEN:
          return TokenType.STAR;
        case fasta.PERCENT_TOKEN:
          return TokenType.PERCENT;
        case fasta.CARET_TOKEN:
          return TokenType.CARET;
        case fasta.STRING_INTERPOLATION_TOKEN:
          return TokenType.STRING_INTERPOLATION_EXPRESSION;
        case fasta.LT_EQ_TOKEN:
          return TokenType.LT_EQ;
        case fasta.FUNCTION_TOKEN:
          return TokenType.FUNCTION;
        case fasta.SLASH_EQ_TOKEN:
          return TokenType.SLASH_EQ;
        case fasta.PERIOD_PERIOD_PERIOD_TOKEN:
          return TokenType.PERIOD_PERIOD_PERIOD;
        case fasta.PERIOD_PERIOD_TOKEN:
          return TokenType.PERIOD_PERIOD;
        case fasta.EQ_EQ_EQ_TOKEN:
          // TODO(paulberry,ahe): what is this?
          throw new UnimplementedError();
        case fasta.EQ_EQ_TOKEN:
          return TokenType.EQ_EQ;
        case fasta.LT_LT_EQ_TOKEN:
          return TokenType.LT_LT_EQ;
        case fasta.LT_LT_TOKEN:
          return TokenType.LT_LT;
        case fasta.GT_EQ_TOKEN:
          return TokenType.GT_EQ;
        case fasta.GT_GT_EQ_TOKEN:
          return TokenType.GT_GT_EQ;
        case fasta.INDEX_EQ_TOKEN:
          return TokenType.INDEX_EQ;
        case fasta.INDEX_TOKEN:
          return TokenType.INDEX;
        case fasta.BANG_EQ_EQ_TOKEN:
          // TODO(paulberry,ahe): what is this?
          throw new UnimplementedError();
        case fasta.BANG_EQ_TOKEN:
          return TokenType.BANG_EQ;
        case fasta.AMPERSAND_AMPERSAND_TOKEN:
          return TokenType.AMPERSAND_AMPERSAND;
        case fasta.AMPERSAND_EQ_TOKEN:
          return TokenType.AMPERSAND_EQ;
        case fasta.BAR_BAR_TOKEN:
          return TokenType.BAR_BAR;
        case fasta.BAR_EQ_TOKEN:
          return TokenType.BAR_EQ;
        case fasta.STAR_EQ_TOKEN:
          return TokenType.STAR_EQ;
        case fasta.PLUS_PLUS_TOKEN:
          return TokenType.PLUS_PLUS;
        case fasta.PLUS_EQ_TOKEN:
          return TokenType.PLUS_EQ;
        case fasta.MINUS_MINUS_TOKEN:
          return TokenType.MINUS_MINUS;
        case fasta.MINUS_EQ_TOKEN:
          return TokenType.MINUS_EQ;
        case fasta.TILDE_SLASH_EQ_TOKEN:
          return TokenType.TILDE_SLASH_EQ;
        case fasta.TILDE_SLASH_TOKEN:
          return TokenType.TILDE_SLASH;
        case fasta.PERCENT_EQ_TOKEN:
          return TokenType.PERCENT_EQ;
        case fasta.GT_GT_TOKEN:
          return TokenType.GT_GT;
        case fasta.CARET_EQ_TOKEN:
          return TokenType.CARET_EQ;
        case fasta.COMMENT_TOKEN:
          // TODO(paulberry,ahe): how to distinguish multi-line from
          // single-line comments?  Causes a failure in test_comment_single().
          return TokenType.MULTI_LINE_COMMENT;
        case fasta.STRING_INTERPOLATION_IDENTIFIER_TOKEN:
          return TokenType.STRING_INTERPOLATION_IDENTIFIER;
        case fasta.QUESTION_PERIOD_TOKEN:
          return TokenType.QUESTION_PERIOD;
        case fasta.QUESTION_QUESTION_TOKEN:
          return TokenType.QUESTION_QUESTION;
        case fasta.QUESTION_QUESTION_EQ_TOKEN:
          return TokenType.QUESTION_QUESTION_EQ;
        default:
          throw new UnimplementedError('$kind');
      }
    }
  }
  
  abstract class ScannerTestBase {
    void fail_incomplete_string_interpolation() {
      // https://code.google.com/p/dart/issues/detail?id=18073
      _assertErrorAndTokens(
  