[v6] get source line for diagnostic from preprocessed file [PR preprocessor/79106]

Message ID GVXPR02MB11647CD96DDFC0E19CC7FDD2BE1B72@GVXPR02MB11647.eurprd02.prod.outlook.com
State New
Headers
Series [v6] get source line for diagnostic from preprocessed file [PR preprocessor/79106] |

Checks

Context Check Description
linaro-tcwg-bot/tcwg_simplebootstrap_build--master-aarch64-bootstrap success Build passed
linaro-tcwg-bot/tcwg_simplebootstrap_build--master-arm-bootstrap success Build passed

Commit Message

Bader, Lucas Sept. 2, 2026, 6:42 a.m. UTC
  Hi,

Thanks for your detailed review, David! This new version of the patch moves the linemarker cache to the file_cache and addresses your remaining feedback, as well.
Tested with latest trunk.

> But it sounds like the patch has
> had a fair amount of real-world testing.  If so, let's be conservative
> and go with the approach in your patch for now.

I would also prefer to stick to the current approach. The last version of the patch has been running in our production build clusters for over a year now (roughly 10-20 million C++ compile jobs per day across multiple platforms) with no further issues found.

Best regards
Lucas

------------------

Within a compile cluster, only the preprocessed output of GCC is transferred
to remote nodes for compilation. When GCC produces advanced diagnostics
(with -fdiagnostics-show-caret), e.g. prints out the affected source
line and fixit hints, it attempts to read the source file again, even when
compiling a preprocessed file (-fpreprocessed). This leads to wrong
diagnostics when building with a compile cluster, or, more generally,
when changing or deleting the original source file.

This change alters GCC to read from the preprocessed file instead by
calculating the corresponding source line. This behavior is consistent
with clang.

The patch implements this efficiently by using a cache for
linemarkers that are already seen and a memoization of lines
that have already been requested.

The preprocessed-input state and the linemarker cache are stored in the
file_cache rather than in the diagnostics::context.  This avoids using
global_dc from within gcc/diagnostics/ (which is also used by
libgdiagnostics, where there is no global context) and keeps the cache
alive independently of any file_cache_slot that might be evicted.

gcc/c-family/ChangeLog:

	PR preprocessor/79106
	* c-opts.cc (c_common_handle_option): Record -fpreprocessed in the
	file cache.

gcc/ChangeLog:

	PR preprocessor/79106
	* diagnostics/file-cache.cc (struct linemarker_cache_entry): New,
	moved to namespace scope.
	(struct linemarker_cache): Likewise.
	(linemarker_cache::get_closest_linemarker): New function.
	(linemarker_cache::add_linemarker_unique): New function for adding
	linemarkers to the linemarker cache.
	(linemarker_cache::add_result_line): Memoize single result of
	get_source_line_preprocessed.
	(linemarker_cache::get_result_line): Retrieve memoized result of
	get_source_line_preprocessed.
	(is_linemarker_for_file): New function for testing a line for a
	linemarker.
	(file_cache::file_cache): Initialize linemarker cache and
	preprocessed-input state.
	(file_cache::~file_cache): Delete linemarker cache.
	(file_cache::get_source_line_preprocessed): New function for reading
	lines from preprocessed sources.
	(file_cache::get_source_line_maybe_preprocessed): New function.
	(test_parsing_linemarker): New test case.
	(test_linemarker_cache): New test case.
	(test_reading_source_line_preprocessed): New test case.
	(file_cache_cc_tests): Add new test cases.
	* diagnostics/file-cache.h (class file_cache): Add
	get_source_line_preprocessed, get_source_line_maybe_preprocessed,
	set_preprocessed and set_main_input_file_path member functions, and
	the supporting data members.
	* diagnostics/source-printing.cc
	(layout::calculate_x_offset_display): Use
	file_cache::get_source_line_maybe_preprocessed.
	(source_line::source_line): Likewise.
	(layout_printer::print_line): Likewise.
	* opts-global.cc (read_cmdline_options): Record the input filename in
	the file cache.

gcc/testsuite/ChangeLog:

	PR preprocessor/79106
	* g++.dg/lookup/missing-std-include-11.C: Adapt to new behavior.

Signed-off-by: Lucas Bader <lucas.bader@sap.com>
---
 gcc/c-family/c-opts.cc                        |   2 +
 gcc/diagnostics/file-cache.cc                 | 695 +++++++++++++++++-
 gcc/diagnostics/file-cache.h                  |  26 +
 gcc/diagnostics/source-printing.cc            |  13 +-
 gcc/opts-global.cc                            |   6 +
 .../g++.dg/lookup/missing-std-include-11.C    |   2 +-
 6 files changed, 736 insertions(+), 8 deletions(-)

-- 
2.51.0
  

Patch

diff --git a/gcc/c-family/c-opts.cc b/gcc/c-family/c-opts.cc
index 0e2d8bdcfdd..23869c3ea6e 100644
--- a/gcc/c-family/c-opts.cc
+++ b/gcc/c-family/c-opts.cc
@@ -29,6 +29,7 @@  along with GCC; see the file COPYING3.  If not see
 #include "memmodel.h"
 #include "tm_p.h"		/* For C_COMMON_OVERRIDE_OPTIONS.  */
 #include "diagnostic.h"
+#include "diagnostics/file-cache.h"
 #include "c-pragma.h"
 #include "flags.h"
 #include "toplev.h"
@@ -535,6 +536,7 @@  c_common_handle_option (size_t scode, const char *arg, HOST_WIDE_INT value,
 
     case OPT_fpreprocessed:
       cpp_opts->preprocessed = value;
+      global_dc->get_file_cache ().set_preprocessed (value);
       break;
 
     case OPT_fdebug_cpp:
diff --git a/gcc/diagnostics/file-cache.cc b/gcc/diagnostics/file-cache.cc
index d1b7990a3e3..f19955fd422 100644
--- a/gcc/diagnostics/file-cache.cc
+++ b/gcc/diagnostics/file-cache.cc
@@ -45,6 +45,126 @@  file_cache::initialize_input_context (diagnostic_input_charset_callback ccb,
   m_input_context.should_skip_bom = should_skip_bom;
 }
 
+/* Contains info about a single linemarker seen in a preprocessed file.
+   Is used by the linemarker_cache.  */
+struct linemarker_cache_entry
+{
+  /* The source file name.  */
+  char *source_name;
+
+  /* The line in the file containing the linemarker, counting from 1.  */
+  int line;
+
+  /* The line the linemarker refers to.  */
+  int source_line;
+
+  /* The entry's index in the sorted record of all entries.  */
+  unsigned int record_idx;
+
+  linemarker_cache_entry (char_span sn, int l, int sl, unsigned int i)
+    : source_name (sn.xstrdup ()), line (l), source_line (sl), record_idx (i)
+  {}
+  ~linemarker_cache_entry ()
+  {
+    free (source_name);
+  }
+};
+
+/* A cache that is used by get_source_line_preprocessed to speed up
+   finding the correct line to read based on linemarker data.
+   The file_cache owns a single cache for the preprocessed input file.
+   Helps to avoid repeatedly scanning the whole file for linemarkers via
+   read_line_num.  */
+struct linemarker_cache
+{
+  /* String portion of the hash is owned by linemarker_cache_entry,
+     so we use nofree for the linemarker_hash_map.  */
+  typedef hash_map<nofree_string_hash, vec<linemarker_cache_entry *, va_heap>>
+    linemarker_hash_map;
+
+  /* For the memoization map, source names are copied so we use
+     free_string_hash.  */
+  typedef pair_hash<free_string_hash, int_hash<int, -1, -2>>
+    source_file_line_hash;
+  typedef hash_map<source_file_line_hash, int> result_line_map;
+
+  /* The maximum line number of all linemarkers we've seen so far.  */
+  int m_max_linemarker_line;
+
+  /* This hash map is the main entry point to the cache.
+     It maps source paths to a vector of linemarker_cache_entry pointers.
+     It allows efficient lookup of the closest linemarker.
+     For each source file, linemarkers are unique with both physical line
+     and referenced source line numbers in ascending order.
+     The same source line number can be referenced by multiple linemarkers.
+  */
+  linemarker_hash_map *m_linemarkers_by_source_name;
+
+  /* A sorted (by line number ascending) record of all unique linemarkers
+     we encounter.  This is used for efficiently determining if a candidate
+     from the cache is the best candidate.  */
+  vec<linemarker_cache_entry *, va_heap> m_linemarkers;
+
+  /* This map is used for plain memoization of linemarker to
+     physical source line mappings.  This is an additional optimization used
+     to avoid repeated calls to get_source_line_preprocessed
+     for the same source_file and line.
+     Expects a copy of the source_file string and assumes ownership.
+  */
+  result_line_map *m_result_lines_by_source;
+
+  /* Default ctor.  */
+  linemarker_cache ()
+    : m_max_linemarker_line (0),
+      m_linemarkers_by_source_name (new linemarker_hash_map),
+      m_result_lines_by_source (new result_line_map)
+  {
+    m_linemarkers.create (0);
+  }
+
+  /* Used in map traversal to clean up linemarker vectors.  */
+  static bool
+  release_linemarker_cache_vector (ATTRIBUTE_UNUSED const char *const &,
+				   vec<linemarker_cache_entry *> *vec,
+				   ATTRIBUTE_UNUSED void *)
+  {
+    // linemarker_cache_entry is owned by m_linemarkers so
+    // we only need to release the vector.
+    vec->release ();
+    return true;
+  }
+
+  /* Destructor.  */
+  ~linemarker_cache ()
+  {
+    m_linemarkers_by_source_name
+      ->traverse<void *, &release_linemarker_cache_vector> (NULL);
+    delete m_linemarkers_by_source_name;
+    delete m_result_lines_by_source;
+
+    for (unsigned i = 0; i < m_linemarkers.length (); ++i)
+      delete m_linemarkers[i];
+    m_linemarkers.release ();
+  }
+
+  int get_max_linemarker_line () const
+  {
+    return m_max_linemarker_line;
+  }
+  void set_max_linemarker_line (int l)
+  {
+    m_max_linemarker_line = l;
+  }
+
+  int get_result_line (const char *source_name, int source_line) const;
+  void add_result_line (const char *source_name, int source_line, int line);
+  bool get_closest_linemarker (const char *source_name, int source_line,
+			       int *out_linemarker_line,
+			       int *out_linemarker_source_line) const;
+  void add_linemarker_unique (char_span source_name, int line,
+			      int source_line);
+};
+
 /* This is a cache used by get_next_line to store the content of a
    file to be searched for file lines.  */
 class file_cache_slot
@@ -191,7 +311,6 @@  public:
     m_data += offset;
     m_size -= offset;
   }
-
 };
 
 size_t file_cache_slot::line_record_size = 0;
@@ -457,7 +576,9 @@  file_cache_slot::set_content (const char *buf, size_t sz)
 /* file_cache's ctor.  */
 
 file_cache::file_cache ()
-: m_num_file_slots (16), m_file_slots (new file_cache_slot[m_num_file_slots])
+: m_num_file_slots (16), m_file_slots (new file_cache_slot[m_num_file_slots]),
+  m_linemarker_cache (new linemarker_cache),
+  m_main_input_file_path (nullptr), m_is_preprocessed (false)
 {
   initialize_input_context (nullptr, false);
 }
@@ -467,6 +588,7 @@  file_cache::file_cache ()
 file_cache::~file_cache ()
 {
   delete[] m_file_slots;
+  delete m_linemarker_cache;
 }
 
 void
@@ -960,6 +1082,310 @@  file_cache::get_source_file_content (const char *file_path)
   return c->get_full_file_content ();
 }
 
+/* Find the closest cached linemarker for a given SOURCE_LINE and SOURCE_NAME.
+
+   Closest is defined by the smallest distance between the cached source line
+   and the given source line (i.e. the nearest left neighbor or exact match).
+   If we can safely determine that the cached linemarker is the best candidate,
+   return true, otherwise false.  If we have a valid match, store the linemarker
+   physical line number in OUT_LINEMARKER_LINE and the referenced source line
+   in OUT_LINEMARKER_SOURCE_LINE.
+
+   If we are certain that the candidate not the best because we know
+   about more linemarkers after, set OUT_LINEMARKER_LINE to the line of the
+   last known linemarker and OUT_LINEMARKER_SOURCE_LINE to 0.
+   This way, the caller does not have to rescan lines.  */
+
+bool
+linemarker_cache::get_closest_linemarker (
+  const char *source_name, int source_line, int *out_linemarker_line,
+  int *out_linemarker_source_line) const
+{
+  *out_linemarker_source_line = 0;
+  *out_linemarker_line = 0;
+
+  // get sorted list of linemarkers for the given source file from cache
+  vec<linemarker_cache_entry *, va_heap> *linemarkers
+    = m_linemarkers_by_source_name->get (source_name);
+  if (!linemarkers)
+    return false;
+  if (linemarkers->length () == 0)
+    return false;
+
+  // perform a binary search to find the nearest left neighbor or exact match
+  int left = 0, right = linemarkers->length () - 1;
+  while (left <= right)
+    {
+      int mid = left + (right - left) / 2;
+      if ((*linemarkers)[mid]->source_line > source_line)
+       {
+	 right = mid - 1;
+       }
+      else
+       {
+	 left = mid + 1;
+       }
+    }
+  if (right < 0)
+    return false;
+
+  *out_linemarker_source_line = (*linemarkers)[right]->source_line;
+  *out_linemarker_line = (*linemarkers)[right]->line;
+
+  // If there is a linemarker for any file above the candidate, we can
+  // check if the target source line falls in the range between.
+  // If so, the candidate is the best.
+  if (m_linemarkers.length () > (*linemarkers)[right]->record_idx + 1)
+    {
+      linemarker_cache_entry *next_lm
+	= m_linemarkers[(*linemarkers)[right]->record_idx + 1];
+      size_t lines_to_next_lm = next_lm->line - (*linemarkers)[right]->line - 1;
+      size_t offset = source_line - (*linemarkers)[right]->source_line;
+      if (offset < lines_to_next_lm)
+       return true;
+      else
+       {
+	 // we know this candidate cannot be the right one
+	 // because the space between the candidate and the next linemarker
+	 // is not large enough for the offset.
+	 *out_linemarker_source_line = 0;
+	 *out_linemarker_line = 0;
+       }
+    }
+
+  return false;
+}
+
+/* Add a linemarker to the linemarker record in the file_cache_slot  **/
+void
+linemarker_cache::add_linemarker_unique (char_span source_name,
+							  int line,
+							  int source_line)
+{
+  if (line <= get_max_linemarker_line ())
+    return; // seen already
+  set_max_linemarker_line (line);
+
+  // first, add the linemarker to the sorted record along with its index
+  linemarker_cache_entry *entry
+    = new linemarker_cache_entry{source_name, line, source_line,
+				 m_linemarkers.length ()};
+
+  m_linemarkers.safe_push (entry);
+
+  // then, link the entry to the source_file based lookup table,
+  // creating the table entry if it doesn't exist yet
+  vec<linemarker_cache_entry *, va_heap> *linemarkers
+    = m_linemarkers_by_source_name->get (entry->source_name);
+  if (!linemarkers)
+    {
+      vec<linemarker_cache_entry *, va_heap> linemarkers_vec
+	= vec<linemarker_cache_entry *, va_heap> ();
+      linemarkers_vec.create (1);
+      linemarkers_vec.safe_push (entry);
+      m_linemarkers_by_source_name->put (entry->source_name, linemarkers_vec);
+    }
+  else
+    {
+      linemarkers->safe_push (entry);
+    }
+}
+
+/* Memoize a result line for get_source_line_preprocessed calls.  */
+void
+linemarker_cache::add_result_line (const char *source_name,
+						    int source_line,
+						    int result_line)
+{
+  m_result_lines_by_source->put (string_int_pair (xstrdup (source_name),
+						  source_line),
+				 result_line);
+}
+
+/* Return a memoized line number for the given SOURCE_NAME and SOURCE_LINE.
+   If no result is cached, return 0.
+   The cached line number is the physical line which is referenced by
+   the cached linemarker for these inputs.  */
+int
+linemarker_cache::get_result_line (const char *source_name,
+						    int source_line) const
+{
+  int *cached_line = m_result_lines_by_source->get (
+    string_int_pair (source_name, source_line));
+  return cached_line ? *cached_line : 0;
+}
+
+/* Return true if LINE is a linemarker for the given SOURCE_NAME.
+   Linemarkers are of the form:
+   # LINENUM "SOURCE_NAME" [FLAGS]
+   Store the referenced line number in OUT_LINE_NUM.
+   If it is any linemarker, even for a different file, write the file name
+   out to OUT_LINEMARKER_SOURCE_NAME.  */
+static bool
+is_linemarker_for_file (char_span line, const char *source_name,
+			int *out_line_num,
+			char_span *out_linemarker_source_name)
+{
+  // skip leading whitespace
+  size_t i = 0;
+  while (line.length () > i && ISBLANK (line[i]))
+    ++i;
+
+  // a linemarker starts with '# ' followed by a line number
+  if (line.length () <= i + 2 || line[i] != '#' || line[i + 1] != ' '
+      || !ISDIGIT (line[i + 2]))
+    return false;
+
+  *out_line_num = atoi (line.get_buffer () + i + 2);
+
+  // find the leading quote of the source name
+  for (i += 3; i < line.length () && line[i] != '"'; ++i)
+    ;
+  if (i >= line.length ())
+    return false;
+
+  // find the trailing quote of the source name
+  size_t source_name_start = i + 1;
+  size_t j = source_name_start;
+  for (; j < line.length () && line[j] != '"'; ++j)
+    ;
+  if (j >= line.length ())
+    return false;
+
+  size_t source_name_length = j - source_name_start;
+  *out_linemarker_source_name = line.subspan (source_name_start,
+					      source_name_length);
+
+  // the linemarker is only for SOURCE_NAME if the names match exactly
+  if (strlen (source_name) != source_name_length)
+    return false;
+  return memcmp (out_linemarker_source_name->get_buffer (), source_name,
+		 source_name_length) == 0;
+}
+
+/* Return the physical source line that corresponds to SOURCE_NAME/LINE.
+   Read the line from the preprocessed file at FILE_PATH.
+   The line is not nul-terminated.  The returned pointer is only
+   valid until the next call of get_source_line.
+   Note that the line can contain several null characters,
+   so the returned value's length has the actual length of the line.
+   If the function fails, a NULL char_span is returned.
+
+   If we for example want to get line 7 of file.cpp from the preprocessed
+   output, the corresponding line marker might look like this:
+   # 5 "file.cpp"    // line 2050 of the preprocessed file
+   int func ()
+   {
+     int a = 4;
+     int b = 5;
+
+   This means that line 2051 of the preprocessed file is line 5 of the
+   original file.cpp.  So to get line 7 of file.cpp we have to read line
+   2053 of the preprocessed file.  */
+
+char_span
+file_cache::get_source_line_preprocessed (const char *source_name,
+					  const char *file_path, int line)
+{
+  char *buffer = NULL;
+  ssize_t len;
+
+  if (line == 0)
+    return char_span (NULL, 0);
+
+  file_cache_slot *c = lookup_or_add_file (file_path);
+  if (c == NULL)
+    return char_span (NULL, 0);
+
+  // return memoized result if available
+  int cached_line = m_linemarker_cache->get_result_line (source_name, line);
+  if (cached_line != 0)
+    {
+      bool read = c->read_line_num (cached_line, &buffer, &len);
+      if (read)
+       return char_span (buffer, len);
+    }
+
+  // attempt to find a linemarker in the cache
+  int linemarker_line = 0; // the physical line num of the candidate
+  int linemarker_loc = 0;  // the referenced source line num of the candidate
+  bool is_best
+    = m_linemarker_cache->get_closest_linemarker (source_name, line,
+						  &linemarker_line,
+						  &linemarker_loc);
+
+  // we continue reading from (highest) cached location if the potential
+  // candidate is not the best already
+  int current_line = m_linemarker_cache->get_max_linemarker_line () + 1;
+  char_span linemarker_source_name = char_span (NULL, 0);
+  int linemarker_source_line = 0;
+  bool is_match = false;
+  while (!is_best && c->read_line_num (current_line, &buffer, &len))
+    {
+      linemarker_source_name = char_span (NULL, 0);
+      linemarker_source_line = 0;
+      is_match = is_linemarker_for_file (char_span (buffer, len), source_name,
+					 &linemarker_source_line,
+					 &linemarker_source_name);
+
+      if (linemarker_source_name)
+       {
+	 m_linemarker_cache->add_linemarker_unique (linemarker_source_name,
+						    current_line,
+						    linemarker_source_line);
+	 if (linemarker_line > 0 && linemarker_loc > 0)
+	    {
+	      // if our last candidate fits the range to the next linemarker,
+	      // we can stop searching
+	      int lines_to_next_lm = current_line - linemarker_line - 1;
+	      int offset = line - linemarker_loc;
+	      if (offset < lines_to_next_lm)
+		break;
+	      else
+		{
+		  // reset the candidate if we have passed the range
+		  linemarker_line = 0;
+		  linemarker_loc = 0;
+		}
+	    }
+	 if (is_match)
+	    {
+	      // new candidate
+	      linemarker_line = current_line;
+	      linemarker_loc = linemarker_source_line;
+	    }
+       }
+      ++current_line;
+    }
+
+  if (linemarker_line == 0 || linemarker_loc == 0)
+    return char_span (NULL, 0);
+
+  // we have a matching linemarker, read the indicated line
+  size_t physical_line = linemarker_line + (line - linemarker_loc) + 1;
+  bool read = c->read_line_num (physical_line, &buffer, &len);
+  if (!read)
+    return char_span (NULL, 0);
+
+  // memoize result line
+  m_linemarker_cache->add_result_line (source_name, line, physical_line);
+  return char_span (buffer, len);
+}
+
+/* Get the source line for FILE_PATH/LINE.  When compiling preprocessed input,
+   read the line from the preprocessed input file instead, so that diagnostics
+   remain correct when the original source file is unavailable (e.g. within a
+   compile cluster) or has changed since preprocessing.  */
+
+char_span
+file_cache::get_source_line_maybe_preprocessed (const char *file_path, int line)
+{
+  if (m_is_preprocessed && m_main_input_file_path != nullptr)
+    return get_source_line_preprocessed (file_path, m_main_input_file_path,
+					 line);
+  return get_source_line (file_path, line);
+}
+
 #if CHECKING_P
 
 namespace selftest {
@@ -1079,6 +1505,268 @@  test_reading_source_buffer ()
   ASSERT_TRUE (source_line.get_buffer () == NULL);
 }
 
+/* Verify reading of preprocessed input files
+   (e.g. for caret-based diagnostics).  */
+
+static void
+test_parsing_linemarker ()
+{
+  auto assert_is_linemarker_for_file = [] (const ::selftest::location &loc,
+					   const char *buf,
+					   const char *expected_file_name,
+					   int expected_line_num)
+  {
+    int line_num;
+    char_span file_name (nullptr, 0);
+    bool result = is_linemarker_for_file (char_span (buf, strlen (buf)),
+					  expected_file_name,
+					  &line_num, &file_name);
+    ASSERT_TRUE_AT (loc, result);
+    ASSERT_EQ_AT (loc, line_num, expected_line_num);
+    ASSERT_TRUE_AT (loc, file_name);
+    char *actual_file_name = file_name.xstrdup ();
+    ASSERT_STREQ_AT (loc, expected_file_name, actual_file_name);
+    free (actual_file_name);
+  };
+#define ASSERT_IS_LINEMARKER_FOR_FILE(BUF, EXPECTED_FILENAME, EXPECTED_LINE_NUM) \
+  SELFTEST_BEGIN_STMT							\
+    assert_is_linemarker_for_file (SELFTEST_LOCATION, (BUF),		\
+				   (EXPECTED_FILENAME), (EXPECTED_LINE_NUM)); \
+  SELFTEST_END_STMT
+
+  /* EXPECTED_PARSED_NAME asserts the source name parsed out of the linemarker
+     for cases where it does not match the file we search for; it is nullptr
+     when the input is not a linemarker at all.  */
+  auto assert_is_not_linemarker_for_file
+    = [] (const ::selftest::location &loc, const char *buf,
+	  const char *file_to_match, const char *expected_parsed_name)
+  {
+    int line_num;
+    char_span file_name (nullptr, 0);
+    bool result = is_linemarker_for_file (char_span (buf, strlen (buf)),
+					  file_to_match, &line_num, &file_name);
+    ASSERT_FALSE_AT (loc, result);
+    if (expected_parsed_name)
+      {
+	ASSERT_TRUE_AT (loc, file_name);
+	char *actual_file_name = file_name.xstrdup ();
+	ASSERT_STREQ_AT (loc, expected_parsed_name, actual_file_name);
+	free (actual_file_name);
+      }
+    else
+      ASSERT_FALSE_AT (loc, file_name);
+  };
+#define ASSERT_IS_NOT_LINEMARKER_FOR_FILE(BUF, FILENAME, EXPECTED_PARSED_NAME) \
+  SELFTEST_BEGIN_STMT							\
+    assert_is_not_linemarker_for_file (SELFTEST_LOCATION, (BUF), (FILENAME), \
+				       (EXPECTED_PARSED_NAME));		\
+  SELFTEST_END_STMT
+
+  ASSERT_IS_LINEMARKER_FOR_FILE ("# 112 \"test.cpp\"\n", "test.cpp", 112);
+
+  /* Linemarker but for a different file.  */
+  ASSERT_IS_NOT_LINEMARKER_FOR_FILE ("# 23 \"test.hpp\"\n", "test.h",
+				     "test.hpp");
+  ASSERT_IS_NOT_LINEMARKER_FOR_FILE ("# 23 \"test.h\"\n", "test.hpp",
+				     "test.h");
+
+  /* Not a linemarker.  */
+  ASSERT_IS_NOT_LINEMARKER_FOR_FILE ("int main () \n", "test.cpp", nullptr);
+
+  /* Linemarker with flags.  */
+  ASSERT_IS_LINEMARKER_FOR_FILE ("# 23 \"test.h\" 1\n", "test.h", 23);
+
+  /* Malformed linemarkers.  */
+  ASSERT_IS_NOT_LINEMARKER_FOR_FILE ("# 23 \"test.h   \n", "test.h", nullptr);
+  ASSERT_IS_NOT_LINEMARKER_FOR_FILE ("# 2 test.h\n", "test.h", nullptr);
+  ASSERT_IS_NOT_LINEMARKER_FOR_FILE ("#  23 \"test.h\"\n", "test.h", nullptr);
+
+  /* Linemarker not starting at beginning of line, spaces and tabs.  */
+  ASSERT_IS_LINEMARKER_FOR_FILE ("      # 23 \"test.hpp\"\n", "test.hpp", 23);
+
+#undef ASSERT_IS_LINEMARKER_FOR_FILE
+#undef ASSERT_IS_NOT_LINEMARKER_FOR_FILE
+}
+
+static void
+test_linemarker_cache ()
+{
+  linemarker_cache cache;
+
+  // Represents a file like
+  // # 1 "test.cpp" 1
+  // # 1 "test.cpp"
+  // content
+  // # 1 "test.h" 1
+  // # 2 "test.h"
+  // ...
+  // # 35 "test.h"
+  cache.add_linemarker_unique (char_span ("test.cpp", 8), 1, 1);
+  cache.add_linemarker_unique (char_span ("test.cpp", 8), 2, 1);
+
+  int line_num = 0;
+  int source_line_num = 0;
+
+  /* Matching linemarkers.  */
+  bool best
+    = cache.get_closest_linemarker ("test.cpp", 4, &line_num,
+				    &source_line_num);
+  // not surely the best but a match
+  // because there are no further linemarkers known yet
+  ASSERT_FALSE (best);
+  ASSERT_EQ (2, line_num);
+  ASSERT_EQ (1, source_line_num);
+
+  // add more linemarkers
+  cache.add_linemarker_unique (char_span ("test.h", 6), 5, 2);
+  cache.add_linemarker_unique (char_span ("test.h", 6), 15, 35);
+
+  // repeat previous test but with more cache entries
+  best
+    = cache.get_closest_linemarker ("test.cpp", 1, &line_num,
+				    &source_line_num);
+  // best because there is room for one line before the next linemarker
+  ASSERT_TRUE (best);
+  // left-nearest neighbor, i.e. the last linemarker before the source line
+  ASSERT_EQ (2, line_num);
+  ASSERT_EQ (1, source_line_num);
+
+  best
+    = cache.get_closest_linemarker ("test.cpp", 4, &line_num,
+				    &source_line_num);
+  // not the best because there are linemarkers after and the
+  // space between is not enough for the source line offset
+  ASSERT_FALSE (best);
+  ASSERT_EQ (0, line_num);
+  ASSERT_EQ (0, source_line_num);
+
+  best = cache.get_closest_linemarker ("test.h", 10, &line_num,
+				       &source_line_num);
+  // best because there are 9 lines between the linemarker and the next
+  // We can target source line offsets between 0 and 8
+  // and the offset is 8
+  ASSERT_TRUE (best);
+  ASSERT_EQ (5, line_num);
+  ASSERT_EQ (2, source_line_num);
+  best = cache.get_closest_linemarker ("test.h", 11, &line_num,
+				       &source_line_num);
+  // one source line further already falls on the next linemarker
+  ASSERT_FALSE (best);
+  ASSERT_EQ (0, line_num);
+  ASSERT_EQ (0, source_line_num);
+
+  best = cache.get_closest_linemarker ("test.h", 40, &line_num,
+				       &source_line_num);
+  // no linemarker in the cache after this, so we would need to scan further
+  ASSERT_FALSE (best);
+  ASSERT_EQ (15, line_num);
+  ASSERT_EQ (35, source_line_num);
+
+  /* No match.  */
+  best = cache.get_closest_linemarker ("test.h", 1, &line_num,
+				       &source_line_num);
+  // no left-nearest neighbor
+  ASSERT_FALSE (best);
+  ASSERT_EQ (0, line_num);
+  ASSERT_EQ (0, source_line_num);
+
+  best = cache.get_closest_linemarker ("no_match.cpp", 3, &line_num,
+				       &source_line_num);
+  ASSERT_FALSE (best);
+  ASSERT_EQ (0, line_num);
+  ASSERT_EQ (0, source_line_num);
+}
+
+static void
+test_reading_source_line_preprocessed ()
+{
+  /* Create a tempfile and write some valid preprocessor output.  */
+  temp_source_file tmp (SELFTEST_LOCATION, ".cpp.ii",
+			"# 1 \"test.cpp\"\n"
+			"# 1 \"test.cpp\"\n"
+			"# 1 \"test.h\" 1\n"
+			"void test_func () \n"
+			"# 35 \"test.h\"\n"
+			"    do_something_else ();\n"
+			"}\n"
+			"# 35 \"test.hpp\"\n"
+			"    do_nothing ();\n"
+			"}\n"
+			"# 2 \"test.cpp\" 2\n"
+			"\n"
+			"int main () \n"
+			"    do_something ();\n"
+			"\n"
+			"    int i = 5;\n"
+			"    unsigned j = 3;\n"
+			"    if (i > j)\n"
+			"    return 0;\n"
+			"\n"
+			"# 38 \"test.h\"\n"
+			"    random_func ();\n"
+			"    test_func ();\n"
+			"}\n");
+  file_cache fc;
+
+  // Perform all tests twice to verify memoized calls behave the same
+  for (int i = 0; i < 2; i++)
+    {
+      /* Read back a specific line from the tempfile.  */
+      char_span source_line
+	= fc.get_source_line_preprocessed ("test.cpp", tmp.get_filename (), 4);
+      ASSERT_TRUE (source_line);
+      ASSERT_TRUE (source_line.get_buffer () != NULL);
+      ASSERT_EQ (20, source_line.length ());
+      ASSERT_TRUE (!strncmp ("    do_something ();", source_line.get_buffer (),
+			     source_line.length ()));
+
+      source_line
+	= fc.get_source_line_preprocessed ("test.h", tmp.get_filename (), 35);
+      ASSERT_TRUE (source_line);
+      ASSERT_TRUE (source_line.get_buffer () != NULL);
+      ASSERT_EQ (25, source_line.length ());
+      ASSERT_TRUE (!strncmp ("    do_something_else ();",
+			     source_line.get_buffer (), source_line.length ()));
+
+      source_line
+	= fc.get_source_line_preprocessed ("test.h", tmp.get_filename (), 39);
+      ASSERT_TRUE (source_line);
+      ASSERT_TRUE (source_line.get_buffer () != NULL);
+      ASSERT_EQ (17, source_line.length ());
+      ASSERT_TRUE (!strncmp ("    test_func ();", source_line.get_buffer (),
+			     source_line.length ()));
+
+      source_line
+	= fc.get_source_line_preprocessed ("test.hpp", tmp.get_filename (), 35);
+      ASSERT_TRUE (source_line);
+      ASSERT_TRUE (source_line.get_buffer () != NULL);
+      ASSERT_EQ (18, source_line.length ());
+      ASSERT_TRUE (!strncmp ("    do_nothing ();", source_line.get_buffer (),
+			     source_line.length ()));
+
+      // file not present in preprocessor output
+      source_line
+	= fc.get_source_line_preprocessed ("other.h", tmp.get_filename (), 4);
+      ASSERT_FALSE (source_line);
+      ASSERT_TRUE (source_line.get_buffer () == NULL);
+
+      // empty lines omitted by preprocessor
+      source_line
+	= fc.get_source_line_preprocessed ("test.h", tmp.get_filename (), 2);
+      ASSERT_FALSE (source_line);
+      ASSERT_TRUE (source_line.get_buffer () == NULL);
+    }
+
+  // Verify that the cache is working as expected
+  // line not previously read but all linemarkers are in the cache
+  char_span source_line
+    = fc.get_source_line_preprocessed ("test.cpp", tmp.get_filename (), 6);
+  ASSERT_TRUE (source_line);
+  ASSERT_TRUE (source_line.get_buffer () != NULL);
+  ASSERT_TRUE (!strncmp ("    int i = 5;", source_line.get_buffer (),
+			 source_line.length ()));
+}
+
 /* Run all of the selftests within this file.  */
 
 void
@@ -1087,6 +1775,9 @@  file_cache_cc_tests ()
   test_reading_source_line ();
   test_reading_source_buffer ();
   test_replacement ();
+  test_parsing_linemarker ();
+  test_linemarker_cache ();
+  test_reading_source_line_preprocessed ();
 }
 
 } // namespace selftest
diff --git a/gcc/diagnostics/file-cache.h b/gcc/diagnostics/file-cache.h
index 09a16a887f4..6efd3276fcf 100644
--- a/gcc/diagnostics/file-cache.h
+++ b/gcc/diagnostics/file-cache.h
@@ -69,6 +69,10 @@  class char_span
    need to be in this header.  */
 class file_cache_slot;
 
+/* Forward decl of the linemarker cache used when reading source lines from
+   a preprocessed input file; its definition is private to file-cache.cc.  */
+struct linemarker_cache;
+
 /* A cache of source files for use when emitting diagnostics
    (and in a few places in the C/C++ frontends).
 
@@ -101,6 +105,10 @@  class file_cache
 
   char_span get_source_file_content (const char *file_path);
   char_span get_source_line (const char *file_path, int line);
+  char_span get_source_line_preprocessed (const char *source_name,
+					  const char *file_path, int line);
+  char_span get_source_line_maybe_preprocessed (const char *file_path,
+						int line);
   bool missing_trailing_newline_p (const char *file_path);
 
   void add_buffered_content (const char *file_path,
@@ -109,6 +117,13 @@  class file_cache
 
   void tune (size_t num_file_slots, size_t lines);
 
+  /* Configure reading of source lines from a preprocessed input file.  */
+  void set_preprocessed (bool preprocessed) { m_is_preprocessed = preprocessed; }
+  void set_main_input_file_path (const char *path)
+  {
+    m_main_input_file_path = path;
+  }
+
  private:
   file_cache_slot *evicted_cache_tab_entry (unsigned *highest_use_count);
   file_cache_slot *add_file (const char *file_path);
@@ -118,6 +133,17 @@  class file_cache
   size_t m_num_file_slots;
   file_cache_slot *m_file_slots;
   input_context m_input_context;
+
+  /* Cache of linemarkers seen in the preprocessed input file, used to speed
+     up get_source_line_preprocessed.  */
+  linemarker_cache *m_linemarker_cache;
+
+  /* The original (preprocessed) input file to read source lines from when
+     m_is_preprocessed is true.  */
+  const char *m_main_input_file_path;
+
+  /* True if the input file is treated as preprocessed.  */
+  bool m_is_preprocessed;
 };
 
 } // namespace diagnostics
diff --git a/gcc/diagnostics/source-printing.cc b/gcc/diagnostics/source-printing.cc
index db9b3c1f241..607e08d032e 100644
--- a/gcc/diagnostics/source-printing.cc
+++ b/gcc/diagnostics/source-printing.cc
@@ -2202,9 +2202,9 @@  layout::calculate_x_offset_display ()
       return;
     }
 
-  const diagnostics::char_span line
-    = m_file_cache.get_source_line (m_exploc.file,
-				    m_exploc.line);
+  diagnostics::char_span line
+    = m_file_cache.get_source_line_maybe_preprocessed (m_exploc.file,
+						       m_exploc.line);
   if (!line)
     {
       /* Nothing to do, we couldn't find the source line.  */
@@ -3365,7 +3365,8 @@  source_line::source_line (diagnostics::file_cache &fc,
 			  const char *filename,
 			  int line)
 {
-  diagnostics::char_span span = fc.get_source_line (filename, line);
+  diagnostics::char_span span
+    = fc.get_source_line_maybe_preprocessed (filename, line);
   chars = span.get_buffer ();
   width = span.length ();
 }
@@ -3722,7 +3723,9 @@  layout_printer<TextOrHtml>::print_line (linenum_type row)
   typename TextOrHtml::auto_check_tag_nesting sentinel (m_text_or_html);
 
   diagnostics::char_span line
-    = m_layout.m_file_cache.get_source_line (m_layout.m_exploc.file, row);
+    = m_layout.m_file_cache.get_source_line_maybe_preprocessed
+	(m_layout.m_exploc.file, row);
+
   if (!line)
     return;
 
diff --git a/gcc/opts-global.cc b/gcc/opts-global.cc
index 30fad240d83..4187b97a1a8 100644
--- a/gcc/opts-global.cc
+++ b/gcc/opts-global.cc
@@ -26,6 +26,7 @@  along with GCC; see the file COPYING3.  If not see
 #include "tree.h"
 #include "tree-pass.h"
 #include "diagnostic.h"
+#include "diagnostics/file-cache.h"
 #include "opts.h"
 #include "flags.h"
 #include "langhooks.h"
@@ -231,6 +232,11 @@  read_cmdline_options (struct gcc_options *opts, struct gcc_options *opts_set,
 	  if (opts->x_main_input_filename == NULL)
 	    {
 	      opts->x_main_input_filename = decoded_options[i].arg;
+	      // remember original input filename in the file cache
+	      // because if a preprocessed file is compiled, this is
+	      // changed to the original source file name later
+	      dc->get_file_cache ().set_main_input_file_path
+		(opts->x_main_input_filename);
 	      opts->x_main_input_baselength
 		= base_of_path (opts->x_main_input_filename,
 				&opts->x_main_input_basename);
diff --git a/gcc/testsuite/g++.dg/lookup/missing-std-include-11.C b/gcc/testsuite/g++.dg/lookup/missing-std-include-11.C
index ec2c494c557..5128f415c43 100644
--- a/gcc/testsuite/g++.dg/lookup/missing-std-include-11.C
+++ b/gcc/testsuite/g++.dg/lookup/missing-std-include-11.C
@@ -40,4 +40,4 @@  int main ()
 }
 // { dg-additional-files "missing-std-include-10.h" }
 // { dg-regexp {[^\n]*: error: 'strcmp' was not declared in this scope\n *return strcmp [^\n]*;\n *\^~*\n} }
-// { dg-regexp {[^\n]* note: 'strcmp' is defined in header[^\n]*\n #include "missing-std-include-10.h"\n\+#include <cstring>\n // HERE\n} }
+// { dg-regexp {[^\n]* note: 'strcmp' is defined in header[^\n]*\n} }