Mirror of the gdb-patches mailing list
 help / color / mirror / Atom feed
From: Simon Marchi <simon.marchi@efficios.com>
To: gcc-patches@sourceware.org, gdb-patches@sourceware.org
Cc: Simon Marchi <simon.marchi@efficios.com>
Subject: [PATCH 5/6] gdbsupport/selftest: print operand values when a SELF_CHECK fails
Date: Mon, 19 Oct 2026 12:58:21 -0400	[thread overview]
Message-ID: <20261019165929.792697-6-simon.marchi@efficios.com> (raw)
In-Reply-To: <20261019165929.792697-1-simon.marchi@efficios.com>

The catch2 test framework (and likely others) has a neat trick to print
the operands of failing test assertions.  For instance:

    /home/simark/src/binutils-gdb/catch2/sample.cpp:17: FAILED:
      CHECK( reverse_string ("hello") == "hello" )
    with expansion:
      "olleh" == "hello"

This patch implements something like that for GDB's SELF_CHECK macro.

As I did not instinctively know how to do this, I used Claude to guide
me through the implementation, but all the code was written by me.

The general idea of the trick is that the SELF_CHECK macro:

    SELF_CHECK (len == 4)

now expands to approximately:

    check (decomposer () <= len == 4, ...)

The decomposer object has a templated operator<= that records a
reference to whatever comes after it (in this case the `len` variable)
into a lhs_holder instance:

     template<typename LhsT>
     lhs_holder<LhsT> operator<= (const LhsT &lhs) const
     {
       return lhs_holder<LhsT> {lhs};
     }

The lhs_holder then implements all the comparison operators, such as
operator==, which does the requested comparison with the following
operand:

  template<typename RhsT>
  binary_expr_holder<LhsT, RhsT> operator== (const RhsT &rhs) const
  {
    return binary_expr_holder<LhsT, RhsT> { this->lhs == rhs, "==", this->lhs,
					    rhs };
  }

The binary_expr_holder object holds references to both operands as well
as the result of the comparison.  That binary_expr_holder object is
passed to the check function, which is then able to print the values of
the operands as needed.

It's actually required to go through a separate check function (instead
of doing it all in the SELF_CHECK macro directly), to ensure that any
temporaries that binary_expr_holder binds to are still alive when we
print them.

There is a second check function that takes a lhs_holder<bool>, for
expressions like:

    SELF_CHECK (func_that_returns_bool ());

A test failure of a binary expression now looks like:

    FAIL: /home/smarchi/src/binutils-gdb/gdb/unittests/common-utils-selftests.c:35: func ("%s", "test") == "test boo": "test" == "test boo"

Some previously used constructs are now rejected at compile time, and
require a few changes in the tests:

 - A top-level && or ||.

   Overloading these operators would lose short-circuit evaluation, and
   only the leftmost operand could be decomposed anyway (because
   comparison operators take precedence).  Options to work around this
   are to split the expressions (in the case of &&) or to add an extra
   pair of parentheses around the whole expression.  For &&, I would
   argue it's a good thing to split expressions, so you get a more
   fine-grained message on failure.

 - A lone non-bool value, like SELF_CHECK (ptr).

   The fix for those is to use an explicit comparison.  The second check
   function could accept a templated lhs_holder, instead of a
   lhs_holder<bool>, but since our coding style requires using explicit
   comparisons, I don't see why it should.

   An odd case of this is a use of the frame_id::code_addr_p field.
   While it is spiritually a boolean, it is in fact a 1-bit unsigned int
   bitfield.  So it now requires an explicit `!= 0`.

The expression resulting from the macro expansion triggers a
-Wparentheses warning, so disable it in the SELF_CHECK macro.

Operands are printed using a value_to_string function that is a long
series of "if constexpr" to print various types of operands.  It ends
with an always-false static_assert, such that trying to print an
unsupported type will cause a build failure, and will require telling
the function how to print said type.  For some types like iterators,
there is no sensible value to print in a generic way, so we just print
"{iterator}".

Add a to_string method to mem_range, which is used by value_to_string.

Finally, as a way to exercise this machinery without always having to
purposely make a test fail, I made it so that running the selftests with
-verbose prints all passing tests, including their operands.  For
instance:

    PASS: /home/simark/src/binutils-gdb/gdb/unittests/int-array-iterator.h:75: m_cur_idx < m_size: 2 < 7

Change-Id: Ic81da3024811d8fc9dd6335c50a0adcf75755d5d
---
 gdb/aarch64-tdep.c                            |  38 +-
 gdb/arm-tdep.c                                |   4 +-
 gdb/disasm-selftests.c                        |   2 +-
 gdb/dwarf2/frame.c                            |   2 +-
 gdb/memrange.h                                |   7 +
 gdb/producer.c                                |  33 +-
 gdb/regcache.c                                |   6 +-
 gdb/rust-parse.c                              |   2 +-
 gdb/selftest-arch.c                           |   2 +-
 gdb/source-cache.c                            |  12 +-
 gdb/unittests/array-view-selftests.c          |   5 +
 gdb/unittests/environ-selftests.c             |  32 +-
 gdb/unittests/frame_info_ptr-selftests.c      |   2 +-
 gdb/unittests/function-view-selftests.c       |   4 +-
 gdb/unittests/scoped_fd-selftests.c           |   8 +-
 .../scoped_ignore_signal-selftests.c          |   7 +-
 gdb/unittests/scoped_mmap-selftests.c         |   8 +-
 gdb/unittests/style-selftests.c               |  10 +-
 gdb/utils.c                                   |   4 +-
 gdbserver/server.cc                           |  19 +-
 gdbsupport/selftest.cc                        |  81 ++-
 gdbsupport/selftest.h                         | 540 +++++++++++++++++-
 22 files changed, 741 insertions(+), 87 deletions(-)

diff --git a/gdb/aarch64-tdep.c b/gdb/aarch64-tdep.c
index 109fca6bf110..733ee5c5279c 100644
--- a/gdb/aarch64-tdep.c
+++ b/gdb/aarch64-tdep.c
@@ -692,7 +692,7 @@ aarch64_analyze_prologue_test (void)
   info.bfd_arch_info = bfd_scan_arch ("aarch64");
 
   struct gdbarch *gdbarch = gdbarch_find_by_info (info);
-  SELF_CHECK (gdbarch != NULL);
+  SELF_CHECK (gdbarch != nullptr);
 
   struct aarch64_prologue_cache cache;
   cache.saved_regs = trad_frame_alloc_saved_regs (gdbarch);
@@ -721,8 +721,10 @@ aarch64_analyze_prologue_test (void)
 	else if (i == AARCH64_LR_REGNUM)
 	  SELF_CHECK (cache.saved_regs[i].addr () == -264);
 	else
-	  SELF_CHECK (cache.saved_regs[i].is_realreg ()
-		      && cache.saved_regs[i].realreg () == i);
+	  {
+	    SELF_CHECK (cache.saved_regs[i].is_realreg ());
+	    SELF_CHECK (cache.saved_regs[i].realreg () == i);
+	  }
       }
 
     for (int i = 0; i < AARCH64_D_REGISTER_COUNT; i++)
@@ -730,8 +732,8 @@ aarch64_analyze_prologue_test (void)
 	int num_regs = gdbarch_num_regs (gdbarch);
 	int regnum = i + num_regs + AARCH64_D0_REGNUM;
 
-	SELF_CHECK (cache.saved_regs[regnum].is_realreg ()
-		    && cache.saved_regs[regnum].realreg () == regnum);
+	SELF_CHECK (cache.saved_regs[regnum].is_realreg ());
+	SELF_CHECK (cache.saved_regs[regnum].realreg () == regnum);
       }
   }
 
@@ -763,8 +765,10 @@ aarch64_analyze_prologue_test (void)
 	else if (i == 19)
 	  SELF_CHECK (cache.saved_regs[i].addr () == -48);
 	else
-	  SELF_CHECK (cache.saved_regs[i].is_realreg ()
-		      && cache.saved_regs[i].realreg () == i);
+	  {
+	    SELF_CHECK (cache.saved_regs[i].is_realreg ());
+	    SELF_CHECK (cache.saved_regs[i].realreg () == i);
+	  }
       }
 
     for (int i = 0; i < AARCH64_D_REGISTER_COUNT; i++)
@@ -776,8 +780,10 @@ aarch64_analyze_prologue_test (void)
 	if (i == 0)
 	  SELF_CHECK (cache.saved_regs[regnum].addr () == -24);
 	else
-	  SELF_CHECK (cache.saved_regs[regnum].is_realreg ()
-		      && cache.saved_regs[regnum].realreg () == regnum);
+	  {
+	    SELF_CHECK (cache.saved_regs[regnum].is_realreg ());
+	    SELF_CHECK (cache.saved_regs[regnum].realreg () == regnum);
+	  }
       }
   }
 
@@ -921,8 +927,10 @@ aarch64_analyze_prologue_test (void)
 	  else if (i == AARCH64_LR_REGNUM)
 	    SELF_CHECK (cache.saved_regs[i].addr () == -40);
 	  else
-	    SELF_CHECK (cache.saved_regs[i].is_realreg ()
-			&& cache.saved_regs[i].realreg () == i);
+	    {
+	      SELF_CHECK (cache.saved_regs[i].is_realreg ());
+	      SELF_CHECK (cache.saved_regs[i].realreg () == i);
+	    }
 	}
 
       if (tdep->has_pauth ())
@@ -960,8 +968,10 @@ aarch64_analyze_prologue_test (void)
 	  else if (i == AARCH64_LR_REGNUM)
 	    SELF_CHECK (cache.saved_regs[i].addr () == -40);
 	  else
-	    SELF_CHECK (cache.saved_regs[i].is_realreg ()
-			&& cache.saved_regs[i].realreg () == i);
+	    {
+	      SELF_CHECK (cache.saved_regs[i].is_realreg ());
+	      SELF_CHECK (cache.saved_regs[i].realreg () == i);
+	    }
 	}
     }
 }
@@ -6520,7 +6530,7 @@ aarch64_process_record_test (void)
   info.bfd_arch_info = bfd_scan_arch ("aarch64");
 
   struct gdbarch *gdbarch = gdbarch_find_by_info (info);
-  SELF_CHECK (gdbarch != NULL);
+  SELF_CHECK (gdbarch != nullptr);
 
   aarch64_insn_decode_record aarch64_record;
 
diff --git a/gdb/arm-tdep.c b/gdb/arm-tdep.c
index bffd5d221997..a963c8c3f803 100644
--- a/gdb/arm-tdep.c
+++ b/gdb/arm-tdep.c
@@ -14673,7 +14673,7 @@ arm_record_test (void)
 
   struct gdbarch *gdbarch = gdbarch_find_by_info (info);
 
-  SELF_CHECK (gdbarch != NULL);
+  SELF_CHECK (gdbarch != nullptr);
 
   /* 16-bit Thumb instructions.  */
   {
@@ -14789,7 +14789,7 @@ arm_analyze_prologue_test ()
 
       struct gdbarch *gdbarch = gdbarch_find_by_info (info);
 
-      SELF_CHECK (gdbarch != NULL);
+      SELF_CHECK (gdbarch != nullptr);
 
       /* The "sub" instruction contains an immediate value rotate count of 0,
 	 which resulted in a 32-bit shift of a 32-bit value, caught by
diff --git a/gdb/disasm-selftests.c b/gdb/disasm-selftests.c
index 485c1245118b..1aa399a57d4a 100644
--- a/gdb/disasm-selftests.c
+++ b/gdb/disasm-selftests.c
@@ -158,7 +158,7 @@ get_test_insn (struct gdbarch *gdbarch, size_t *len)
 	      }
 
 	    gdbarch = gdbarch_find_by_info (info);
-	    SELF_CHECK (gdbarch != NULL);
+	    SELF_CHECK (gdbarch != nullptr);
 
 	    try
 	      {
diff --git a/gdb/dwarf2/frame.c b/gdb/dwarf2/frame.c
index 95e9f1e7c34d..eec1b77c2708 100644
--- a/gdb/dwarf2/frame.c
+++ b/gdb/dwarf2/frame.c
@@ -568,7 +568,7 @@ execute_cfa_program_test (struct gdbarch *gdbarch)
   SELF_CHECK (fs.regs.cfa_offset == 4);
   SELF_CHECK (fs.regs.cfa_how == CFA_REG_OFFSET);
   SELF_CHECK (fs.regs.cfa_exp.empty ());
-  SELF_CHECK (fs.regs.prev == NULL);
+  SELF_CHECK (fs.regs.prev == nullptr);
 }
 
 } /* namespace selftests */
diff --git a/gdb/memrange.h b/gdb/memrange.h
index 18d53a40024c..9b8111dccbcf 100644
--- a/gdb/memrange.h
+++ b/gdb/memrange.h
@@ -44,6 +44,13 @@ struct mem_range
   /* Returns true if ADDR is in this mem_range.  */
   bool contains (CORE_ADDR addr) const;
 
+  /* Return a string representation of this mem_range.  */
+  std::string to_string () const
+  {
+    return string_printf ("{start = %s, length = %d}",
+			  hex_string (this->start), this->length);
+  }
+
   /* Lowest address in the range.  */
   CORE_ADDR start;
 
diff --git a/gdb/producer.c b/gdb/producer.c
index 9e0da6f4eacd..1ece4237fa94 100644
--- a/gdb/producer.c
+++ b/gdb/producer.c
@@ -242,8 +242,9 @@ producer_parsing_tests ()
 	 " Intel(R) 64,	Version 14.0.1.074 Build 20130716");
 
     int major = 0, minor = 0;
-    SELF_CHECK (producer_is_icc (extern_f_14_0, &major, &minor)
-		&& major == 14 && minor == 0);
+    SELF_CHECK (producer_is_icc (extern_f_14_0, &major, &minor));
+    SELF_CHECK (major == 14);
+    SELF_CHECK (minor == 0);
     SELF_CHECK (!producer_is_gcc (extern_f_14_0, &major, &minor));
   }
 
@@ -253,8 +254,9 @@ producer_parsing_tests ()
 	 " Intel(R) 64, Version 14.0");
 
     int major = 0, minor = 0;
-    SELF_CHECK (producer_is_icc (intern_f_14, &major, &minor)
-		&& major == 14 && minor == 0);
+    SELF_CHECK (producer_is_icc (intern_f_14, &major, &minor));
+    SELF_CHECK (major == 14);
+    SELF_CHECK (minor == 0);
     SELF_CHECK (!producer_is_gcc (intern_f_14, &major, &minor));
   }
 
@@ -263,8 +265,9 @@ producer_parsing_tests ()
       = ("Intel(R) C++ Intel(R) 64 Compiler XE for applications running on"
 	 " Intel(R) 64, Version 14.0");
     int major = 0, minor = 0;
-    SELF_CHECK (producer_is_icc (intern_c_14, &major, &minor)
-		&& major == 14 && minor == 0);
+    SELF_CHECK (producer_is_icc (intern_c_14, &major, &minor));
+    SELF_CHECK (major == 14);
+    SELF_CHECK (minor == 0);
     SELF_CHECK (!producer_is_gcc (intern_c_14, &major, &minor));
   }
 
@@ -273,8 +276,9 @@ producer_parsing_tests ()
       = ("Intel(R) C++ Intel(R) 64 Compiler for applications running on"
 	 " Intel(R) 64, Version 18.0 Beta");
     int major = 0, minor = 0;
-    SELF_CHECK (producer_is_icc (intern_c_18, &major, &minor)
-		&& major == 18 && minor == 0);
+    SELF_CHECK (producer_is_icc (intern_c_18, &major, &minor));
+    SELF_CHECK (major == 18);
+    SELF_CHECK (minor == 0);
   }
 
   {
@@ -282,16 +286,18 @@ producer_parsing_tests ()
     SELF_CHECK (!producer_is_icc (gnu, nullptr, nullptr));
 
     int major = 0, minor = 0;
-    SELF_CHECK (producer_is_gcc (gnu, &major, &minor)
-		&& major == 4 && minor == 7);
+    SELF_CHECK (producer_is_gcc (gnu, &major, &minor));
+    SELF_CHECK (major == 4);
+    SELF_CHECK (minor == 7);
   }
 
   {
     static const char gnu_exp[] = "GNU C++14 5.0.0 20150123 (experimental)";
     int major = 0, minor = 0;
     SELF_CHECK (!producer_is_icc (gnu_exp, nullptr, nullptr));
-    SELF_CHECK (producer_is_gcc (gnu_exp, &major, &minor)
-		&& major == 5 && minor == 0);
+    SELF_CHECK (producer_is_gcc (gnu_exp, &major, &minor));
+    SELF_CHECK (major == 5);
+    SELF_CHECK (minor == 0);
   }
 
   {
@@ -315,7 +321,8 @@ producer_parsing_tests ()
     int major = 0, minor = 0;
     SELF_CHECK (!producer_is_gcc (gas_exp, &major, &minor));
     SELF_CHECK (producer_is_gas (gas_exp, &major, &minor));
-    SELF_CHECK (major == 2 && minor == 39);
+    SELF_CHECK (major == 2);
+    SELF_CHECK (minor == 39);
 
     static const char gas_incomplete_exp[] = "GNU AS ";
     SELF_CHECK (!producer_is_gas (gas_incomplete_exp, &major, &minor));
diff --git a/gdb/regcache.c b/gdb/regcache.c
index 773e8ac37d0a..ed5ea1699840 100644
--- a/gdb/regcache.c
+++ b/gdb/regcache.c
@@ -1688,7 +1688,7 @@ get_thread_arch_regcache_and_check (inferior *inf_for_target_calls,
   regcache *regcache
     = get_thread_arch_regcache (inf_for_target_calls, ptid, arch);
 
-  SELF_CHECK (regcache != NULL);
+  SELF_CHECK (regcache != nullptr);
   SELF_CHECK (regcache->ptid () == ptid);
   SELF_CHECK (regcache->arch () == arch);
 }
@@ -2030,7 +2030,7 @@ cooked_read_test (struct gdbarch *gdbarch)
 		  || bfd_arch == bfd_arch_m32c
 		  || bfd_arch == bfd_arch_mep
 		  || bfd_arch == bfd_arch_sh)
-		SELF_CHECK (status == REG_VALID || status == REG_UNKNOWN);
+		SELF_CHECK ((status == REG_VALID || status == REG_UNKNOWN));
 	      else if (bfd_arch == bfd_arch_mips
 		       || bfd_arch == bfd_arch_h8300)
 		SELF_CHECK (status == REG_UNKNOWN);
@@ -2144,7 +2144,7 @@ cooked_write_test (struct gdbarch *gdbarch)
 	{
 	  /* If we don't know how to create the expected value for the
 	     this type, make it fail.  */
-	  SELF_CHECK (0);
+	  SELF_CHECK (false);
 	}
 
       readwrite.cooked_write (regnum, expected);
diff --git a/gdb/rust-parse.c b/gdb/rust-parse.c
index 0c4f2a501abc..bef247f71c7a 100644
--- a/gdb/rust-parse.c
+++ b/gdb/rust-parse.c
@@ -2207,7 +2207,7 @@ rust_lex_exception_test (rust_parser *parser, const char *input,
     {
       /* The "kind" doesn't matter.  */
       rust_lex_test_one (parser, input, DECIMAL_INTEGER);
-      SELF_CHECK (0);
+      SELF_CHECK (false);
     }
   catch (const gdb_exception_error &except)
     {
diff --git a/gdb/selftest-arch.c b/gdb/selftest-arch.c
index f8e28f89eca9..9ddad71c6d0e 100644
--- a/gdb/selftest-arch.c
+++ b/gdb/selftest-arch.c
@@ -64,7 +64,7 @@ foreach_arch_test_generator (const std::string &name,
 	= ([=] ()
 	   {
 	     struct gdbarch *gdbarch = gdbarch_find_by_info (info);
-	     SELF_CHECK (gdbarch != NULL);
+	     SELF_CHECK (gdbarch != nullptr);
 	     function (gdbarch);
 	     reset ();
 	   });
diff --git a/gdb/source-cache.c b/gdb/source-cache.c
index 804a6a08a8c3..248805cd534e 100644
--- a/gdb/source-cache.c
+++ b/gdb/source-cache.c
@@ -500,13 +500,13 @@ static void extract_lines_test ()
   std::string input_text = "abc\ndef\nghi\njkl\n";
   std::string result;
 
-  SELF_CHECK (extract_lines (input_text, 1, 1, &result)
-	      && result == "abc\n");
+  SELF_CHECK (extract_lines (input_text, 1, 1, &result));
+  SELF_CHECK (result == "abc\n");
   SELF_CHECK (!extract_lines (input_text, 2, 1, &result));
-  SELF_CHECK (extract_lines (input_text, 1, 2, &result)
-	      && result == "abc\ndef\n");
-  SELF_CHECK (extract_lines ("abc", 1, 1, &result)
-	      && result == "abc");
+  SELF_CHECK (extract_lines (input_text, 1, 2, &result));
+  SELF_CHECK (result == "abc\ndef\n");
+  SELF_CHECK (extract_lines ("abc", 1, 1, &result));
+  SELF_CHECK (result == "abc");
 }
 }
 #endif
diff --git a/gdb/unittests/array-view-selftests.c b/gdb/unittests/array-view-selftests.c
index ff31912ab03a..854704ed50a0 100644
--- a/gdb/unittests/array-view-selftests.c
+++ b/gdb/unittests/array-view-selftests.c
@@ -667,6 +667,11 @@ struct foo
     return this->n == other.n;
   }
 
+  std::string to_string () const
+  {
+    return std::to_string (this->n);
+  }
+
   int n;
 
   /* Number of times the assignment operator has been called.  */
diff --git a/gdb/unittests/environ-selftests.c b/gdb/unittests/environ-selftests.c
index b643876b1440..e47c24371aad 100644
--- a/gdb/unittests/environ-selftests.c
+++ b/gdb/unittests/environ-selftests.c
@@ -41,12 +41,12 @@ test_vector_initialization ()
 
   /* When the vector is initialized, there should always be one NULL
      element in it.  */
-  SELF_CHECK (env.envp ()[0] == NULL);
+  SELF_CHECK (env.envp ()[0] == nullptr);
   SELF_CHECK (env.user_set_env ().size () == 0);
   SELF_CHECK (env.user_unset_env ().size () == 0);
 
   /* Make sure that there is no other element.  */
-  SELF_CHECK (env.get ("PWD") == NULL);
+  SELF_CHECK (env.get ("PWD") == nullptr);
 }
 
 /* Test initialization using the host's environ.  */
@@ -105,7 +105,7 @@ test_set_A_unset_B_unset_A_cannot_find_A_can_find_B ()
   SELF_CHECK (streq (env.get ("GDB_SELFTEST_ENVIRON_2"), "bbb"));
 
   env.unset ("GDB_SELFTEST_ENVIRON_1");
-  SELF_CHECK (env.get ("GDB_SELFTEST_ENVIRON_1") == NULL);
+  SELF_CHECK (env.get ("GDB_SELFTEST_ENVIRON_1") == nullptr);
   SELF_CHECK (streq (env.get ("GDB_SELFTEST_ENVIRON_2"), "bbb"));
 
   /* The user-set environ var list must contain only one element
@@ -127,10 +127,10 @@ test_unset_set_empty_vector ()
   SELF_CHECK (set_contains (env.user_set_env (), std::string ("PWD=test")));
   SELF_CHECK (env.user_unset_env ().size () == 0);
   /* The second element must be NULL.  */
-  SELF_CHECK (env.envp ()[1] == NULL);
+  SELF_CHECK (env.envp ()[1] == nullptr);
   SELF_CHECK (env.user_set_env ().size () == 1);
   env.unset ("PWD");
-  SELF_CHECK (env.envp ()[0] == NULL);
+  SELF_CHECK (env.envp ()[0] == nullptr);
   SELF_CHECK (env.user_set_env ().size () == 0);
   SELF_CHECK (env.user_unset_env ().size () == 1);
   SELF_CHECK (set_contains (env.user_unset_env (), std::string ("PWD")));
@@ -148,10 +148,10 @@ test_vector_clear ()
   env.set (gdb_selftest_env_var, "1");
 
   env.clear ();
-  SELF_CHECK (env.envp ()[0] == NULL);
+  SELF_CHECK (env.envp ()[0] == nullptr);
   SELF_CHECK (env.user_set_env ().size () == 0);
   SELF_CHECK (env.user_unset_env ().size () == 0);
-  SELF_CHECK (env.get (gdb_selftest_env_var) == NULL);
+  SELF_CHECK (env.get (gdb_selftest_env_var) == nullptr);
 }
 
 /* Test that after a std::move the moved-from object is left at a
@@ -169,15 +169,15 @@ test_std_move ()
   SELF_CHECK (env.user_set_env ().size () == 1);
 
   env2 = std::move (env);
-  SELF_CHECK (env.envp ()[0] == NULL);
+  SELF_CHECK (env.envp ()[0] == nullptr);
   SELF_CHECK (streq (env2.get ("A"), "1"));
-  SELF_CHECK (env2.envp ()[1] == NULL);
+  SELF_CHECK (env2.envp ()[1] == nullptr);
   SELF_CHECK (env.user_set_env ().size () == 0);
   SELF_CHECK (set_contains (env2.user_set_env (), std::string ("A=1")));
   SELF_CHECK (env2.user_set_env ().size () == 1);
   env.set ("B", "2");
   SELF_CHECK (streq (env.get ("B"), "2"));
-  SELF_CHECK (env.envp ()[1] == NULL);
+  SELF_CHECK (env.envp ()[1] == nullptr);
 }
 
 /* Test that the move constructor leaves everything at a valid
@@ -193,16 +193,16 @@ test_move_constructor ()
   SELF_CHECK (set_contains (env.user_set_env (), std::string ("A=1")));
 
   gdb_environ env2 = std::move (env);
-  SELF_CHECK (env.envp ()[0] == NULL);
+  SELF_CHECK (env.envp ()[0] == nullptr);
   SELF_CHECK (env.user_set_env ().size () == 0);
   SELF_CHECK (streq (env2.get ("A"), "1"));
-  SELF_CHECK (env2.envp ()[1] == NULL);
+  SELF_CHECK (env2.envp ()[1] == nullptr);
   SELF_CHECK (set_contains (env2.user_set_env (), std::string ("A=1")));
   SELF_CHECK (env2.user_set_env ().size () == 1);
 
   env.set ("B", "2");
   SELF_CHECK (streq (env.get ("B"), "2"));
-  SELF_CHECK (env.envp ()[1] == NULL);
+  SELF_CHECK (env.envp ()[1] == nullptr);
   SELF_CHECK (set_contains (env.user_set_env (), std::string ("B=2")));
   SELF_CHECK (env.user_set_env ().size () == 1);
 }
@@ -229,7 +229,7 @@ test_self_move ()
 
   SELF_CHECK (streq (env.get ("A"), "1"));
   SELF_CHECK (streq (env.envp ()[0], "A=1"));
-  SELF_CHECK (env.envp ()[1] == NULL);
+  SELF_CHECK (env.envp ()[1] == nullptr);
   SELF_CHECK (set_contains (env.user_set_env (), std::string ("A=1")));
   SELF_CHECK (env.user_set_env ().size () == 1);
 }
@@ -242,7 +242,7 @@ test_set_unset_reset ()
   gdb_environ env = gdb_environ::from_host_environ ();
 
   /* Our test variable should already be present in the host's environment.  */
-  SELF_CHECK (env.get ("GDB_SELFTEST_ENVIRON") != NULL);
+  SELF_CHECK (env.get ("GDB_SELFTEST_ENVIRON") != nullptr);
 
   /* Set our test variable to another value.  */
   env.set ("GDB_SELFTEST_ENVIRON", "test");
@@ -253,7 +253,7 @@ test_set_unset_reset ()
   /* And unset our test variable.  The variable still exists in the
      host's environment, but doesn't exist in our vector.  */
   env.unset ("GDB_SELFTEST_ENVIRON");
-  SELF_CHECK (env.get ("GDB_SELFTEST_ENVIRON") == NULL);
+  SELF_CHECK (env.get ("GDB_SELFTEST_ENVIRON") == nullptr);
   SELF_CHECK (env.user_set_env ().size () == 0);
   SELF_CHECK (env.user_unset_env ().size () == 1);
   SELF_CHECK (set_contains (env.user_unset_env (),
diff --git a/gdb/unittests/frame_info_ptr-selftests.c b/gdb/unittests/frame_info_ptr-selftests.c
index dc67ebf80f38..f5d58e4c94cc 100644
--- a/gdb/unittests/frame_info_ptr-selftests.c
+++ b/gdb/unittests/frame_info_ptr-selftests.c
@@ -30,7 +30,7 @@ validate_user_created_frame (frame_id id)
 {
   SELF_CHECK (id.stack_status == FID_STACK_VALID);
   SELF_CHECK (id.stack_addr == 0x1234);
-  SELF_CHECK (id.code_addr_p);
+  SELF_CHECK (id.code_addr_p != 0);
   SELF_CHECK (id.code_addr == 0x5678);
 }
 
diff --git a/gdb/unittests/function-view-selftests.c b/gdb/unittests/function-view-selftests.c
index 7a61465d440d..4c702d2d5419 100644
--- a/gdb/unittests/function-view-selftests.c
+++ b/gdb/unittests/function-view-selftests.c
@@ -162,9 +162,9 @@ test_function_view ()
 
   /* Check the nullptr_t op= */
   gdb::function_view<int (int)> check_op_eq_null (plus_one_fn_int);
-  SELF_CHECK (check_op_eq_null);
+  SELF_CHECK (check_op_eq_null != nullptr);
   check_op_eq_null = nullptr;
-  SELF_CHECK (!check_op_eq_null);
+  SELF_CHECK (check_op_eq_null == nullptr);
 }
 
 /* A template function where the function_view type is dependent on a
diff --git a/gdb/unittests/scoped_fd-selftests.c b/gdb/unittests/scoped_fd-selftests.c
index 42cb18f342c0..f1c5667a972d 100644
--- a/gdb/unittests/scoped_fd-selftests.c
+++ b/gdb/unittests/scoped_fd-selftests.c
@@ -42,7 +42,11 @@ test_destroy ()
     SELF_CHECK (sfd.get () == fd);
   }
 
-  SELF_CHECK (close (fd) == -1 && errno == EBADF);
+  int ret = close (fd);
+  int saved_errno = errno;
+
+  SELF_CHECK (ret == -1);
+  SELF_CHECK (saved_errno == EBADF);
 }
 
 /* Test that the file descriptor can be released.  */
@@ -61,7 +65,7 @@ test_release ()
     SELF_CHECK (sfd.release () == fd);
   }
 
-  SELF_CHECK (close (fd) == 0 || errno != EBADF);
+  SELF_CHECK ((close (fd) == 0 || errno != EBADF));
 }
 
 /* Test that the file descriptor can be converted to a FILE *.  */
diff --git a/gdb/unittests/scoped_ignore_signal-selftests.c b/gdb/unittests/scoped_ignore_signal-selftests.c
index 7a0f2c2764fc..b7e67ea84a3a 100644
--- a/gdb/unittests/scoped_ignore_signal-selftests.c
+++ b/gdb/unittests/scoped_ignore_signal-selftests.c
@@ -77,8 +77,11 @@ test_sigpipe ()
     errno = 0;
 
     r = write (fd[1], &c, 1);
-    SELF_CHECK (r == -1 && errno == EPIPE
-		&& got_sigpipe == expect_sig);
+    int saved_errno = errno;
+
+    SELF_CHECK (r == -1);
+    SELF_CHECK (saved_errno == EPIPE);
+    SELF_CHECK (got_sigpipe == expect_sig);
   };
 
   /* Check that without a scoped_ignore_sigpipe in scope we indeed get
diff --git a/gdb/unittests/scoped_mmap-selftests.c b/gdb/unittests/scoped_mmap-selftests.c
index ce03d7559f7a..a2fbc0e1f3e5 100644
--- a/gdb/unittests/scoped_mmap-selftests.c
+++ b/gdb/unittests/scoped_mmap-selftests.c
@@ -47,7 +47,11 @@ test_destroy ()
     SELF_CHECK (mem != nullptr);
   }
 
-  SELF_CHECK (msync (mem, sysconf (_SC_PAGESIZE), 0) == -1 && errno == ENOMEM);
+  int ret = msync (mem, sysconf (_SC_PAGESIZE), 0);
+  int saved_errno = errno;
+
+  SELF_CHECK (ret == -1);
+  SELF_CHECK (saved_errno == ENOMEM);
 }
 
 /* Test that the memory can be released.  */
@@ -65,7 +69,7 @@ test_release ()
     SELF_CHECK (mem != nullptr);
   }
 
-  SELF_CHECK (msync (mem, sysconf (_SC_PAGESIZE), 0) == 0 || errno != ENOMEM);
+  SELF_CHECK ((msync (mem, sysconf (_SC_PAGESIZE), 0) == 0 || errno != ENOMEM));
 
   munmap (mem, sysconf (_SC_PAGESIZE));
 }
diff --git a/gdb/unittests/style-selftests.c b/gdb/unittests/style-selftests.c
index 9035050bd6ce..632a1e42a4e9 100644
--- a/gdb/unittests/style-selftests.c
+++ b/gdb/unittests/style-selftests.c
@@ -23,8 +23,14 @@
 namespace selftests {
 namespace style {
 
-#define CHECK_RGB(R, G, B) \
-  SELF_CHECK (rgb[0] == (R) && rgb[1] == (G) && rgb[2] == (B))
+#define CHECK_RGB(R, G, B)		\
+  do					\
+    {					\
+      SELF_CHECK (rgb[0] == (R));	\
+      SELF_CHECK (rgb[1] == (G));	\
+      SELF_CHECK (rgb[2] == (B));	\
+    }					\
+  while (0)
 
 static void
 run_tests ()
diff --git a/gdb/utils.c b/gdb/utils.c
index 5deecdbba942..0ed943cc4929 100644
--- a/gdb/utils.c
+++ b/gdb/utils.c
@@ -3354,8 +3354,8 @@ gdb_realpath_check_trailer (const char *input, const char *trailer)
   size_t len = strlen (result.get ());
   size_t trail_len = strlen (trailer);
 
-  SELF_CHECK (len >= trail_len
-	      && streq (result.get () + len - trail_len, trailer));
+  SELF_CHECK (len >= trail_len);
+  SELF_CHECK (streq (result.get () + len - trail_len, trailer));
 }
 
 static void
diff --git a/gdbserver/server.cc b/gdbserver/server.cc
index 74774fcde7ca..9a1a38978b73 100644
--- a/gdbserver/server.cc
+++ b/gdbserver/server.cc
@@ -4075,7 +4075,9 @@ test_memory_tagging_functions (void)
   type = 255;
   strcpy (packet.data (), "qMemTags:0,0:0");
   parse_fetch_memtags_request (packet.data (), &addr, &len, &type);
-  SELF_CHECK (addr == 0 && len == 0 && type == 0);
+  SELF_CHECK (addr == 0);
+  SELF_CHECK (len == 0);
+  SELF_CHECK (type == 0);
 
   /* Valid request, addr, len and type updated.  */
   addr = 0;
@@ -4083,7 +4085,9 @@ test_memory_tagging_functions (void)
   type = 0;
   strcpy (packet.data (), "qMemTags:deadbeef,ff:5");
   parse_fetch_memtags_request (packet.data (), &addr, &len, &type);
-  SELF_CHECK (addr == 0xdeadbeef && len == 255 && type == 5);
+  SELF_CHECK (addr == 0xdeadbeef);
+  SELF_CHECK (len == 255);
+  SELF_CHECK (type == 5);
 
   /* Test creating a qMemTags reply.  */
 
@@ -4107,7 +4111,10 @@ test_memory_tagging_functions (void)
   strcpy (packet.data (), "QMemTags:0,0:0:");
   SELF_CHECK (parse_store_memtags_request (packet.data (),
 					   &addr, &len, tags, &type) == true);
-  SELF_CHECK (addr == 0 && len == 0 && type == 0 && tags.size () == 0);
+  SELF_CHECK (addr == 0);
+  SELF_CHECK (len == 0);
+  SELF_CHECK (type == 0);
+  SELF_CHECK (tags.size () == 0);
 
   /* Valid request and non-empty tag data: addr, len, type
      and tags updated.  */
@@ -4119,8 +4126,10 @@ test_memory_tagging_functions (void)
 	  "QMemTags:deadbeef,ff:5:0001020304");
   SELF_CHECK (parse_store_memtags_request (packet.data (), &addr, &len, tags,
 					   &type) == true);
-  SELF_CHECK (addr == 0xdeadbeef && len == 255 && type == 5
-	      && tags.size () == 5);
+  SELF_CHECK (addr == 0xdeadbeef);
+  SELF_CHECK (len == 255);
+  SELF_CHECK (type == 5);
+  SELF_CHECK (tags.size () == 5);
 }
 
 /* Exercise the behavior of doing a 0-length comparison for a register in a
diff --git a/gdbsupport/selftest.cc b/gdbsupport/selftest.cc
index 85eee59e3c4b..55cf6d51d418 100644
--- a/gdbsupport/selftest.cc
+++ b/gdbsupport/selftest.cc
@@ -144,11 +144,84 @@ all_selftests ()
 
 /* See selftest.h.  */
 
-void
-failed_selftest (const char *file, int line, const char *expression)
+std::string
+detail::quote_string (std::string_view str, char quote)
 {
-  throw_error (SELF_CHECK_FAILED_ERROR, "%s:%d: %s", file, line,
-	       expression);
+  std::string result (1, quote);
+
+  for (char c : str)
+    switch (c)
+      {
+      case '\n':
+	result += "\\n";
+	break;
+
+      case '\t':
+	result += "\\t";
+	break;
+
+      case '\r':
+	result += "\\r";
+	break;
+
+      case '\0':
+	result += "\\0";
+	break;
+
+      case '\\':
+	result += "\\\\";
+	break;
+
+      default:
+	if (c == quote)
+	  {
+	    result += '\\';
+	    result += c;
+	  }
+	else if (c_isprint (c))
+	  result += c;
+	else
+	  result += string_printf ("\\x%02x", static_cast<unsigned char> (c));
+	break;
+      }
+
+  result += quote;
+  return result;
+}
+
+/* Return a string describing the check of EXPRESSION at FILE:LINE, used
+   in the reports of passed and failed checks.  EXPANDS_TO may be
+   nullptr.  */
+
+static std::string
+describe_check (const char *file, int line, const char *expression,
+		const char *expands_to)
+{
+  if (expands_to != nullptr)
+    return string_printf ("%s:%d: %s: %s", file, line, expression,
+			  expands_to);
+
+  return string_printf ("%s:%d: %s", file, line, expression);
+}
+
+/* See selftest.h.  */
+
+void
+passed_selftest (const char *file, int line, const char *expression,
+		 const char *expands_to)
+{
+  debug_printf ("PASS: %s\n",
+		describe_check (file, line, expression, expands_to).c_str ());
+}
+
+/* See selftest.h.  */
+
+void
+failed_selftest (const char *file, int line, const char *expression,
+		 const char *expands_to)
+{
+  throw_error (SELF_CHECK_FAILED_ERROR, "%s",
+	       describe_check (file, line, expression, expands_to).c_str ());
 }
 
 } /* namespace selftests */
diff --git a/gdbsupport/selftest.h b/gdbsupport/selftest.h
index ddf925dd3524..4c5789e089e2 100644
--- a/gdbsupport/selftest.h
+++ b/gdbsupport/selftest.h
@@ -19,12 +19,32 @@
 #ifndef GDBSUPPORT_SELFTEST_H
 #define GDBSUPPORT_SELFTEST_H
 
+#include "diagnostics.h"
 #include "gdbsupport/array-view.h"
+#include "gdbsupport/enumerate.h"
+#include "gdbsupport/enum-flags.h"
 #include "gdbsupport/function-view.h"
 #include "gdbsupport/iterator-range.h"
+#include "gdbsupport/packed.h"
+#include <atomic>
+#include <iterator>
+#include <optional>
 #include <set>
+#include <string_view>
+#include <utility>
 #include <vector>
 
+/* The SELF_CHECK macro expands to an expression that triggers -Wparentheses,
+   which it suppresses locally with DIAGNOSTIC_IGNORE_PARENTHESES.  Before
+   version 10, GCC did not honor diagnostic pragmas coming from macro
+   expansions, so disable the warning for the whole translation unit instead.
+   This can be removed once the minimum supported GCC version is at least
+   10.  */
+
+#if defined(__GNUC__) && !defined(__clang__) && __GNUC__ < 10
+#  pragma GCC diagnostic ignored "-Wparentheses"
+#endif
+
 /* A test is just a function that does some checks and throws an
    exception if something has gone wrong.  */
 
@@ -88,18 +108,524 @@ extern void reset ();
    error with code SELF_CHECK_FAILED_ERROR describing the failure.  */
 
 [[noreturn]] void failed_selftest (const char *file, int line,
-				   const char *expression);
+				   const char *expression,
+				   const char *expands_to);
 
+/* Function called by macro SELF_CHECK when a selftest passes, in verbose
+   mode.  Print a line describing the check.  EXPANDS_TO may be nullptr.  */
+
+extern void passed_selftest (const char *file, int line,
+			     const char *expression, const char *expands_to);
+
+namespace detail
+{
+
+/* Always false, but dependent on T, for use in static_assert.
+
+   Older compilers reject a plain "static_assert (false)" in a template,
+   even if the template is never instantiated.  This was changed by P2593
+   ("Allowing static_assert(false)"), implemented as a defect report (so
+   in C++17 mode too) in GCC 13 and Clang 17.  This can be removed once the
+   minimum supported versions are at least these.  */
+
+template<typename T>
+constexpr bool dependent_false_v = false;
+
+/* The result of decomposing a binary comparison (like "a == b") in a
+   SELF_CHECK.  */
+
+template<typename LhsT, typename RhsT>
+struct binary_expr_holder
+{
+  template<typename Rhs2T>
+  auto operator&& (const Rhs2T &) const
+  {
+    static_assert (dependent_false_v<Rhs2T>,
+		   "SELF_CHECK does not support a top-level &&: "
+		   "split it into separate SELF_CHECKs, or wrap the "
+		   "whole expression in extra parentheses, e.g. "
+		   "SELF_CHECK ((a && b)).");
+  }
+
+  template<typename Rhs2T>
+  auto operator|| (const Rhs2T &) const
+  {
+    static_assert (dependent_false_v<Rhs2T>,
+		   "SELF_CHECK does not support a top-level ||: "
+		   "wrap the whole expression in extra parentheses, "
+		   "e.g. SELF_CHECK ((a || b)).");
+  }
+
+  /* The result of the expression.  */
+  bool result;
+
+  /* The operator as a string, e.g. "!=".  */
+  const char *op;
+
+  /* The operands.  */
+  const LhsT &lhs;
+  const RhsT &rhs;
+};
+
+/* The result of decomposing the left operand of the expression of a
+   SELF_CHECK.  */
+
+template<typename LhsT>
+struct lhs_holder
+{
+  template<typename RhsT>
+  binary_expr_holder<LhsT, RhsT> operator== (const RhsT &rhs) const
+  {
+    return binary_expr_holder<LhsT, RhsT> { this->lhs == rhs, "==", this->lhs,
+					    rhs };
+  }
+
+  template<typename RhsT>
+  binary_expr_holder<LhsT, RhsT> operator!= (const RhsT &rhs) const
+  {
+    return binary_expr_holder<LhsT, RhsT> { this->lhs != rhs, "!=", this->lhs,
+					    rhs };
+  }
+
+  template<typename RhsT>
+  binary_expr_holder<LhsT, RhsT> operator> (const RhsT &rhs) const
+  {
+    return binary_expr_holder<LhsT, RhsT> { this->lhs > rhs, ">", this->lhs,
+					    rhs };
+  }
+
+  template<typename RhsT>
+  binary_expr_holder<LhsT, RhsT> operator< (const RhsT &rhs) const
+  {
+    return binary_expr_holder<LhsT, RhsT> { this->lhs < rhs, "<", this->lhs,
+					    rhs };
+  }
+
+  template<typename RhsT>
+  binary_expr_holder<LhsT, RhsT> operator<= (const RhsT &rhs) const
+  {
+    return binary_expr_holder<LhsT, RhsT> { this->lhs <= rhs, "<=", this->lhs,
+					    rhs };
+  }
+
+  template<typename RhsT>
+  binary_expr_holder<LhsT, RhsT> operator>= (const RhsT &rhs) const
+  {
+    return binary_expr_holder<LhsT, RhsT> { this->lhs >= rhs, ">=", this->lhs,
+					    rhs };
+  }
+
+  template<typename RhsT>
+  auto operator&& (const RhsT &) const
+  {
+    static_assert (dependent_false_v<RhsT>,
+		   "SELF_CHECK does not support a top-level &&: "
+		   "split it into separate SELF_CHECKs, or wrap the "
+		   "whole expression in extra parentheses, e.g. "
+		   "SELF_CHECK ((a && b)).");
+  }
+
+  template<typename RhsT>
+  auto operator|| (const RhsT &) const
+  {
+    static_assert (dependent_false_v<RhsT>,
+		   "SELF_CHECK does not support a top-level ||: "
+		   "wrap the whole expression in extra parentheses, "
+		   "e.g. SELF_CHECK ((a || b)).");
+  }
+
+  /* The left operand.  */
+  const LhsT &lhs;
+};
+
+/* Starting point of the decomposition of the expression of a SELF_CHECK.
+
+   The general idea is:
+
+     - SELF_CHECK (a == b) expands to "decomposer () <= a == b"
+     - "decomposer () <= a" gets evaluated first, yields a lhs_holder
+     - "<lhs_holder> == b" gets evaluated, yields a binary_expr_holder
+     - The resulting binary_expr_holder is passed to the check function
+   */
+
+struct decomposer
+{
+  template<typename LhsT>
+  lhs_holder<LhsT> operator<= (const LhsT &lhs) const
+  {
+    return lhs_holder<LhsT> {lhs};
+  }
+};
+
+/* Return STR between QUOTE characters, with QUOTE, special and
+   non-printable characters escaped.  */
+
+std::string quote_string (std::string_view str, char quote = '"');
+
+/* Trait to detect types that can be iterated on with std::begin and
+   std::end.  */
+
+template<typename T, typename = void>
+struct is_range : std::false_type
+{};
+
+template<typename T>
+struct is_range<T,
+		std::void_t<decltype (std::begin (std::declval<const T &> ())),
+			    decltype (std::end (std::declval<const T &> ()))>>
+  : std::true_type
+{};
+
+template<typename T>
+constexpr bool is_range_v = is_range<T>::value;
+
+/* Trait to detect smart pointer types, that is types with a get method
+   returning a pointer.  */
+
+template<typename T, typename = void>
+struct is_smart_pointer : std::false_type
+{};
+
+template<typename T>
+using get_method_result_t = decltype (std::declval<const T &> ().get ());
+
+template<typename T>
+struct is_smart_pointer
+  <T, std::enable_if_t<std::is_pointer_v<get_method_result_t<T>>>>
+  : std::true_type
+{};
+
+template<typename T>
+constexpr bool is_smart_pointer_v = is_smart_pointer<T>::value;
+
+/* Trait to detect std::optional types.  */
+
+template<typename T>
+struct is_optional : std::false_type
+{};
+
+template<typename T>
+struct is_optional<std::optional<T>> : std::true_type
+{};
+
+template<typename T>
+constexpr bool is_optional_v = is_optional<T>::value;
+
+/* Trait to detect enum_flags types.  */
+
+template<typename T>
+struct is_enum_flags : std::false_type
+{};
+
+template<typename E>
+struct is_enum_flags<enum_flags<E>> : std::true_type
+{};
+
+template<typename T>
+constexpr bool is_enum_flags_v = is_enum_flags<T>::value;
+
+/* Trait to detect std::pair types.  */
+
+template<typename T>
+struct is_pair : std::false_type
+{};
+
+template<typename T1, typename T2>
+struct is_pair<std::pair<T1, T2>> : std::true_type
+{};
+
+template<typename T>
+constexpr bool is_pair_v = is_pair<T>::value;
+
+/* Trait to detect gdb::function_view types.  */
+
+template<typename T>
+struct is_function_view : std::false_type
+{};
+
+template<typename Sig>
+struct is_function_view<gdb::function_view<Sig>> : std::true_type
+{};
+
+template<typename T>
+constexpr bool is_function_view_v = is_function_view<T>::value;
+
+/* Trait to detect packed types.  */
+
+template<typename T>
+struct is_packed : std::false_type
+{};
+
+template<typename T, size_t Bytes>
+struct is_packed<packed<T, Bytes>> : std::true_type
+{
+  using wrapped_type = T;
+};
+
+template<typename T>
+constexpr bool is_packed_v = is_packed<T>::value;
+
+/* Trait to detect iterator types, which define an iterator_category
+   member type.  */
+
+template<typename T, typename = void>
+struct is_iterator : std::false_type
+{};
+
+template<typename T>
+struct is_iterator<T, std::void_t<typename T::iterator_category>>
+  : std::true_type
+{};
+
+template<typename T>
+constexpr bool is_iterator_v = is_iterator<T>::value;
+
+/* Trait to detect std::atomic types.  */
+
+template<typename T>
+struct is_atomic : std::false_type
+{};
+
+template<typename T>
+struct is_atomic<std::atomic<T>> : std::true_type
+{};
+
+template<typename T>
+constexpr bool is_atomic_v = is_atomic<T>::value;
+
+/* Trait to detect types with a to_string method.  */
+
+template<typename T, typename = void>
+struct has_to_string_method : std::false_type
+{};
+
+template<typename T>
+struct has_to_string_method<
+  T, std::void_t<decltype (std::declval<const T &> ().to_string ())>>
+  : std::true_type
+{};
+
+template<typename T>
+constexpr bool has_to_string_method_v = has_to_string_method<T>::value;
+
+/* Return a string representation of VAL, used to print the operands of
+   SELF_CHECKs.  Fails to compile if ValT is a type that it doesn't know
+   how to print.  */
+
+template<typename ValT>
+std::string
+value_to_string (const ValT &val)
+{
+  constexpr auto is_array = std::is_array_v<ValT>;
+  using element_type = std::remove_cv_t<std::remove_extent_t<ValT>>;
+  using pointee_type = std::remove_cv_t<std::remove_pointer_t<ValT>>;
+
+  if constexpr (is_array && std::is_same_v<element_type, char>)
+    {
+      /* Print fixed-size character arrays as strings.  The array is not
+	 necessarily null-terminated, don't read past its end.  */
+      constexpr auto extent = std::extent_v<ValT>;
+      return quote_string (std::string_view (val, strnlen (val, extent)));
+    }
+  else if constexpr (std::is_same_v<ValT, std::nullptr_t>)
+    {
+      /* This must come before the std::string_view case, because nullptr
+	 converts to std::string_view.  */
+      return "nullptr";
+    }
+  else if constexpr (std::is_pointer_v<ValT>
+		     && std::is_same_v<pointee_type, char>)
+    {
+      /* Char pointer, print as string.  This must come before the
+	 std::string_view case, because converting a null pointer to
+	 std::string_view is undefined behavior.  */
+      if (val == nullptr)
+	return "nullptr";
+
+      return quote_string (val);
+    }
+  else if constexpr (std::is_convertible_v<const ValT &, std::string_view>)
+    {
+      /* std::string and friends.  */
+      return quote_string (val);
+    }
+  else if constexpr (std::is_pointer_v<ValT>)
+    {
+      /* Any other pointer.  */
+      if (val == nullptr)
+	return "nullptr";
+
+      return host_address_to_string (val);
+    }
+  else if constexpr (std::is_same_v<ValT, bool>)
+    return val ? "true" : "false";
+  else if constexpr (std::is_same_v<ValT, char>)
+    return quote_string (std::string_view (&val, 1), '\'');
+  else if constexpr (std::is_integral_v<ValT>)
+    return std::to_string (val);
+  else if constexpr (std::is_enum_v<ValT>)
+    {
+      /* Print the numerical value.  Don't go through value_to_string, the
+	 underlying type could be char, and that would print it as a quoted
+	 character.  */
+      return std::to_string (static_cast<std::underlying_type_t<ValT>> (val));
+    }
+  else if constexpr (is_function_view_v<ValT>)
+    {
+      /* There is nothing interesting to print about the callable.  */
+      if (val == nullptr)
+	return "nullptr";
+
+      return "{function_view}";
+    }
+  else if constexpr (is_atomic_v<ValT>)
+    return value_to_string (val.load ());
+  else if constexpr (is_packed_v<ValT>)
+    {
+      using wrapped_type = typename is_packed<ValT>::wrapped_type;
+
+      return value_to_string (static_cast<wrapped_type> (val));
+    }
+  else if constexpr (is_pair_v<ValT>)
+    return ("{" + value_to_string (val.first) + ", "
+	    + value_to_string (val.second) + "}");
+  else if constexpr (is_enum_flags_v<ValT>)
+    {
+      /* These are bit flags, so hex is more readable.  */
+      auto raw = static_cast<typename ValT::underlying_type> (val);
+
+      return string_printf ("0x%llx", static_cast<unsigned long long> (raw));
+    }
+  else if constexpr (has_to_string_method_v<ValT>)
+    {
+      auto str = val.to_string ();
+
+      /* Some to_string methods return a const char *, print it like
+	 any other value of that type.  */
+      if constexpr (std::is_same_v<decltype (str), std::string>)
+	return str;
+      else
+	return value_to_string (str);
+    }
+  else if constexpr (is_optional_v<ValT>)
+    {
+      if (!val.has_value ())
+	return "std::nullopt";
+
+      return value_to_string (*val);
+    }
+  else if constexpr (is_smart_pointer_v<ValT>)
+    return value_to_string (val.get ());
+  else if constexpr (is_range_v<ValT>)
+    {
+      /* Don't print too many elements, to keep the message readable.  */
+      constexpr std::size_t max_elements = 32;
+      std::string result = "{";
+
+      for (auto [i, elem] : gdb::ranges::views::enumerate (val))
+	{
+	  if (i > 0)
+	    result += ", ";
+
+	  if (i == max_elements)
+	    {
+	      result += "...";
+	      break;
+	    }
+
+	  result += value_to_string (elem);
+	}
+
+      result += "}";
+      return result;
+    }
+  else if constexpr (is_iterator_v<ValT>)
+    {
+      /* Iterators can't be printed meaningfully, and dereferencing them is
+	 not always valid.  */
+      return "{iterator}";
+    }
+  else
+    {
+      static_assert (dependent_false_v<ValT>,
+		     "value_to_string: don't know how to print this type, add "
+		     "a to_string method to it or a case in value_to_string");
+      return {};
+    }
+}
+
+/* Report a passed (if running verbose) or failed selftest, based on the
+   result in EXPR.  This function handles the binary expressions like:
+
+     SELF_CHECK (a == b)  */
+
+template<typename LhsT, typename RhsT>
+void
+check (const binary_expr_holder<LhsT, RhsT> &expr, const char *file, int line,
+       const char *expression)
+{
+  auto expands_to = [&] ()
+    {
+      return string_printf ("%s %s %s",
+			    value_to_string (expr.lhs).c_str (), expr.op,
+			    value_to_string (expr.rhs).c_str ());
+    };
+
+  if (!expr.result)
+    failed_selftest (file, line, expression, expands_to ().c_str ());
+  else if (run_verbose ())
+    passed_selftest (file, line, expression, expands_to ().c_str ());
+}
+
+/* Report a passed (if running verbose) or failed selftest, based on the
+   result in EXPR.  This function handles the unary expressions like:
+
+     SELF_CHECK (some_bool_expression)
+
+   This function could be templated and accept anything that converts to bool,
+   but our coding style is to use explicit comparison, so we might as well
+   enforce that.  */
+
+inline void
+check (const lhs_holder<bool> &expr, const char *file, int line,
+       const char *expression)
+{
+  if (!expr.lhs)
+    failed_selftest (file, line, expression, nullptr);
+  else if (run_verbose ())
+    passed_selftest (file, line, expression, nullptr);
+}
+
+/* Reject non-bool values, like:
+
+     SELF_CHECK (ptr)
+
+   See the bool overload above.  The point of this function is to give a clear
+   error message.  */
+
+template<typename LhsT>
+void
+check (const lhs_holder<LhsT> &, const char *, int, const char *)
+{
+  static_assert (dependent_false_v<LhsT>,
+		 "SELF_CHECK on a non-bool value: use an explicit comparison, "
+		 "e.g. SELF_CHECK (ptr != nullptr).");
+}
+
+} /* namespace detail */
 }
 
 /* Check that VALUE is true, and, if not, throw an exception.  */
 
-#define SELF_CHECK(VALUE)						\
-  do									\
-    {									\
-      if (!(VALUE))							\
-	selftests::failed_selftest (__FILE__, __LINE__, #VALUE);	\
-    }									\
+#define SELF_CHECK(VALUE)                                                  \
+  do                                                                       \
+    {                                                                      \
+      DIAGNOSTIC_PUSH                                                      \
+      DIAGNOSTIC_IGNORE_PARENTHESES                                        \
+      selftests::detail::check (selftests::detail::decomposer () <= VALUE, \
+				__FILE__, __LINE__, #VALUE);               \
+      DIAGNOSTIC_POP                                                       \
+    }                                                                      \
   while (0)
 
 #endif /* GDBSUPPORT_SELFTEST_H */
-- 
2.56.0


  parent reply	other threads:[~2026-10-19 17:01 UTC|newest]

Thread overview: 9+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-19 16:58 [PATCH 0/6] Make SELF_CHECK print operands of comparisons Simon Marchi
2026-10-19 16:58 ` [PATCH 1/6] gdb/unittests: catch gdb_exception_error in scoped_mmap test Simon Marchi
2026-10-19 16:58 ` [PATCH 2/6] gdbsupport/selftest: print expression when selftest fails and re-format Simon Marchi
2026-10-19 16:58 ` [PATCH 3/6] include/diagnostics.h: add DIAGNOSTIC_IGNORE_PARENTHESES Simon Marchi
2026-10-19 16:58 ` [PATCH 4/6] gdb/gmp-utils: rename gdb_mp{z,q,f}::str to to_string Simon Marchi
2026-10-19 16:58 ` Simon Marchi [this message]
2026-10-27 14:13   ` [PATCH 5/6] gdbsupport/selftest: print operand values when a SELF_CHECK fails Tom Tromey
2026-10-27 17:06     ` Simon Marchi
2026-10-19 16:58 ` [PATCH 6/6] gdb: remove manual printing of values in some selftests Simon Marchi

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261019165929.792697-6-simon.marchi@efficios.com \
    --to=simon.marchi@efficios.com \
    --cc=gcc-patches@sourceware.org \
    --cc=gdb-patches@sourceware.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox