Просмотр исходного кода

Support multi-file lex printing and testing. (#3214)

Lex now prints its yaml as:
```
- filename: name
  tokens: [ ... ]
```

New support in file_test allows the `filename` marker at the top to
define the default file number for later lines, meaning multi-file
output from lexing is now associated with the appropriate file. Similar
support will probably also apply to lowering, semir, and other places
that print a filename once for the full dump.

This hammers a bit at how line number replacements work in file_test,
allowing stacking them so that lex errors and stdout can both be
line-associated properly. I've tried to make the autoupdate more
frequently work in one pass, now also taking into account the file index
when doing line replacements.

There are still some issues with EndOfFile that it may be good to
discuss: because CHECK lines are appended to the end of the file now,
and the EndOfFile token points at the last line including comments, new
lex tests now take two runs to autoupdate (because without CHECK lines,
the EndOfFile points at a content line, which content is then inserted
after). Note that removing CHECK lines from the test is not a solution:
autoupdate also started inserting blank lines, which breaks this for a
similar reason. One solution here might be to not have EndOfFile
associate with a line or column, which has been a bit of an issue
regardless.

Also fixes a small issue with toolchain's autoupdate script.
Jon Ross-Perkins 2 лет назад
Родитель
Сommit
2ecab78297

+ 4 - 7
explorer/file_test.cpp

@@ -75,15 +75,12 @@ class ExplorerFileTest : public FileTestBase {
     return args;
   }
 
-  auto GetLineNumberReplacement(llvm::ArrayRef<llvm::StringRef> filenames)
-      -> LineNumberReplacement override {
+  auto GetLineNumberReplacements(llvm::ArrayRef<llvm::StringRef> filenames)
+      -> llvm::SmallVector<LineNumberReplacement> override {
     if (check_trace_output()) {
-      return {.has_file = false,
-              .pattern = R"((DO NOT MATCH))",
-              // The `{{{{` becomes `{{`.
-              .line_formatv = "{{{{ *}}{0}"};
+      return {};
     }
-    return FileTestBase::GetLineNumberReplacement(filenames);
+    return FileTestBase::GetLineNumberReplacements(filenames);
   }
 
   auto DoExtraCheckReplacements(std::string& check_line) -> void override {

+ 169 - 109
testing/file_test/autoupdate.cpp

@@ -13,7 +13,6 @@
 #include "llvm/ADT/STLFunctionalExtras.h"
 #include "llvm/ADT/StringExtras.h"
 #include "llvm/Support/FormatVariadic.h"
-#include "re2/re2.h"
 
 namespace Carbon::Testing {
 
@@ -26,20 +25,34 @@ static auto ParseLineNumber(absl::string_view matched_line_number) -> int {
   trimmed = trimmed.trim();
   // NOLINTNEXTLINE(google-runtime-int): API requirement.
   long long val;
-  CARBON_CHECK(!llvm::getAsSignedInteger(trimmed, 10, val));
+  CARBON_CHECK(!llvm::getAsSignedInteger(trimmed, 10, val))
+      << matched_line_number;
   return val;
 }
 
+// The file and line number that a CHECK line refers to, and the
+// replacement from which they were determined, if any.
+struct FileAndLineNumber {
+  explicit FileAndLineNumber(int file_number) : file_number(file_number) {}
+
+  explicit FileAndLineNumber(const FileTestLineNumberReplacement* replacement,
+                             int file_number, absl::string_view line_number)
+      : replacement(replacement),
+        file_number(file_number),
+        line_number(ParseLineNumber(line_number)) {}
+
+  const FileTestLineNumberReplacement* replacement = nullptr;
+  int file_number;
+  int line_number = -1;
+};
+
 class CheckLine : public FileTestLineBase {
  public:
   // RE2 is passed by a pointer because it doesn't support std::optional.
-  explicit CheckLine(int file_number, int line_number,
-                     bool line_number_re_has_file, const RE2* line_number_re,
-                     std::string line)
-      : FileTestLineBase(line_number),
-        file_number_(file_number),
-        line_number_re_has_file_(line_number_re_has_file),
-        line_number_re_(line_number_re),
+  explicit CheckLine(FileAndLineNumber file_and_line_number, std::string line)
+      : FileTestLineBase(file_and_line_number.line_number),
+        file_number_(file_and_line_number.file_number),
+        replacement_(file_and_line_number.replacement),
         line_(std::move(line)) {}
 
   auto Print(llvm::raw_ostream& out) const -> void override {
@@ -57,32 +70,31 @@ class CheckLine : public FileTestLineBase {
 
   // When the location of all lines in a file are known, we can set the line
   // offset based on the target line.
-  auto RemapLineNumbers(const std::string& line_formatv,
-                        llvm::function_ref<int(int)> line_remap) -> void {
-    // Only need to do remappings when there's a regex.
-    if (!line_number_re_) {
-      return;
-    }
-
-    // If the CHECK was written to a different file from the file that it refers
-    // to, leave behind an absolute line reference rather than a cross-file
-    // offset.
-    // TODO: We should also remap cross-file line references so that we don't
-    // need multiple runs of autoupdate for the output to stabilize.
-    if (output_file_number_ != file_number_) {
+  auto RemapLineNumbers(
+      const llvm::DenseMap<std::pair<int, int>, int>& output_line_remap,
+      const llvm::SmallVector<int>& new_last_line_numbers) -> void {
+    // Only need to do remappings when there's a line number replacement.
+    if (!replacement_) {
       return;
     }
 
     bool found_one = false;
+    // Use a cursor for the line so that we can't keep matching the same
+    // content, which may occur when we keep a literal line number.
+    int line_offset = 0;
     while (true) {
+      // Rebuild the cursor each time because we're editing the line, which
+      // could cause a reallocation.
+      absl::string_view line_cursor = line_;
+      line_cursor.remove_prefix(line_offset);
       // Look for a line number to replace. There may be multiple, so we
       // repeatedly check.
       absl::string_view matched_line_number;
-      if (line_number_re_has_file_) {
-        RE2::PartialMatch(line_, *line_number_re_, nullptr,
+      if (replacement_->has_file) {
+        RE2::PartialMatch(line_cursor, *replacement_->re, nullptr,
                           &matched_line_number);
       } else {
-        RE2::PartialMatch(line_, *line_number_re_, &matched_line_number);
+        RE2::PartialMatch(line_cursor, *replacement_->re, &matched_line_number);
       }
       if (matched_line_number.empty()) {
         CARBON_CHECK(found_one) << line_;
@@ -90,29 +102,50 @@ class CheckLine : public FileTestLineBase {
       }
       found_one = true;
 
-      // Calculate the offset from the CHECK line to the new line number
-      // (possibly with new CHECK lines added, or some removed).
-      int new_line_number = line_remap(ParseLineNumber(matched_line_number));
-      int offset = new_line_number - output_line_number_;
+      // Update the cursor offset from the match.
+      line_offset = matched_line_number.begin() - line_.c_str();
+
+      // Calculate the new line number (possibly with new CHECK lines added, or
+      // some removed).
+      int old_line_number = ParseLineNumber(matched_line_number);
+      int new_line_number = -1;
+      if (auto remapped =
+              output_line_remap.find({file_number_, old_line_number});
+          remapped != output_line_remap.end()) {
+        // Map old non-check lines to their new line numbers.
+        new_line_number = remapped->second;
+      } else {
+        // We assume unmapped references point to the end-of-file.
+        new_line_number = new_last_line_numbers[file_number_];
+      }
 
-      // Update the line offset in the CHECK line.
-      const char* offset_prefix = offset < 0 ? "" : "+";
-      std::string replacement = llvm::formatv(
-          line_formatv.c_str(),
-          llvm::formatv("[[@LINE{0}{1}]]", offset_prefix, offset));
+      std::string replacement;
+      if (output_file_number_ == file_number_) {
+        int offset = new_line_number - output_line_number_;
+        // Update the line offset in the CHECK line.
+        const char* offset_prefix = offset < 0 ? "" : "+";
+        replacement = llvm::formatv(
+            replacement_->line_formatv.c_str(),
+            llvm::formatv("[[@LINE{0}{1}]]", offset_prefix, offset));
+      } else {
+        // If the CHECK was written to a different file from the file that it
+        // refers to, leave behind an absolute line reference rather than a
+        // cross-file offset.
+        replacement =
+            llvm::formatv(replacement_->line_formatv.c_str(), new_line_number);
+      }
       line_.replace(matched_line_number.data() - line_.data(),
                     matched_line_number.size(), replacement);
     }
   }
 
-  int file_number() const { return file_number_; }
+  auto file_number() const -> int { return file_number_; }
 
   auto is_blank() const -> bool override { return false; }
 
  private:
   int file_number_;
-  bool line_number_re_has_file_;
-  const RE2* line_number_re_;
+  const FileTestLineNumberReplacement* replacement_;
   std::string line_;
   llvm::StringRef indent_;
   int output_file_number_ = -1;
@@ -121,11 +154,45 @@ class CheckLine : public FileTestLineBase {
 
 }  // namespace
 
+// Looks for the patterns in the line. Returns the first match, or defaulted
+// information if not found.
+static auto GetFileAndLineNumber(
+    const llvm::SmallVector<FileTestLineNumberReplacement>& replacements,
+    llvm::DenseMap<llvm::StringRef, int> file_to_number_map,
+    int default_file_number, const std::string& check_line)
+    -> FileAndLineNumber {
+  for (const auto& replacement : replacements) {
+    if (replacement.has_file) {
+      absl::string_view filename;
+      absl::string_view line_number;
+      if (RE2::PartialMatch(check_line, *replacement.re, &filename,
+                            &line_number)) {
+        if (auto it = file_to_number_map.find(filename);
+            it != file_to_number_map.end()) {
+          return FileAndLineNumber(&replacement, it->second, line_number);
+        } else {
+          return FileAndLineNumber(default_file_number);
+        }
+      }
+    } else {
+      // There's no file association, so we only look at the line, and assume
+      // it refers to the default file.
+      absl::string_view line_number;
+      if (RE2::PartialMatch(check_line, *replacement.re, &line_number)) {
+        return FileAndLineNumber(&replacement, default_file_number,
+                                 line_number);
+      }
+    }
+  }
+  return FileAndLineNumber(default_file_number);
+}
+
 // Builds CheckLine lists for autoupdate.
 static auto BuildCheckLines(
     llvm::StringRef output, const char* label,
     const llvm::SmallVector<llvm::StringRef>& filenames,
-    bool line_number_re_has_file, const RE2& line_number_re,
+    const std::optional<RE2>& default_file_re,
+    const llvm::SmallVector<FileTestLineNumberReplacement>& replacements,
     std::function<void(std::string&)> do_extra_check_replacements)
     -> llvm::SmallVector<CheckLine> {
   llvm::SmallVector<CheckLine> check_lines;
@@ -157,6 +224,9 @@ static auto BuildCheckLines(
   // End-of-line whitespace is replaced with a regex matcher to make it visible.
   RE2 end_of_line_whitespace_re(R"((\s+)$)");
 
+  // The default file number for when no specific file is found.
+  int default_file_number = 0;
+
   for (const auto& line : lines) {
     std::string check_line = llvm::formatv("// CHECK:{0}:{1}{2}", label,
                                            line.empty() ? "" : " ", line);
@@ -171,32 +241,19 @@ static auto BuildCheckLines(
 
     do_extra_check_replacements(check_line);
 
-    // Look for line information in the output. use_line_number is only set if
-    // the match is correct.
-    std::optional<llvm::StringRef> use_line_number;
-    absl::string_view match_line_number;
-    int file_number = 0;
-    if (line_number_re_has_file) {
-      absl::string_view match_filename;
-      if (RE2::PartialMatch(check_line, line_number_re, &match_filename,
-                            &match_line_number)) {
-        if (auto it = file_to_number_map.find(match_filename);
-            it != file_to_number_map.end()) {
-          file_number = it->second;
-          use_line_number = match_line_number;
-        }
-      }
-    } else {
-      // There's no file association, so we only look at the line, and assume it
-      // refers to the main file.
-      if (RE2::PartialMatch(check_line, line_number_re, &match_line_number)) {
-        use_line_number = match_line_number;
+    if (default_file_re) {
+      absl::string_view filename;
+      if (RE2::PartialMatch(line, *default_file_re, &filename)) {
+        auto it = file_to_number_map.find(filename);
+        CARBON_CHECK(it != file_to_number_map.end())
+            << "default_file_re had unexpected match in '" << line << "' (`"
+            << default_file_re->pattern() << "`)";
+        default_file_number = it->second;
       }
     }
-    int line_number = use_line_number ? ParseLineNumber(*use_line_number) : -1;
-    check_lines.push_back(
-        CheckLine(file_number, line_number, line_number_re_has_file,
-                  use_line_number ? &line_number_re : nullptr, check_line));
+    auto file_and_line = GetFileAndLineNumber(replacements, file_to_number_map,
+                                              default_file_number, check_line);
+    check_lines.push_back(CheckLine(file_and_line, check_line));
   }
 
   return check_lines;
@@ -206,21 +263,27 @@ auto AutoupdateFileTest(
     const std::filesystem::path& file_test_path, llvm::StringRef input_content,
     const llvm::SmallVector<llvm::StringRef>& filenames,
     int autoupdate_line_number,
-    llvm::SmallVector<llvm::SmallVector<FileTestLine>>& non_check_lines,
+    const llvm::SmallVector<llvm::SmallVector<FileTestLine>>& non_check_lines,
     llvm::StringRef stdout, llvm::StringRef stderr,
-    FileTestLineNumberReplacement line_number_replacement,
+    const std::optional<RE2>& default_file_re,
+    const llvm::SmallVector<FileTestLineNumberReplacement>&
+        line_number_replacements,
     std::function<void(std::string&)> do_extra_check_replacements) -> bool {
-  RE2 line_number_re(line_number_replacement.pattern);
-  CARBON_CHECK(line_number_re.ok()) << "Invalid line replacement RE2: `"
-                                    << line_number_replacement.pattern << "`";
+  for (const auto& replacement : line_number_replacements) {
+    CARBON_CHECK(replacement.has_file || default_file_re)
+        << "For replacement with pattern `" << replacement.re->pattern()
+        << "` to have has_file=false, override GetDefaultFileRE.";
+    CARBON_CHECK(replacement.re->ok())
+        << "Invalid line replacement RE2: " << replacement.re->error();
+  }
 
   // Prepare CHECK lines.
-  llvm::SmallVector<CheckLine> stdout_check_lines = BuildCheckLines(
-      stdout, "STDOUT", filenames, line_number_replacement.has_file,
-      line_number_re, do_extra_check_replacements);
-  llvm::SmallVector<CheckLine> stderr_check_lines = BuildCheckLines(
-      stderr, "STDERR", filenames, line_number_replacement.has_file,
-      line_number_re, do_extra_check_replacements);
+  llvm::SmallVector<CheckLine> stdout_check_lines =
+      BuildCheckLines(stdout, "STDOUT", filenames, default_file_re,
+                      line_number_replacements, do_extra_check_replacements);
+  llvm::SmallVector<CheckLine> stderr_check_lines =
+      BuildCheckLines(stderr, "STDERR", filenames, default_file_re,
+                      line_number_replacements, do_extra_check_replacements);
   auto* stdout_check_line = stdout_check_lines.begin();
   auto* stderr_check_line = stderr_check_lines.begin();
 
@@ -233,15 +296,23 @@ auto AutoupdateFileTest(
 
   const FileTestLine blank_line(-1, "");
 
+  // Maps {file_number, original line number} to a new line number.
+  llvm::DenseMap<std::pair<int, int>, int> output_line_remap;
+
+  // Tracks the new last line numbers for each file.
+  llvm::SmallVector<int> new_last_line_numbers;
+  new_last_line_numbers.reserve(filenames.size());
+
   // Stitch together content.
   llvm::SmallVector<const FileTestLineBase*> new_lines;
   for (auto [file_number_as_size_t, filename, non_check_file] :
        llvm::enumerate(filenames, non_check_lines)) {
     auto file_number = static_cast<int>(file_number_as_size_t);
-    llvm::DenseMap<int, int> output_line_remap;
-    llvm::SmallVector<CheckLine*> check_lines_this_file;
     int output_line_number = 0;
 
+    // Track the offset in new_lines to later determine the line count.
+    int file_offset_in_new_lines = new_lines.size();
+
     // Add all check lines from the given vector until we reach a check line
     // attached to a line later than `to_line_number`.
     auto add_check_lines = [&](const llvm::SmallVector<CheckLine>& lines,
@@ -253,7 +324,6 @@ auto AutoupdateFileTest(
            ++line) {
         new_lines.push_back(line);
         line->SetOutputLine(indent, file_number, ++output_line_number);
-        check_lines_this_file.push_back(line);
       }
     };
 
@@ -264,6 +334,10 @@ auto AutoupdateFileTest(
       // something like a split directive which shouldn't increment
       // output_line_number.
       if (non_check_line.line_number() < 1) {
+        // These are ignored when calculating line counts.
+        CARBON_CHECK(file_offset_in_new_lines ==
+                     static_cast<int>(new_lines.size()));
+        ++file_offset_in_new_lines;
         new_lines.push_back(&non_check_line);
         continue;
       }
@@ -281,10 +355,10 @@ auto AutoupdateFileTest(
       }
 
       new_lines.push_back(&non_check_line);
-      CARBON_CHECK(
-          output_line_remap
-              .insert({non_check_line.line_number(), ++output_line_number})
-              .second);
+      CARBON_CHECK(output_line_remap
+                       .insert({{file_number, non_check_line.line_number()},
+                                ++output_line_number})
+                       .second);
 
       // If we just added the AUTOUPDATE line, include any early STDERR lines
       // now, so that the initial batch of CHECK lines have STDERR before
@@ -306,11 +380,12 @@ auto AutoupdateFileTest(
     // This should always be true after the first file is processed.
     CARBON_CHECK(reached_autoupdate);
 
-    // At the end of the last file, print remaining check lines which -- for
-    // whatever reason -- come after all original lines.
-    if (file_number == static_cast<int>(filenames.size()) - 1 &&
-        (stderr_check_line != stderr_check_lines.end() ||
-         stdout_check_line != stdout_check_lines.end())) {
+    // At the end of each file, print any remaining lines which are associated
+    // with the file.
+    if ((stderr_check_line != stderr_check_lines.end() &&
+         stderr_check_line->file_number() == file_number) ||
+        (stdout_check_line != stdout_check_lines.end() &&
+         stdout_check_line->file_number() == file_number)) {
       // Ensure there's a blank line before any trailing CHECKs.
       if (!new_lines.empty() && !new_lines.back()->is_blank()) {
         new_lines.push_back(&blank_line);
@@ -321,30 +396,15 @@ auto AutoupdateFileTest(
       add_check_lines(stdout_check_lines, stdout_check_line, INT_MAX, "");
     }
 
-    // Update all remapped lines in CHECK output.
-    for (auto* offset_check_line : check_lines_this_file) {
-      int last_non_check_line = non_check_file.back().line_number();
-      offset_check_line->RemapLineNumbers(
-          line_number_replacement.line_formatv, [&](int old_line_number) {
-            // Map old non-check lines to their new line numbers.
-            auto remapped = output_line_remap.find(old_line_number);
-            if (remapped != output_line_remap.end()) {
-              return remapped->second;
-            }
-
-            // Map any reference to a line past the final non-check line to
-            // the new end-of-file. We assume that any such reference is
-            // referring to the end of file, not to some specific CHECK
-            // comment.
-            if (old_line_number > last_non_check_line) {
-              return output_line_number;
-            }
-
-            // Line didn't get remapped; maybe it refers to a CHECK line.
-            // We can't express that as an offset, just leave it as-is.
-            return old_line_number;
-          });
-    }
+    new_last_line_numbers.push_back(new_lines.size() -
+                                    file_offset_in_new_lines);
+  }
+
+  for (auto& check_line : stdout_check_lines) {
+    check_line.RemapLineNumbers(output_line_remap, new_last_line_numbers);
+  }
+  for (auto& check_line : stderr_check_lines) {
+    check_line.RemapLineNumbers(output_line_remap, new_last_line_numbers);
   }
 
   // Generate the autoupdated file.

+ 10 - 4
testing/file_test/autoupdate.h

@@ -9,6 +9,7 @@
 
 #include "llvm/ADT/SmallVector.h"
 #include "llvm/ADT/StringRef.h"
+#include "re2/re2.h"
 #include "testing/file_test/line.h"
 
 namespace Carbon::Testing {
@@ -17,8 +18,11 @@ struct FileTestLineNumberReplacement {
   bool has_file;
 
   // The line replacement. The pattern should match lines. If has_file, pattern
-  // should have a file and line group; otherwise, only a line group.
-  std::string pattern;
+  // should have a file and line group; otherwise, only a line group, but
+  // default_file_re should be provided.
+  //
+  // Uses shared_ptr for storage in SmallVector.
+  std::shared_ptr<RE2> re;
 
   // line_formatv should provide {0} to substitute with [[@LINE...]] deltas.
   std::string line_formatv;
@@ -29,9 +33,11 @@ auto AutoupdateFileTest(
     const std::filesystem::path& file_test_path, llvm::StringRef input_content,
     const llvm::SmallVector<llvm::StringRef>& filenames,
     int autoupdate_line_number,
-    llvm::SmallVector<llvm::SmallVector<FileTestLine>>& non_check_lines,
+    const llvm::SmallVector<llvm::SmallVector<FileTestLine>>& non_check_lines,
     llvm::StringRef stdout, llvm::StringRef stderr,
-    FileTestLineNumberReplacement line_number_replacement,
+    const std::optional<RE2>& default_file_re,
+    const llvm::SmallVector<FileTestLineNumberReplacement>&
+        line_number_replacements,
     std::function<void(std::string&)> do_extra_check_replacements) -> bool;
 
 }  // namespace Carbon::Testing

+ 7 - 0
testing/file_test/autoupdate_testdata.sh

@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+# Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+# Exceptions. See /LICENSE for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+bazel run -c opt //testing/file_test:file_test_base_test -- --autoupdate "$@"

+ 15 - 11
testing/file_test/file_test_base.cpp

@@ -6,6 +6,7 @@
 
 #include <filesystem>
 #include <fstream>
+#include <optional>
 #include <string>
 #include <utility>
 
@@ -109,25 +110,26 @@ auto FileTestBase::Autoupdate() -> ErrorOr<bool> {
     filenames.push_back(file.filename);
   }
 
-  llvm::ArrayRef filenames_for_line_number = filenames;
+  llvm::ArrayRef expected_filenames = filenames;
   if (filenames.size() > 1) {
-    filenames_for_line_number = filenames_for_line_number.drop_front();
+    expected_filenames = expected_filenames.drop_front();
   }
 
   return AutoupdateFileTest(
       std::filesystem::absolute(test_name_.str()), context.input_content,
       filenames, *context.autoupdate_line_number, context.non_check_lines,
-      context.stdout, context.stderr,
-      GetLineNumberReplacement(filenames_for_line_number),
+      context.stdout, context.stderr, GetDefaultFileRE(expected_filenames),
+      GetLineNumberReplacements(expected_filenames),
       [&](std::string& line) { DoExtraCheckReplacements(line); });
 }
 
-auto FileTestBase::GetLineNumberReplacement(
-    llvm::ArrayRef<llvm::StringRef> filenames) -> LineNumberReplacement {
-  return {
-      .has_file = true,
-      .pattern = llvm::formatv(R"(({0}):(\d+))", llvm::join(filenames, "|")),
-      .line_formatv = R"({0})"};
+auto FileTestBase::GetLineNumberReplacements(
+    llvm::ArrayRef<llvm::StringRef> filenames)
+    -> llvm::SmallVector<LineNumberReplacement> {
+  return {{.has_file = true,
+           .re = std::make_shared<RE2>(
+               llvm::formatv(R"(({0}):(\d+))", llvm::join(filenames, "|"))),
+           .line_formatv = R"({0})"}};
 }
 
 auto FileTestBase::ProcessTestFileAndRun(TestContext& context)
@@ -501,4 +503,6 @@ static auto Main(int argc, char** argv) -> int {
 
 }  // namespace Carbon::Testing
 
-auto main(int argc, char** argv) -> int { Carbon::Testing::Main(argc, argv); }
+auto main(int argc, char** argv) -> int {
+  return Carbon::Testing::Main(argc, argv);
+}

+ 11 - 2
testing/file_test/file_test_base.h

@@ -66,10 +66,19 @@ class FileTestBase : public testing::Test {
   // Returns default arguments. Only called when a file doesn't set ARGS.
   virtual auto GetDefaultArgs() -> llvm::SmallVector<std::string> = 0;
 
+  // Returns a regex to match the default file when a line may not be present.
+  // May return nullptr if unused. If GetLineNumberReplacements returns an entry
+  // with has_file=false, this is required.
+  virtual auto GetDefaultFileRE(llvm::ArrayRef<llvm::StringRef> /*filenames*/)
+      -> std::optional<RE2> {
+    return std::nullopt;
+  }
+
   // Returns replacement information for line numbers. See LineReplacement for
   // construction.
-  virtual auto GetLineNumberReplacement(
-      llvm::ArrayRef<llvm::StringRef> filenames) -> LineNumberReplacement;
+  virtual auto GetLineNumberReplacements(
+      llvm::ArrayRef<llvm::StringRef> filenames)
+      -> llvm::SmallVector<LineNumberReplacement>;
 
   // Optionally allows children to provide extra replacements for autoupdate.
   virtual auto DoExtraCheckReplacements(std::string& /*check_line*/) -> void {}

+ 40 - 2
testing/file_test/file_test_base_test.cpp

@@ -9,12 +9,11 @@
 
 #include "common/ostream.h"
 #include "llvm/ADT/StringExtras.h"
+#include "llvm/Support/FormatVariadic.h"
 
 namespace Carbon::Testing {
 namespace {
 
-using ::testing::Eq;
-
 class FileTestBaseTest : public FileTestBase {
  public:
   using FileTestBase::FileTestBase;
@@ -96,6 +95,27 @@ class FileTestBaseTest : public FileTestBase {
       stderr << "unattached message 3\n"
              << "unattached message 4\n";
       return true;
+    } else if (filename == "file_only_re_one_file.carbon") {
+      stdout << "unattached message 1\n"
+             << "file: file_only_re_one_file.carbon\n"
+             << "line: 1\n"
+             << "unattached message 2\n";
+      return true;
+    } else if (filename == "file_only_re_multi_file.carbon") {
+      int msg_count = 0;
+      stdout << "unattached message " << ++msg_count << "\n"
+             << "file: a.carbon\n"
+             << "unattached message " << ++msg_count << "\n"
+             << "line: 1: attached message " << ++msg_count << "\n"
+             << "unattached message " << ++msg_count << "\n"
+             << "line: 8: late message " << ++msg_count << "\n"
+             << "unattached message " << ++msg_count << "\n"
+             << "file: b.carbon\n"
+             << "line: 1: attached message " << ++msg_count << "\n"
+             << "unattached message " << ++msg_count << "\n"
+             << "line: 7: late message " << ++msg_count << "\n"
+             << "unattached message " << ++msg_count << "\n";
+      return true;
     } else {
       return ErrorBuilder() << "Unexpected file: " << filename;
     }
@@ -104,6 +124,24 @@ class FileTestBaseTest : public FileTestBase {
   auto GetDefaultArgs() -> llvm::SmallVector<std::string> override {
     return {"default_args", "%s"};
   }
+
+  auto GetDefaultFileRE(llvm::ArrayRef<llvm::StringRef> filenames)
+      -> std::optional<RE2> override {
+    return std::make_optional<RE2>(
+        llvm::formatv(R"(file: ({0}))", llvm::join(filenames, "|")));
+  }
+
+  auto GetLineNumberReplacements(llvm::ArrayRef<llvm::StringRef> filenames)
+      -> llvm::SmallVector<LineNumberReplacement> override {
+    auto replacements = FileTestBase::GetLineNumberReplacements(filenames);
+    auto filename = std::filesystem::path(test_name().str()).filename();
+    if (llvm::StringRef(filename).startswith("file_only_re_")) {
+      replacements.push_back({.has_file = false,
+                              .re = std::make_shared<RE2>(R"(line: (\d+))"),
+                              .line_formatv = "{0}"});
+    }
+    return replacements;
+  }
 };
 
 }  // namespace

+ 25 - 0
testing/file_test/testdata/file_only_re_multi_file.carbon

@@ -0,0 +1,25 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+// AUTOUPDATE
+// CHECK:STDOUT: 3 args: `default_args`, `a.carbon`, `b.carbon`
+// CHECK:STDOUT: unattached message 1
+
+// --- a.carbon
+aaa
+// CHECK:STDOUT: file: a.carbon
+// CHECK:STDOUT: unattached message 2
+// CHECK:STDOUT: line: [[@LINE-3]]: attached message 3
+// CHECK:STDOUT: unattached message 4
+
+// CHECK:STDOUT: line: [[@LINE+1]]: late message 5
+// CHECK:STDOUT: unattached message 6
+// --- b.carbon
+bbb
+// CHECK:STDOUT: file: b.carbon
+// CHECK:STDOUT: line: [[@LINE-2]]: attached message 7
+// CHECK:STDOUT: unattached message 8
+
+// CHECK:STDOUT: line: [[@LINE+1]]: late message 9
+// CHECK:STDOUT: unattached message 10

+ 10 - 0
testing/file_test/testdata/file_only_re_one_file.carbon

@@ -0,0 +1,10 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+// AUTOUPDATE
+// CHECK:STDOUT: 2 args: `default_args`, `file_only_re_one_file.carbon`
+// CHECK:STDOUT: unattached message 1
+// CHECK:STDOUT: file: file_only_re_one_file.carbon
+// CHECK:STDOUT: line: [[@LINE-8]]
+// CHECK:STDOUT: unattached message 2

+ 3 - 4
testing/file_test/testdata/unattached_multi_file.carbon

@@ -6,12 +6,11 @@
 // CHECK:STDERR: unattached message 3
 // CHECK:STDERR: unattached message 4
 
+// CHECK:STDOUT: 3 args: `default_args`, `a.carbon`, `b.carbon`
+// CHECK:STDOUT: unattached message 1
+// CHECK:STDOUT: unattached message 2
 // --- a.carbon
 aaa
 
 // --- b.carbon
 bbb
-
-// CHECK:STDOUT: 3 args: `default_args`, `a.carbon`, `b.carbon`
-// CHECK:STDOUT: unattached message 1
-// CHECK:STDOUT: unattached message 2

+ 3 - 3
toolchain/autoupdate_testdata.py

@@ -26,13 +26,13 @@ def main() -> None:
     # Support specifying tests to update, such as:
     # ./autoupdate_testdata.py lex/**/*
     if len(sys.argv) > 1:
-        repo_root = Path(__file__).parent.parent
+        repo_root = Path(__file__).resolve().parent.parent
         file_tests = []
         # Filter down to just test files.
         for f in sys.argv[1:]:
             if f.endswith(".carbon"):
-                path = str(Path(f).absolute().relative_to(repo_root))
-                if path.find("/testdata/"):
+                path = str(Path(f).resolve().relative_to(repo_root))
+                if path.count("/testdata/"):
                     file_tests.append(path)
         if not file_tests:
             sys.exit(

+ 2 - 28
toolchain/driver/driver_test.cpp

@@ -15,13 +15,11 @@
 #include "llvm/Object/Binary.h"
 #include "llvm/Support/FormatVariadic.h"
 #include "testing/base/test_raw_ostream.h"
-#include "toolchain/base/yaml_test_helpers.h"
 
 namespace Carbon::Testing {
 namespace {
 
 using ::testing::ContainsRegex;
-using ::testing::ElementsAre;
 using ::testing::HasSubstr;
 using ::testing::StrEq;
 
@@ -126,32 +124,8 @@ TEST_F(DriverTest, DumpTokens) {
   EXPECT_TRUE(
       driver_.RunCommand({"compile", "--phase=lex", "--dump-tokens", file}));
   EXPECT_THAT(test_error_stream_.TakeStr(), StrEq(""));
-  auto tokenized_text = test_output_stream_.TakeStr();
-
-  EXPECT_THAT(Yaml::Value::FromText(tokenized_text),
-              ElementsAre(Yaml::SequenceValue{
-                  Yaml::MappingValue{{"index", "0"},
-                                     {"kind", "Identifier"},
-                                     {"line", "1"},
-                                     {"column", "1"},
-                                     {"indent", "1"},
-                                     {"spelling", "Hello"},
-                                     {"identifier", "0"},
-                                     {"has_trailing_space", "true"}},
-                  Yaml::MappingValue{{"index", "1"},
-                                     {"kind", "Identifier"},
-                                     {"line", "1"},
-                                     {"column", "7"},
-                                     {"indent", "1"},
-                                     {"spelling", "World"},
-                                     {"identifier", "1"},
-                                     {"has_trailing_space", "true"}},
-                  Yaml::MappingValue{{"index", "2"},
-                                     {"kind", "EndOfFile"},
-                                     {"line", "1"},
-                                     {"column", "12"},
-                                     {"indent", "1"},
-                                     {"spelling", ""}}}));
+  // Verify there is output without examining it.
+  EXPECT_FALSE(test_output_stream_.TakeStr().empty());
 }
 
 TEST_F(DriverTest, DumpParseTree) {

+ 15 - 14
toolchain/lex/testdata/basic_syntax.carbon

@@ -3,22 +3,23 @@
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
 // AUTOUPDATE
-// CHECK:STDOUT: [
+// CHECK:STDOUT: - filename: basic_syntax.carbon
+// CHECK:STDOUT:   tokens: [
 
 fn run(String program) {
-// CHECK:STDOUT: { index:  0, kind:                'Fn', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'fn', has_trailing_space: true },
-// CHECK:STDOUT: { index:  1, kind:        'Identifier', line: {{ *}}[[@LINE-2]], column:  4, indent: 1, spelling: 'run', identifier: 0 },
-// CHECK:STDOUT: { index:  2, kind:         'OpenParen', line: {{ *}}[[@LINE-3]], column:  7, indent: 1, spelling: '(', closing_token: 5 },
-// CHECK:STDOUT: { index:  3, kind: 'StringTypeLiteral', line: {{ *}}[[@LINE-4]], column:  8, indent: 1, spelling: 'String', has_trailing_space: true },
-// CHECK:STDOUT: { index:  4, kind:        'Identifier', line: {{ *}}[[@LINE-5]], column: 15, indent: 1, spelling: 'program', identifier: 1 },
-// CHECK:STDOUT: { index:  5, kind:        'CloseParen', line: {{ *}}[[@LINE-6]], column: 22, indent: 1, spelling: ')', opening_token: 2, has_trailing_space: true },
-// CHECK:STDOUT: { index:  6, kind:    'OpenCurlyBrace', line: {{ *}}[[@LINE-7]], column: 24, indent: 1, spelling: '{', closing_token: 10, has_trailing_space: true },
+// CHECK:STDOUT:     { index:  0, kind:                'Fn', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'fn', has_trailing_space: true },
+// CHECK:STDOUT:     { index:  1, kind:        'Identifier', line: {{ *}}[[@LINE-2]], column:  4, indent: 1, spelling: 'run', identifier: 0 },
+// CHECK:STDOUT:     { index:  2, kind:         'OpenParen', line: {{ *}}[[@LINE-3]], column:  7, indent: 1, spelling: '(', closing_token: 5 },
+// CHECK:STDOUT:     { index:  3, kind: 'StringTypeLiteral', line: {{ *}}[[@LINE-4]], column:  8, indent: 1, spelling: 'String', has_trailing_space: true },
+// CHECK:STDOUT:     { index:  4, kind:        'Identifier', line: {{ *}}[[@LINE-5]], column: 15, indent: 1, spelling: 'program', identifier: 1 },
+// CHECK:STDOUT:     { index:  5, kind:        'CloseParen', line: {{ *}}[[@LINE-6]], column: 22, indent: 1, spelling: ')', opening_token: 2, has_trailing_space: true },
+// CHECK:STDOUT:     { index:  6, kind:    'OpenCurlyBrace', line: {{ *}}[[@LINE-7]], column: 24, indent: 1, spelling: '{', closing_token: 10, has_trailing_space: true },
   return True;
-  // CHECK:STDOUT: { index:  7, kind:            'Return', line: {{ *}}[[@LINE-1]], column:  3, indent: 3, spelling: 'return', has_trailing_space: true },
-  // CHECK:STDOUT: { index:  8, kind:        'Identifier', line: {{ *}}[[@LINE-2]], column: 10, indent: 3, spelling: 'True', identifier: 2 },
-  // CHECK:STDOUT: { index:  9, kind:              'Semi', line: {{ *}}[[@LINE-3]], column: 14, indent: 3, spelling: ';', has_trailing_space: true },
+  // CHECK:STDOUT:     { index:  7, kind:            'Return', line: {{ *}}[[@LINE-1]], column:  3, indent: 3, spelling: 'return', has_trailing_space: true },
+  // CHECK:STDOUT:     { index:  8, kind:        'Identifier', line: {{ *}}[[@LINE-2]], column: 10, indent: 3, spelling: 'True', identifier: 2 },
+  // CHECK:STDOUT:     { index:  9, kind:              'Semi', line: {{ *}}[[@LINE-3]], column: 14, indent: 3, spelling: ';', has_trailing_space: true },
 }
-// CHECK:STDOUT: { index: 10, kind:   'CloseCurlyBrace', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: '}', opening_token: 6, has_trailing_space: true },
+// CHECK:STDOUT:     { index: 10, kind:   'CloseCurlyBrace', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: '}', opening_token: 6, has_trailing_space: true },
 
-// CHECK:STDOUT: { index: 11, kind:         'EndOfFile', line: {{ *}}[[@LINE+1]], column: {{ *\d+}}, indent: 1, spelling: '' },
-// CHECK:STDOUT: ]
+// CHECK:STDOUT:     { index: 11, kind:         'EndOfFile', line: {{ *}}[[@LINE+1]], column: {{ *\d+}}, indent: 1, spelling: '' },
+// CHECK:STDOUT:   ]

+ 10 - 9
toolchain/lex/testdata/fail_block_string_second_line.carbon

@@ -4,20 +4,21 @@
 //
 // An autoupdate wants to put the CHECK in the middle of the block string.
 // NOAUTOUPDATE
-// CHECK:STDOUT: [
+// CHECK:STDOUT: - filename: fail_block_string_second_line.carbon
+// CHECK:STDOUT:   tokens: [
 
 // CHECK:STDERR: fail_block_string_second_line.carbon:[[@LINE+4]]:3: Only whitespace is permitted before the closing `'''` of a multi-line string.
 // CHECK:STDERR:   error here: '''
 // CHECK:STDERR:   ^
 var s: String = '''
   error here: '''
-// CHECK:STDOUT: { index: 0, kind:               'Var', line: {{ *}}[[@LINE-2]], column:  1, indent: 1, spelling: 'var', has_trailing_space: true },
-// CHECK:STDOUT: { index: 1, kind:        'Identifier', line: {{ *}}[[@LINE-3]], column:  5, indent: 1, spelling: 's', identifier: 0 },
-// CHECK:STDOUT: { index: 2, kind:             'Colon', line: {{ *}}[[@LINE-4]], column:  6, indent: 1, spelling: ':', has_trailing_space: true },
-// CHECK:STDOUT: { index: 3, kind: 'StringTypeLiteral', line: {{ *}}[[@LINE-5]], column:  8, indent: 1, spelling: 'String', has_trailing_space: true },
-// CHECK:STDOUT: { index: 4, kind:             'Equal', line: {{ *}}[[@LINE-6]], column: 15, indent: 1, spelling: '=', has_trailing_space: true },
-// CHECK:STDOUT: { index: 5, kind:     'StringLiteral', line: {{ *}}[[@LINE-7]], column: 17, indent: 1, spelling: ''''
+// CHECK:STDOUT:     { index: 0, kind:               'Var', line: {{ *}}[[@LINE-2]], column:  1, indent: 1, spelling: 'var', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 1, kind:        'Identifier', line: {{ *}}[[@LINE-3]], column:  5, indent: 1, spelling: 's', identifier: 0 },
+// CHECK:STDOUT:     { index: 2, kind:             'Colon', line: {{ *}}[[@LINE-4]], column:  6, indent: 1, spelling: ':', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 3, kind: 'StringTypeLiteral', line: {{ *}}[[@LINE-5]], column:  8, indent: 1, spelling: 'String', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 4, kind:             'Equal', line: {{ *}}[[@LINE-6]], column: 15, indent: 1, spelling: '=', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 5, kind:     'StringLiteral', line: {{ *}}[[@LINE-7]], column: 17, indent: 1, spelling: ''''
 // CHECK:STDOUT:   error here: '''', value: `error here: `, has_trailing_space: true },
 
-// CHECK:STDOUT: { index: 6, kind:         'EndOfFile', line: {{ *}}[[@LINE+1]], column: {{ *\d+}}, indent: 1, spelling: '' },
-// CHECK:STDOUT: ]
+// CHECK:STDOUT:     { index: 6, kind:         'EndOfFile', line: {{ *}}[[@LINE+1]], column: {{ *\d+}}, indent: 1, spelling: '' },
+// CHECK:STDOUT:   ]

+ 28 - 0
toolchain/lex/testdata/fail_multifile.carbon

@@ -0,0 +1,28 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+// AUTOUPDATE
+
+// --- a.carbon
+// CHECK:STDERR: a.carbon:[[@LINE+3]]:3: Empty digit sequence in numeric literal.
+// CHECK:STDERR: 1.a
+// CHECK:STDERR:   ^
+1.a
+// CHECK:STDOUT: - filename: a.carbon
+// CHECK:STDOUT:   tokens: [
+// CHECK:STDOUT:     { index: 0, kind:     'Error', line: {{ *}}[[@LINE-3]], column:  1, indent: 1, spelling: '1.a', has_trailing_space: true },
+
+// CHECK:STDOUT:     { index: 1, kind: 'EndOfFile', line: {{ *}}[[@LINE+1]], column: {{ *\d+}}, indent: 1, spelling: '' },
+// CHECK:STDOUT:   ]
+// --- b.carbon
+// CHECK:STDERR: b.carbon:[[@LINE+3]]:3: Empty digit sequence in numeric literal.
+// CHECK:STDERR: 2.b
+// CHECK:STDERR:   ^
+2.b
+// CHECK:STDOUT: - filename: b.carbon
+// CHECK:STDOUT:   tokens: [
+// CHECK:STDOUT:     { index: 0, kind:     'Error', line: {{ *}}[[@LINE-3]], column:  1, indent: 1, spelling: '2.b', has_trailing_space: true },
+
+// CHECK:STDOUT:     { index: 1, kind: 'EndOfFile', line: {{ *}}[[@LINE+1]], column: {{ *\d+}}, indent: 1, spelling: '' },
+// CHECK:STDOUT:   ]

+ 66 - 65
toolchain/lex/testdata/keywords.carbon

@@ -3,133 +3,134 @@
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
 // AUTOUPDATE
-// CHECK:STDOUT: [
+// CHECK:STDOUT: - filename: keywords.carbon
+// CHECK:STDOUT:   tokens: [
 
 abstract
-// CHECK:STDOUT: { index:  0, kind:            'Abstract', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'abstract', has_trailing_space: true },
+// CHECK:STDOUT:     { index:  0, kind:            'Abstract', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'abstract', has_trailing_space: true },
 adapt
-// CHECK:STDOUT: { index:  1, kind:               'Adapt', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'adapt', has_trailing_space: true },
+// CHECK:STDOUT:     { index:  1, kind:               'Adapt', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'adapt', has_trailing_space: true },
 addr
-// CHECK:STDOUT: { index:  2, kind:                'Addr', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'addr', has_trailing_space: true },
+// CHECK:STDOUT:     { index:  2, kind:                'Addr', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'addr', has_trailing_space: true },
 alias
-// CHECK:STDOUT: { index:  3, kind:               'Alias', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'alias', has_trailing_space: true },
+// CHECK:STDOUT:     { index:  3, kind:               'Alias', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'alias', has_trailing_space: true },
 and
-// CHECK:STDOUT: { index:  4, kind:                 'And', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'and', has_trailing_space: true },
+// CHECK:STDOUT:     { index:  4, kind:                 'And', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'and', has_trailing_space: true },
 api
-// CHECK:STDOUT: { index:  5, kind:                 'Api', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'api', has_trailing_space: true },
+// CHECK:STDOUT:     { index:  5, kind:                 'Api', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'api', has_trailing_space: true },
 as
-// CHECK:STDOUT: { index:  6, kind:                  'As', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'as', has_trailing_space: true },
+// CHECK:STDOUT:     { index:  6, kind:                  'As', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'as', has_trailing_space: true },
 auto
-// CHECK:STDOUT: { index:  7, kind:                'Auto', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'auto', has_trailing_space: true },
+// CHECK:STDOUT:     { index:  7, kind:                'Auto', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'auto', has_trailing_space: true },
 base
-// CHECK:STDOUT: { index:  8, kind:                'Base', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'base', has_trailing_space: true },
+// CHECK:STDOUT:     { index:  8, kind:                'Base', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'base', has_trailing_space: true },
 bool
-// CHECK:STDOUT: { index:  9, kind:                'Bool', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'bool', has_trailing_space: true },
+// CHECK:STDOUT:     { index:  9, kind:                'Bool', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'bool', has_trailing_space: true },
 break
-// CHECK:STDOUT: { index: 10, kind:               'Break', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'break', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 10, kind:               'Break', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'break', has_trailing_space: true },
 case
-// CHECK:STDOUT: { index: 11, kind:                'Case', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'case', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 11, kind:                'Case', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'case', has_trailing_space: true },
 choice
-// CHECK:STDOUT: { index: 12, kind:              'Choice', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'choice', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 12, kind:              'Choice', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'choice', has_trailing_space: true },
 class
-// CHECK:STDOUT: { index: 13, kind:               'Class', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'class', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 13, kind:               'Class', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'class', has_trailing_space: true },
 const
-// CHECK:STDOUT: { index: 14, kind:               'Const', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'const', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 14, kind:               'Const', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'const', has_trailing_space: true },
 constraint
-// CHECK:STDOUT: { index: 15, kind:          'Constraint', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'constraint', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 15, kind:          'Constraint', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'constraint', has_trailing_space: true },
 continue
-// CHECK:STDOUT: { index: 16, kind:            'Continue', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'continue', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 16, kind:            'Continue', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'continue', has_trailing_space: true },
 default
-// CHECK:STDOUT: { index: 17, kind:             'Default', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'default', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 17, kind:             'Default', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'default', has_trailing_space: true },
 destructor
-// CHECK:STDOUT: { index: 18, kind:          'Destructor', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'destructor', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 18, kind:          'Destructor', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'destructor', has_trailing_space: true },
 else
-// CHECK:STDOUT: { index: 19, kind:                'Else', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'else', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 19, kind:                'Else', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'else', has_trailing_space: true },
 extend
-// CHECK:STDOUT: { index: 20, kind:              'Extend', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'extend', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 20, kind:              'Extend', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'extend', has_trailing_space: true },
 false
-// CHECK:STDOUT: { index: 21, kind:               'False', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'false', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 21, kind:               'False', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'false', has_trailing_space: true },
 final
-// CHECK:STDOUT: { index: 22, kind:               'Final', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'final', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 22, kind:               'Final', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'final', has_trailing_space: true },
 fn
-// CHECK:STDOUT: { index: 23, kind:                  'Fn', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'fn', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 23, kind:                  'Fn', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'fn', has_trailing_space: true },
 for
-// CHECK:STDOUT: { index: 24, kind:                 'For', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'for', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 24, kind:                 'For', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'for', has_trailing_space: true },
 forall
-// CHECK:STDOUT: { index: 25, kind:              'Forall', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'forall', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 25, kind:              'Forall', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'forall', has_trailing_space: true },
 friend
-// CHECK:STDOUT: { index: 26, kind:              'Friend', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'friend', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 26, kind:              'Friend', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'friend', has_trailing_space: true },
 if
-// CHECK:STDOUT: { index: 27, kind:                  'If', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'if', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 27, kind:                  'If', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'if', has_trailing_space: true },
 impl
-// CHECK:STDOUT: { index: 28, kind:                'Impl', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'impl', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 28, kind:                'Impl', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'impl', has_trailing_space: true },
 impls
-// CHECK:STDOUT: { index: 29, kind:               'Impls', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'impls', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 29, kind:               'Impls', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'impls', has_trailing_space: true },
 import
-// CHECK:STDOUT: { index: 30, kind:              'Import', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'import', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 30, kind:              'Import', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'import', has_trailing_space: true },
 in
-// CHECK:STDOUT: { index: 31, kind:                  'In', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'in', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 31, kind:                  'In', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'in', has_trailing_space: true },
 interface
-// CHECK:STDOUT: { index: 32, kind:           'Interface', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'interface', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 32, kind:           'Interface', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'interface', has_trailing_space: true },
 let
-// CHECK:STDOUT: { index: 33, kind:                 'Let', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'let', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 33, kind:                 'Let', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'let', has_trailing_space: true },
 library
-// CHECK:STDOUT: { index: 34, kind:             'Library', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'library', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 34, kind:             'Library', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'library', has_trailing_space: true },
 like
-// CHECK:STDOUT: { index: 35, kind:                'Like', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'like', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 35, kind:                'Like', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'like', has_trailing_space: true },
 match
-// CHECK:STDOUT: { index: 36, kind:               'Match', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'match', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 36, kind:               'Match', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'match', has_trailing_space: true },
 namespace
-// CHECK:STDOUT: { index: 37, kind:           'Namespace', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'namespace', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 37, kind:           'Namespace', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'namespace', has_trailing_space: true },
 not
-// CHECK:STDOUT: { index: 38, kind:                 'Not', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'not', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 38, kind:                 'Not', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'not', has_trailing_space: true },
 observe
-// CHECK:STDOUT: { index: 39, kind:             'Observe', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'observe', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 39, kind:             'Observe', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'observe', has_trailing_space: true },
 or
-// CHECK:STDOUT: { index: 40, kind:                  'Or', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'or', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 40, kind:                  'Or', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'or', has_trailing_space: true },
 override
-// CHECK:STDOUT: { index: 41, kind:            'Override', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'override', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 41, kind:            'Override', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'override', has_trailing_space: true },
 package
-// CHECK:STDOUT: { index: 42, kind:             'Package', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'package', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 42, kind:             'Package', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'package', has_trailing_space: true },
 partial
-// CHECK:STDOUT: { index: 43, kind:             'Partial', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'partial', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 43, kind:             'Partial', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'partial', has_trailing_space: true },
 private
-// CHECK:STDOUT: { index: 44, kind:             'Private', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'private', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 44, kind:             'Private', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'private', has_trailing_space: true },
 protected
-// CHECK:STDOUT: { index: 45, kind:           'Protected', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'protected', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 45, kind:           'Protected', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'protected', has_trailing_space: true },
 require
-// CHECK:STDOUT: { index: 46, kind:             'Require', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'require', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 46, kind:             'Require', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'require', has_trailing_space: true },
 return
-// CHECK:STDOUT: { index: 47, kind:              'Return', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'return', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 47, kind:              'Return', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'return', has_trailing_space: true },
 returned
-// CHECK:STDOUT: { index: 48, kind:            'Returned', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'returned', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 48, kind:            'Returned', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'returned', has_trailing_space: true },
 Self
-// CHECK:STDOUT: { index: 49, kind:  'SelfTypeIdentifier', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'Self', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 49, kind:  'SelfTypeIdentifier', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'Self', has_trailing_space: true },
 self
-// CHECK:STDOUT: { index: 50, kind: 'SelfValueIdentifier', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'self', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 50, kind: 'SelfValueIdentifier', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'self', has_trailing_space: true },
 String
-// CHECK:STDOUT: { index: 51, kind:   'StringTypeLiteral', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'String', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 51, kind:   'StringTypeLiteral', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'String', has_trailing_space: true },
 template
-// CHECK:STDOUT: { index: 52, kind:            'Template', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'template', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 52, kind:            'Template', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'template', has_trailing_space: true },
 then
-// CHECK:STDOUT: { index: 53, kind:                'Then', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'then', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 53, kind:                'Then', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'then', has_trailing_space: true },
 true
-// CHECK:STDOUT: { index: 54, kind:                'True', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'true', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 54, kind:                'True', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'true', has_trailing_space: true },
 type
-// CHECK:STDOUT: { index: 55, kind:                'Type', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'type', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 55, kind:                'Type', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'type', has_trailing_space: true },
 _
-// CHECK:STDOUT: { index: 56, kind:          'Underscore', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: '_', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 56, kind:          'Underscore', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: '_', has_trailing_space: true },
 var
-// CHECK:STDOUT: { index: 57, kind:                 'Var', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'var', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 57, kind:                 'Var', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'var', has_trailing_space: true },
 virtual
-// CHECK:STDOUT: { index: 58, kind:             'Virtual', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'virtual', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 58, kind:             'Virtual', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'virtual', has_trailing_space: true },
 where
-// CHECK:STDOUT: { index: 59, kind:               'Where', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'where', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 59, kind:               'Where', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'where', has_trailing_space: true },
 while
-// CHECK:STDOUT: { index: 60, kind:               'While', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'while', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 60, kind:               'While', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'while', has_trailing_space: true },
 
 notakeyword
-// CHECK:STDOUT: { index: 61, kind:          'Identifier', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'notakeyword', identifier: 0, has_trailing_space: true },
+// CHECK:STDOUT:     { index: 61, kind:          'Identifier', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'notakeyword', identifier: 0, has_trailing_space: true },
 
-// CHECK:STDOUT: { index: 62, kind:           'EndOfFile', line: {{ *}}[[@LINE+1]], column: {{ *\d+}}, indent: 1, spelling: '' },
-// CHECK:STDOUT: ]
+// CHECK:STDOUT:     { index: 62, kind:           'EndOfFile', line: {{ *}}[[@LINE+1]], column: {{ *\d+}}, indent: 1, spelling: '' },
+// CHECK:STDOUT:   ]

+ 26 - 0
toolchain/lex/testdata/multifile.carbon

@@ -0,0 +1,26 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+// AUTOUPDATE
+
+// --- a.carbon
+
+// CHECK:STDOUT: - filename: a.carbon
+// CHECK:STDOUT:   tokens: [
+a;
+// CHECK:STDOUT:     { index: 0, kind: 'Identifier', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'a', identifier: 0 },
+// CHECK:STDOUT:     { index: 1, kind:       'Semi', line: {{ *}}[[@LINE-2]], column:  2, indent: 1, spelling: ';', has_trailing_space: true },
+
+// CHECK:STDOUT:     { index: 2, kind:  'EndOfFile', line: {{ *}}[[@LINE+1]], column: {{ *\d+}}, indent: 1, spelling: '' },
+// CHECK:STDOUT:   ]
+// --- b.carbon
+
+// CHECK:STDOUT: - filename: b.carbon
+// CHECK:STDOUT:   tokens: [
+b;
+// CHECK:STDOUT:     { index: 0, kind: 'Identifier', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'b', identifier: 0 },
+// CHECK:STDOUT:     { index: 1, kind:       'Semi', line: {{ *}}[[@LINE-2]], column:  2, indent: 1, spelling: ';', has_trailing_space: true },
+
+// CHECK:STDOUT:     { index: 2, kind:  'EndOfFile', line: {{ *}}[[@LINE+1]], column: {{ *\d+}}, indent: 1, spelling: '' },
+// CHECK:STDOUT:   ]

+ 58 - 57
toolchain/lex/testdata/numeric_literals.carbon

@@ -3,82 +3,83 @@
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
 // AUTOUPDATE
-// CHECK:STDOUT: [
+// CHECK:STDOUT: - filename: numeric_literals.carbon
+// CHECK:STDOUT:   tokens: [
 
 fn F() {
-// CHECK:STDOUT: { index:  0, kind:                       'Fn', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'fn', has_trailing_space: true },
-// CHECK:STDOUT: { index:  1, kind:               'Identifier', line: {{ *}}[[@LINE-2]], column:  4, indent: 1, spelling: 'F', identifier: 0 },
-// CHECK:STDOUT: { index:  2, kind:                'OpenParen', line: {{ *}}[[@LINE-3]], column:  5, indent: 1, spelling: '(', closing_token: 3 },
-// CHECK:STDOUT: { index:  3, kind:               'CloseParen', line: {{ *}}[[@LINE-4]], column:  6, indent: 1, spelling: ')', opening_token: 2, has_trailing_space: true },
-// CHECK:STDOUT: { index:  4, kind:           'OpenCurlyBrace', line: {{ *}}[[@LINE-5]], column:  8, indent: 1, spelling: '{', closing_token: 53, has_trailing_space: true },
+// CHECK:STDOUT:     { index:  0, kind:                       'Fn', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: 'fn', has_trailing_space: true },
+// CHECK:STDOUT:     { index:  1, kind:               'Identifier', line: {{ *}}[[@LINE-2]], column:  4, indent: 1, spelling: 'F', identifier: 0 },
+// CHECK:STDOUT:     { index:  2, kind:                'OpenParen', line: {{ *}}[[@LINE-3]], column:  5, indent: 1, spelling: '(', closing_token: 3 },
+// CHECK:STDOUT:     { index:  3, kind:               'CloseParen', line: {{ *}}[[@LINE-4]], column:  6, indent: 1, spelling: ')', opening_token: 2, has_trailing_space: true },
+// CHECK:STDOUT:     { index:  4, kind:           'OpenCurlyBrace', line: {{ *}}[[@LINE-5]], column:  8, indent: 1, spelling: '{', closing_token: 53, has_trailing_space: true },
   // 8 and 9 trigger special behavior in APInt when mishandling signed versus
   // unsigned, so we pay extra attention to those.
   var ints: [i32; 5] = (
-  // CHECK:STDOUT: { index:  5, kind:                      'Var', line: {{ *}}[[@LINE-1]], column:  3, indent: 3, spelling: 'var', has_trailing_space: true },
-  // CHECK:STDOUT: { index:  6, kind:               'Identifier', line: {{ *}}[[@LINE-2]], column:  7, indent: 3, spelling: 'ints', identifier: 1 },
-  // CHECK:STDOUT: { index:  7, kind:                    'Colon', line: {{ *}}[[@LINE-3]], column: 11, indent: 3, spelling: ':', has_trailing_space: true },
-  // CHECK:STDOUT: { index:  8, kind:        'OpenSquareBracket', line: {{ *}}[[@LINE-4]], column: 13, indent: 3, spelling: '[', closing_token: 12 },
-  // CHECK:STDOUT: { index:  9, kind:       'IntegerTypeLiteral', line: {{ *}}[[@LINE-5]], column: 14, indent: 3, spelling: 'i32' },
-  // CHECK:STDOUT: { index: 10, kind:                     'Semi', line: {{ *}}[[@LINE-6]], column: 17, indent: 3, spelling: ';', has_trailing_space: true },
-  // CHECK:STDOUT: { index: 11, kind:           'IntegerLiteral', line: {{ *}}[[@LINE-7]], column: 19, indent: 3, spelling: '5', value: `5` },
-  // CHECK:STDOUT: { index: 12, kind:       'CloseSquareBracket', line: {{ *}}[[@LINE-8]], column: 20, indent: 3, spelling: ']', opening_token: 8, has_trailing_space: true },
-  // CHECK:STDOUT: { index: 13, kind:                    'Equal', line: {{ *}}[[@LINE-9]], column: 22, indent: 3, spelling: '=', has_trailing_space: true },
-  // CHECK:STDOUT: { index: 14, kind:                'OpenParen', line: {{ *}}[[@LINE-10]], column: 24, indent: 3, spelling: '(', closing_token: 25, has_trailing_space: true },
+  // CHECK:STDOUT:     { index:  5, kind:                      'Var', line: {{ *}}[[@LINE-1]], column:  3, indent: 3, spelling: 'var', has_trailing_space: true },
+  // CHECK:STDOUT:     { index:  6, kind:               'Identifier', line: {{ *}}[[@LINE-2]], column:  7, indent: 3, spelling: 'ints', identifier: 1 },
+  // CHECK:STDOUT:     { index:  7, kind:                    'Colon', line: {{ *}}[[@LINE-3]], column: 11, indent: 3, spelling: ':', has_trailing_space: true },
+  // CHECK:STDOUT:     { index:  8, kind:        'OpenSquareBracket', line: {{ *}}[[@LINE-4]], column: 13, indent: 3, spelling: '[', closing_token: 12 },
+  // CHECK:STDOUT:     { index:  9, kind:       'IntegerTypeLiteral', line: {{ *}}[[@LINE-5]], column: 14, indent: 3, spelling: 'i32' },
+  // CHECK:STDOUT:     { index: 10, kind:                     'Semi', line: {{ *}}[[@LINE-6]], column: 17, indent: 3, spelling: ';', has_trailing_space: true },
+  // CHECK:STDOUT:     { index: 11, kind:           'IntegerLiteral', line: {{ *}}[[@LINE-7]], column: 19, indent: 3, spelling: '5', value: `5` },
+  // CHECK:STDOUT:     { index: 12, kind:       'CloseSquareBracket', line: {{ *}}[[@LINE-8]], column: 20, indent: 3, spelling: ']', opening_token: 8, has_trailing_space: true },
+  // CHECK:STDOUT:     { index: 13, kind:                    'Equal', line: {{ *}}[[@LINE-9]], column: 22, indent: 3, spelling: '=', has_trailing_space: true },
+  // CHECK:STDOUT:     { index: 14, kind:                'OpenParen', line: {{ *}}[[@LINE-10]], column: 24, indent: 3, spelling: '(', closing_token: 25, has_trailing_space: true },
     8,
-    // CHECK:STDOUT: { index: 15, kind:           'IntegerLiteral', line: {{ *}}[[@LINE-1]], column:  5, indent: 5, spelling: '8', value: `8` },
-    // CHECK:STDOUT: { index: 16, kind:                    'Comma', line: {{ *}}[[@LINE-2]], column:  6, indent: 5, spelling: ',', has_trailing_space: true },
+    // CHECK:STDOUT:     { index: 15, kind:           'IntegerLiteral', line: {{ *}}[[@LINE-1]], column:  5, indent: 5, spelling: '8', value: `8` },
+    // CHECK:STDOUT:     { index: 16, kind:                    'Comma', line: {{ *}}[[@LINE-2]], column:  6, indent: 5, spelling: ',', has_trailing_space: true },
     9,
-    // CHECK:STDOUT: { index: 17, kind:           'IntegerLiteral', line: {{ *}}[[@LINE-1]], column:  5, indent: 5, spelling: '9', value: `9` },
-    // CHECK:STDOUT: { index: 18, kind:                    'Comma', line: {{ *}}[[@LINE-2]], column:  6, indent: 5, spelling: ',', has_trailing_space: true },
+    // CHECK:STDOUT:     { index: 17, kind:           'IntegerLiteral', line: {{ *}}[[@LINE-1]], column:  5, indent: 5, spelling: '9', value: `9` },
+    // CHECK:STDOUT:     { index: 18, kind:                    'Comma', line: {{ *}}[[@LINE-2]], column:  6, indent: 5, spelling: ',', has_trailing_space: true },
     0x8,
-    // CHECK:STDOUT: { index: 19, kind:           'IntegerLiteral', line: {{ *}}[[@LINE-1]], column:  5, indent: 5, spelling: '0x8', value: `8` },
-    // CHECK:STDOUT: { index: 20, kind:                    'Comma', line: {{ *}}[[@LINE-2]], column:  8, indent: 5, spelling: ',', has_trailing_space: true },
+    // CHECK:STDOUT:     { index: 19, kind:           'IntegerLiteral', line: {{ *}}[[@LINE-1]], column:  5, indent: 5, spelling: '0x8', value: `8` },
+    // CHECK:STDOUT:     { index: 20, kind:                    'Comma', line: {{ *}}[[@LINE-2]], column:  8, indent: 5, spelling: ',', has_trailing_space: true },
     0b1000,
-    // CHECK:STDOUT: { index: 21, kind:           'IntegerLiteral', line: {{ *}}[[@LINE-1]], column:  5, indent: 5, spelling: '0b1000', value: `8` },
-    // CHECK:STDOUT: { index: 22, kind:                    'Comma', line: {{ *}}[[@LINE-2]], column: 11, indent: 5, spelling: ',', has_trailing_space: true },
+    // CHECK:STDOUT:     { index: 21, kind:           'IntegerLiteral', line: {{ *}}[[@LINE-1]], column:  5, indent: 5, spelling: '0b1000', value: `8` },
+    // CHECK:STDOUT:     { index: 22, kind:                    'Comma', line: {{ *}}[[@LINE-2]], column: 11, indent: 5, spelling: ',', has_trailing_space: true },
     39999999999999999993,
-    // CHECK:STDOUT: { index: 23, kind:           'IntegerLiteral', line: {{ *}}[[@LINE-1]], column:  5, indent: 5, spelling: '39999999999999999993', value: `39999999999999999993` },
-    // CHECK:STDOUT: { index: 24, kind:                    'Comma', line: {{ *}}[[@LINE-2]], column: 25, indent: 5, spelling: ',', has_trailing_space: true },
+    // CHECK:STDOUT:     { index: 23, kind:           'IntegerLiteral', line: {{ *}}[[@LINE-1]], column:  5, indent: 5, spelling: '39999999999999999993', value: `39999999999999999993` },
+    // CHECK:STDOUT:     { index: 24, kind:                    'Comma', line: {{ *}}[[@LINE-2]], column: 25, indent: 5, spelling: ',', has_trailing_space: true },
   );
-  // CHECK:STDOUT: { index: 25, kind:               'CloseParen', line: {{ *}}[[@LINE-1]], column:  3, indent: 3, spelling: ')', opening_token: 14 },
-  // CHECK:STDOUT: { index: 26, kind:                     'Semi', line: {{ *}}[[@LINE-2]], column:  4, indent: 3, spelling: ';', has_trailing_space: true },
+  // CHECK:STDOUT:     { index: 25, kind:               'CloseParen', line: {{ *}}[[@LINE-1]], column:  3, indent: 3, spelling: ')', opening_token: 14 },
+  // CHECK:STDOUT:     { index: 26, kind:                     'Semi', line: {{ *}}[[@LINE-2]], column:  4, indent: 3, spelling: ';', has_trailing_space: true },
   var floats: [f64; 7] = (
-  // CHECK:STDOUT: { index: 27, kind:                      'Var', line: {{ *}}[[@LINE-1]], column:  3, indent: 3, spelling: 'var', has_trailing_space: true },
-  // CHECK:STDOUT: { index: 28, kind:               'Identifier', line: {{ *}}[[@LINE-2]], column:  7, indent: 3, spelling: 'floats', identifier: 2 },
-  // CHECK:STDOUT: { index: 29, kind:                    'Colon', line: {{ *}}[[@LINE-3]], column: 13, indent: 3, spelling: ':', has_trailing_space: true },
-  // CHECK:STDOUT: { index: 30, kind:        'OpenSquareBracket', line: {{ *}}[[@LINE-4]], column: 15, indent: 3, spelling: '[', closing_token: 34 },
-  // CHECK:STDOUT: { index: 31, kind: 'FloatingPointTypeLiteral', line: {{ *}}[[@LINE-5]], column: 16, indent: 3, spelling: 'f64' },
-  // CHECK:STDOUT: { index: 32, kind:                     'Semi', line: {{ *}}[[@LINE-6]], column: 19, indent: 3, spelling: ';', has_trailing_space: true },
-  // CHECK:STDOUT: { index: 33, kind:           'IntegerLiteral', line: {{ *}}[[@LINE-7]], column: 21, indent: 3, spelling: '7', value: `7` },
-  // CHECK:STDOUT: { index: 34, kind:       'CloseSquareBracket', line: {{ *}}[[@LINE-8]], column: 22, indent: 3, spelling: ']', opening_token: 30, has_trailing_space: true },
-  // CHECK:STDOUT: { index: 35, kind:                    'Equal', line: {{ *}}[[@LINE-9]], column: 24, indent: 3, spelling: '=', has_trailing_space: true },
-  // CHECK:STDOUT: { index: 36, kind:                'OpenParen', line: {{ *}}[[@LINE-10]], column: 26, indent: 3, spelling: '(', closing_token: 51, has_trailing_space: true },
+  // CHECK:STDOUT:     { index: 27, kind:                      'Var', line: {{ *}}[[@LINE-1]], column:  3, indent: 3, spelling: 'var', has_trailing_space: true },
+  // CHECK:STDOUT:     { index: 28, kind:               'Identifier', line: {{ *}}[[@LINE-2]], column:  7, indent: 3, spelling: 'floats', identifier: 2 },
+  // CHECK:STDOUT:     { index: 29, kind:                    'Colon', line: {{ *}}[[@LINE-3]], column: 13, indent: 3, spelling: ':', has_trailing_space: true },
+  // CHECK:STDOUT:     { index: 30, kind:        'OpenSquareBracket', line: {{ *}}[[@LINE-4]], column: 15, indent: 3, spelling: '[', closing_token: 34 },
+  // CHECK:STDOUT:     { index: 31, kind: 'FloatingPointTypeLiteral', line: {{ *}}[[@LINE-5]], column: 16, indent: 3, spelling: 'f64' },
+  // CHECK:STDOUT:     { index: 32, kind:                     'Semi', line: {{ *}}[[@LINE-6]], column: 19, indent: 3, spelling: ';', has_trailing_space: true },
+  // CHECK:STDOUT:     { index: 33, kind:           'IntegerLiteral', line: {{ *}}[[@LINE-7]], column: 21, indent: 3, spelling: '7', value: `7` },
+  // CHECK:STDOUT:     { index: 34, kind:       'CloseSquareBracket', line: {{ *}}[[@LINE-8]], column: 22, indent: 3, spelling: ']', opening_token: 30, has_trailing_space: true },
+  // CHECK:STDOUT:     { index: 35, kind:                    'Equal', line: {{ *}}[[@LINE-9]], column: 24, indent: 3, spelling: '=', has_trailing_space: true },
+  // CHECK:STDOUT:     { index: 36, kind:                'OpenParen', line: {{ *}}[[@LINE-10]], column: 26, indent: 3, spelling: '(', closing_token: 51, has_trailing_space: true },
     0.9,
-    // CHECK:STDOUT: { index: 37, kind:              'RealLiteral', line: {{ *}}[[@LINE-1]], column:  5, indent: 5, spelling: '0.9', value: `9*10^-1` },
-    // CHECK:STDOUT: { index: 38, kind:                    'Comma', line: {{ *}}[[@LINE-2]], column:  8, indent: 5, spelling: ',', has_trailing_space: true },
+    // CHECK:STDOUT:     { index: 37, kind:              'RealLiteral', line: {{ *}}[[@LINE-1]], column:  5, indent: 5, spelling: '0.9', value: `9*10^-1` },
+    // CHECK:STDOUT:     { index: 38, kind:                    'Comma', line: {{ *}}[[@LINE-2]], column:  8, indent: 5, spelling: ',', has_trailing_space: true },
     8.0,
-    // CHECK:STDOUT: { index: 39, kind:              'RealLiteral', line: {{ *}}[[@LINE-1]], column:  5, indent: 5, spelling: '8.0', value: `80*10^-1` },
-    // CHECK:STDOUT: { index: 40, kind:                    'Comma', line: {{ *}}[[@LINE-2]], column:  8, indent: 5, spelling: ',', has_trailing_space: true },
+    // CHECK:STDOUT:     { index: 39, kind:              'RealLiteral', line: {{ *}}[[@LINE-1]], column:  5, indent: 5, spelling: '8.0', value: `80*10^-1` },
+    // CHECK:STDOUT:     { index: 40, kind:                    'Comma', line: {{ *}}[[@LINE-2]], column:  8, indent: 5, spelling: ',', has_trailing_space: true },
     80.0,
-    // CHECK:STDOUT: { index: 41, kind:              'RealLiteral', line: {{ *}}[[@LINE-1]], column:  5, indent: 5, spelling: '80.0', value: `800*10^-1` },
-    // CHECK:STDOUT: { index: 42, kind:                    'Comma', line: {{ *}}[[@LINE-2]], column:  9, indent: 5, spelling: ',', has_trailing_space: true },
+    // CHECK:STDOUT:     { index: 41, kind:              'RealLiteral', line: {{ *}}[[@LINE-1]], column:  5, indent: 5, spelling: '80.0', value: `800*10^-1` },
+    // CHECK:STDOUT:     { index: 42, kind:                    'Comma', line: {{ *}}[[@LINE-2]], column:  9, indent: 5, spelling: ',', has_trailing_space: true },
     1.0e7,
-    // CHECK:STDOUT: { index: 43, kind:              'RealLiteral', line: {{ *}}[[@LINE-1]], column:  5, indent: 5, spelling: '1.0e7', value: `10*10^6` },
-    // CHECK:STDOUT: { index: 44, kind:                    'Comma', line: {{ *}}[[@LINE-2]], column: 10, indent: 5, spelling: ',', has_trailing_space: true },
+    // CHECK:STDOUT:     { index: 43, kind:              'RealLiteral', line: {{ *}}[[@LINE-1]], column:  5, indent: 5, spelling: '1.0e7', value: `10*10^6` },
+    // CHECK:STDOUT:     { index: 44, kind:                    'Comma', line: {{ *}}[[@LINE-2]], column: 10, indent: 5, spelling: ',', has_trailing_space: true },
     1.0e8,
-    // CHECK:STDOUT: { index: 45, kind:              'RealLiteral', line: {{ *}}[[@LINE-1]], column:  5, indent: 5, spelling: '1.0e8', value: `10*10^7` },
-    // CHECK:STDOUT: { index: 46, kind:                    'Comma', line: {{ *}}[[@LINE-2]], column: 10, indent: 5, spelling: ',', has_trailing_space: true },
+    // CHECK:STDOUT:     { index: 45, kind:              'RealLiteral', line: {{ *}}[[@LINE-1]], column:  5, indent: 5, spelling: '1.0e8', value: `10*10^7` },
+    // CHECK:STDOUT:     { index: 46, kind:                    'Comma', line: {{ *}}[[@LINE-2]], column: 10, indent: 5, spelling: ',', has_trailing_space: true },
     1.0e-8,
-    // CHECK:STDOUT: { index: 47, kind:              'RealLiteral', line: {{ *}}[[@LINE-1]], column:  5, indent: 5, spelling: '1.0e-8', value: `10*10^-9` },
-    // CHECK:STDOUT: { index: 48, kind:                    'Comma', line: {{ *}}[[@LINE-2]], column: 11, indent: 5, spelling: ',', has_trailing_space: true },
+    // CHECK:STDOUT:     { index: 47, kind:              'RealLiteral', line: {{ *}}[[@LINE-1]], column:  5, indent: 5, spelling: '1.0e-8', value: `10*10^-9` },
+    // CHECK:STDOUT:     { index: 48, kind:                    'Comma', line: {{ *}}[[@LINE-2]], column: 11, indent: 5, spelling: ',', has_trailing_space: true },
     39999999999999999993.0e39999999999999999993,
-    // CHECK:STDOUT: { index: 49, kind:              'RealLiteral', line: {{ *}}[[@LINE-1]], column:  5, indent: 5, spelling: '39999999999999999993.0e39999999999999999993', value: `399999999999999999930*10^39999999999999999992` },
-    // CHECK:STDOUT: { index: 50, kind:                    'Comma', line: {{ *}}[[@LINE-2]], column: 48, indent: 5, spelling: ',', has_trailing_space: true },
+    // CHECK:STDOUT:     { index: 49, kind:              'RealLiteral', line: {{ *}}[[@LINE-1]], column:  5, indent: 5, spelling: '39999999999999999993.0e39999999999999999993', value: `399999999999999999930*10^39999999999999999992` },
+    // CHECK:STDOUT:     { index: 50, kind:                    'Comma', line: {{ *}}[[@LINE-2]], column: 48, indent: 5, spelling: ',', has_trailing_space: true },
   );
-  // CHECK:STDOUT: { index: 51, kind:               'CloseParen', line: {{ *}}[[@LINE-1]], column:  3, indent: 3, spelling: ')', opening_token: 36 },
-  // CHECK:STDOUT: { index: 52, kind:                     'Semi', line: {{ *}}[[@LINE-2]], column:  4, indent: 3, spelling: ';', has_trailing_space: true },
+  // CHECK:STDOUT:     { index: 51, kind:               'CloseParen', line: {{ *}}[[@LINE-1]], column:  3, indent: 3, spelling: ')', opening_token: 36 },
+  // CHECK:STDOUT:     { index: 52, kind:                     'Semi', line: {{ *}}[[@LINE-2]], column:  4, indent: 3, spelling: ';', has_trailing_space: true },
 }
-// CHECK:STDOUT: { index: 53, kind:          'CloseCurlyBrace', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: '}', opening_token: 4, has_trailing_space: true },
+// CHECK:STDOUT:     { index: 53, kind:          'CloseCurlyBrace', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: '}', opening_token: 4, has_trailing_space: true },
 
-// CHECK:STDOUT: { index: 54, kind:                'EndOfFile', line: {{ *}}[[@LINE+1]], column: {{ *\d+}}, indent: 1, spelling: '' },
-// CHECK:STDOUT: ]
+// CHECK:STDOUT:     { index: 54, kind:                'EndOfFile', line: {{ *}}[[@LINE+1]], column: {{ *\d+}}, indent: 1, spelling: '' },
+// CHECK:STDOUT:   ]

+ 8 - 7
toolchain/lex/testdata/printing_digit_padding.carbon

@@ -3,16 +3,17 @@
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
 // AUTOUPDATE
-// CHECK:STDOUT: [
+// CHECK:STDOUT: - filename: printing_digit_padding.carbon
+// CHECK:STDOUT:   tokens: [
 
 ;
-// CHECK:STDOUT: { index: 0, kind:      'Semi', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: ';', has_trailing_space: true },
+// CHECK:STDOUT:     { index: 0, kind:      'Semi', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: ';', has_trailing_space: true },
 
 // Test digit padding with values of 9, 10, and 11.
         ;;;
-        // CHECK:STDOUT: { index: 1, kind:      'Semi', line: {{ *}}[[@LINE-1]], column:  9, indent: 9, spelling: ';' },
-        // CHECK:STDOUT: { index: 2, kind:      'Semi', line: {{ *}}[[@LINE-2]], column: 10, indent: 9, spelling: ';' },
-        // CHECK:STDOUT: { index: 3, kind:      'Semi', line: {{ *}}[[@LINE-3]], column: 11, indent: 9, spelling: ';', has_trailing_space: true },
+        // CHECK:STDOUT:     { index: 1, kind:      'Semi', line: {{ *}}[[@LINE-1]], column:  9, indent: 9, spelling: ';' },
+        // CHECK:STDOUT:     { index: 2, kind:      'Semi', line: {{ *}}[[@LINE-2]], column: 10, indent: 9, spelling: ';' },
+        // CHECK:STDOUT:     { index: 3, kind:      'Semi', line: {{ *}}[[@LINE-3]], column: 11, indent: 9, spelling: ';', has_trailing_space: true },
 
-// CHECK:STDOUT: { index: 4, kind: 'EndOfFile', line: {{ *}}[[@LINE+1]], column: {{ *\d+}}, indent: 1, spelling: '' },
-// CHECK:STDOUT: ]
+// CHECK:STDOUT:     { index: 4, kind: 'EndOfFile', line: {{ *}}[[@LINE+1]], column: {{ *\d+}}, indent: 1, spelling: '' },
+// CHECK:STDOUT:   ]

+ 5 - 4
toolchain/lex/testdata/printing_integer_literal.carbon

@@ -3,10 +3,11 @@
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
 // AUTOUPDATE
-// CHECK:STDOUT: [
+// CHECK:STDOUT: - filename: printing_integer_literal.carbon
+// CHECK:STDOUT:   tokens: [
 
 123
-// CHECK:STDOUT: { index: 0, kind: 'IntegerLiteral', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: '123', value: `123`, has_trailing_space: true },
+// CHECK:STDOUT:     { index: 0, kind: 'IntegerLiteral', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: '123', value: `123`, has_trailing_space: true },
 
-// CHECK:STDOUT: { index: 1, kind:      'EndOfFile', line: {{ *}}[[@LINE+1]], column: {{ *\d+}}, indent: 1, spelling: '' },
-// CHECK:STDOUT: ]
+// CHECK:STDOUT:     { index: 1, kind:      'EndOfFile', line: {{ *}}[[@LINE+1]], column: {{ *\d+}}, indent: 1, spelling: '' },
+// CHECK:STDOUT:   ]

+ 5 - 4
toolchain/lex/testdata/printing_real_literal.carbon

@@ -3,10 +3,11 @@
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
 // AUTOUPDATE
-// CHECK:STDOUT: [
+// CHECK:STDOUT: - filename: printing_real_literal.carbon
+// CHECK:STDOUT:   tokens: [
 
 2.5
-// CHECK:STDOUT: { index: 0, kind: 'RealLiteral', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: '2.5', value: `25*10^-1`, has_trailing_space: true },
+// CHECK:STDOUT:     { index: 0, kind: 'RealLiteral', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: '2.5', value: `25*10^-1`, has_trailing_space: true },
 
-// CHECK:STDOUT: { index: 1, kind:   'EndOfFile', line: {{ *}}[[@LINE+1]], column: {{ *\d+}}, indent: 1, spelling: '' },
-// CHECK:STDOUT: ]
+// CHECK:STDOUT:     { index: 1, kind:   'EndOfFile', line: {{ *}}[[@LINE+1]], column: {{ *\d+}}, indent: 1, spelling: '' },
+// CHECK:STDOUT:   ]

+ 5 - 4
toolchain/lex/testdata/printing_token.carbon

@@ -3,10 +3,11 @@
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
 // AUTOUPDATE
-// CHECK:STDOUT: [
+// CHECK:STDOUT: - filename: printing_token.carbon
+// CHECK:STDOUT:   tokens: [
 
 0x9
-// CHECK:STDOUT: { index: 0, kind: 'IntegerLiteral', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: '0x9', value: `9`, has_trailing_space: true },
+// CHECK:STDOUT:     { index: 0, kind: 'IntegerLiteral', line: {{ *}}[[@LINE-1]], column:  1, indent: 1, spelling: '0x9', value: `9`, has_trailing_space: true },
 
-// CHECK:STDOUT: { index: 1, kind:      'EndOfFile', line: {{ *}}[[@LINE+1]], column: {{ *\d+}}, indent: 1, spelling: '' },
-// CHECK:STDOUT: ]
+// CHECK:STDOUT:     { index: 1, kind:      'EndOfFile', line: {{ *}}[[@LINE+1]], column: {{ *\d+}}, indent: 1, spelling: '' },
+// CHECK:STDOUT:   ]

+ 5 - 3
toolchain/lex/tokenized_buffer.cpp

@@ -1045,18 +1045,20 @@ auto TokenizedBuffer::Print(llvm::raw_ostream& output_stream) const -> void {
     return;
   }
 
+  output_stream << "- filename: " << source_->filename() << "\n"
+                << "  tokens: [\n";
+
   PrintWidths widths = {};
   widths.index = ComputeDecimalPrintedWidth((token_infos_.size()));
   for (Token token : tokens()) {
     widths.Widen(GetTokenPrintWidths(token));
   }
 
-  output_stream << "[\n";
   for (Token token : tokens()) {
     PrintToken(output_stream, token, widths);
     output_stream << "\n";
   }
-  output_stream << "]\n";
+  output_stream << "  ]\n";
 }
 
 auto TokenizedBuffer::PrintToken(llvm::raw_ostream& output_stream,
@@ -1075,7 +1077,7 @@ auto TokenizedBuffer::PrintToken(llvm::raw_ostream& output_stream, Token token,
   // justification manually in order to use the dynamically computed widths
   // and get the quotes included.
   output_stream << llvm::formatv(
-      "{ index: {0}, kind: {1}, line: {2}, column: {3}, indent: {4}, "
+      "    { index: {0}, kind: {1}, line: {2}, column: {3}, indent: {4}, "
       "spelling: '{5}'",
       llvm::format_decimal(token_index, widths.index),
       llvm::right_justify(llvm::formatv("'{0}'", token_info.kind.name()).str(),

+ 32 - 29
toolchain/lex/tokenized_buffer_test.cpp

@@ -1001,35 +1001,38 @@ TEST_F(LexerTest, PrintingAsYaml) {
   TestRawOstream print_stream;
   buffer.Print(print_stream);
 
-  EXPECT_THAT(Yaml::Value::FromText(print_stream.TakeStr()),
-              ElementsAre(Yaml::SequenceValue{
-                  Yaml::MappingValue{{"index", "0"},
-                                     {"kind", "Semi"},
-                                     {"line", "2"},
-                                     {"column", "2"},
-                                     {"indent", "2"},
-                                     {"spelling", ";"},
-                                     {"has_trailing_space", "true"}},
-                  Yaml::MappingValue{{"index", "1"},
-                                     {"kind", "Semi"},
-                                     {"line", "5"},
-                                     {"column", "1"},
-                                     {"indent", "1"},
-                                     {"spelling", ";"},
-                                     {"has_trailing_space", "true"}},
-                  Yaml::MappingValue{{"index", "2"},
-                                     {"kind", "Semi"},
-                                     {"line", "5"},
-                                     {"column", "3"},
-                                     {"indent", "1"},
-                                     {"spelling", ";"},
-                                     {"has_trailing_space", "true"}},
-                  Yaml::MappingValue{{"index", "3"},
-                                     {"kind", "EndOfFile"},
-                                     {"line", "15"},
-                                     {"column", "1"},
-                                     {"indent", "1"},
-                                     {"spelling", ""}}}));
+  EXPECT_THAT(
+      Yaml::Value::FromText(print_stream.TakeStr()),
+      ElementsAre(Yaml::SequenceValue{Yaml::MappingValue{
+          {"filename", Yaml::ScalarValue{source_storage_.front().filename()}},
+          {"tokens", Yaml::SequenceValue{
+                         Yaml::MappingValue{{"index", "0"},
+                                            {"kind", "Semi"},
+                                            {"line", "2"},
+                                            {"column", "2"},
+                                            {"indent", "2"},
+                                            {"spelling", ";"},
+                                            {"has_trailing_space", "true"}},
+                         Yaml::MappingValue{{"index", "1"},
+                                            {"kind", "Semi"},
+                                            {"line", "5"},
+                                            {"column", "1"},
+                                            {"indent", "1"},
+                                            {"spelling", ";"},
+                                            {"has_trailing_space", "true"}},
+                         Yaml::MappingValue{{"index", "2"},
+                                            {"kind", "Semi"},
+                                            {"line", "5"},
+                                            {"column", "3"},
+                                            {"indent", "1"},
+                                            {"spelling", ";"},
+                                            {"has_trailing_space", "true"}},
+                         Yaml::MappingValue{{"index", "3"},
+                                            {"kind", "EndOfFile"},
+                                            {"line", "15"},
+                                            {"column", "1"},
+                                            {"indent", "1"},
+                                            {"spelling", ""}}}}}}));
 }
 
 }  // namespace

+ 18 - 8
toolchain/testing/file_test.cpp

@@ -9,6 +9,7 @@
 
 #include "llvm/ADT/SmallVector.h"
 #include "llvm/ADT/StringRef.h"
+#include "llvm/Support/FormatVariadic.h"
 #include "llvm/Support/VirtualFileSystem.h"
 #include "testing/file_test/file_test_base.h"
 #include "toolchain/driver/driver.h"
@@ -47,16 +48,25 @@ class ToolchainFileTest : public FileTestBase {
     }
   }
 
-  auto GetLineNumberReplacement(llvm::ArrayRef<llvm::StringRef> filenames)
-      -> LineNumberReplacement override {
+  auto GetDefaultFileRE(llvm::ArrayRef<llvm::StringRef> filenames)
+      -> std::optional<RE2> override {
     if (component_ == "lex") {
-      return {.has_file = false,
-              .pattern = R"(line: (\s*\d+))",
-              // The `{{{{` becomes `{{`.
-              .line_formatv = "{{{{ *}}{0}"};
-    } else {
-      return FileTestBase::GetLineNumberReplacement(filenames);
+      return std::make_optional<RE2>(
+          llvm::formatv(R"(^- filename: ({0})$)", llvm::join(filenames, "|")));
+    }
+    return FileTestBase::GetDefaultFileRE(filenames);
+  }
+
+  auto GetLineNumberReplacements(llvm::ArrayRef<llvm::StringRef> filenames)
+      -> llvm::SmallVector<LineNumberReplacement> override {
+    auto replacements = FileTestBase::GetLineNumberReplacements(filenames);
+    if (component_ == "lex") {
+      replacements.push_back({.has_file = false,
+                              .re = std::make_shared<RE2>(R"(line: (\s*\d+))"),
+                              // The `{{{{` becomes `{{`.
+                              .line_formatv = "{{{{ *}}{0}"});
     }
+    return replacements;
   }
 
   auto DoExtraCheckReplacements(std::string& check_line) -> void override {