[v2] c: Implement C23 nullptr (N3042)

Message ID YwZswN8f619HrP6p@redhat.com
State New, archived
Headers
Series [v2] c: Implement C23 nullptr (N3042) |

Commit Message

Marek Polacek Aug. 24, 2022, 6:24 p.m. UTC
  On Mon, Aug 15, 2022 at 05:48:34PM +0000, Joseph Myers wrote:
> On Sat, 13 Aug 2022, Marek Polacek via Gcc-patches wrote:
> 
> > This patch also defines nullptr_t in <stddef.h>.  I'm uncertain about
> > the __STDC_VERSION__ version I should be checking.  Also, I'm not
> 
> We're using defined (__STDC_VERSION__) && __STDC_VERSION__ > 201710L until 
> the final version for C23 is settled.
 
OK, adjusted.

> > defining __STDC_VERSION_STDDEF_H__ yet, because I don't know what value
> > it should be defined to.  Do we know yet?
> 
> No, and Jens's comments on the editorial review before CD ballot include 
> that lots of headers don't yet have such a macro definition but should 
> have one, as well as needing consistency for the numbers.
> 
> We won't know the final values for these macros until much later, because 
> the timescale depends on whether ISO decides to delay things at any point 
> by coming up with a long list of editorial issues required to follow the 
> JTC1 Directives as they did for C17 (objections to particular words 
> appearing in non-normative text, etc.).
 
Ack, thanks.

> > -  { "nullptr",		RID_NULLPTR,	D_CXXONLY | D_CXX11 | D_CXXWARN },
> > +  { "nullptr",		RID_NULLPTR,	D_CXX11 | D_CXXWARN },
> 
> You need to use D_C2X (which doesn't yet exist).  In pre-C23 modes, 
> nullptr needs to be a normal identifier that can be used in all contexts 
> where identifiers can be used, not a keyword at all (and then you need a 
> c11-nullptr*.c test to verify that use of it as an identifier works as 
> expected).

Fixed.  Adding D_C2X meant that I had to enlarge struct c_common_resword
by a word.
 
> > diff --git a/gcc/c/c-convert.cc b/gcc/c/c-convert.cc
> > index 18083d59618..013fe6b2a53 100644
> > --- a/gcc/c/c-convert.cc
> > +++ b/gcc/c/c-convert.cc
> > @@ -133,6 +133,14 @@ c_convert (tree type, tree expr, bool init_const)
> >  	(loc, type, c_objc_common_truthvalue_conversion (input_location, expr));
> >  
> >      case POINTER_TYPE:
> > +      /* The type nullptr_t may be converted to a pointer type.  The result is
> > +	 a null pointer value.  */
> > +      if (NULLPTR_TYPE_P (TREE_TYPE (e)))
> > +	{
> > +	  ret = build_int_cst (type, 0);
> > +	  goto maybe_fold;
> > +	}
> 
> That looks like it would lose side-effects.  You need to preserve 
> side-effects in an expression of nullptr_t type being converted to a 
> pointer type, and need an execution testcase that verifies such 
> side-effects are preserved.

Ah, of course.  Fixed by building up a COMPOUND_EXPR.  Covered by
c2x-nullptr-5.c.
 
> Also, you need to make sure that (void *)nullptr is not treated as a null 
> pointer constant, only a null pointer; build_int_cst (type, 0) would 
> produce a null pointer constant when type is void *.  (void *)nullptr 
> should be handled similarly to (void *)(void *)0, which isn't a null 
> pointer constant either.

That (void *)nullptr is not considered an NPC ought to be achieved by
build_c_cast wrapping the result of c_convert in a NOP_EXPR:

  /* Don't allow the results of casting to floating-point or complex
     types be confused with actual constants, or casts involving
     integer and pointer types other than direct integer-to-integer
     and integer-to-pointer be confused with integer constant
     expressions and null pointer constants.  */

Then null_pointer_constant_p sees the NOP_EXPR and returns false.  I've
added a comment explaining that.

> > @@ -133,6 +133,13 @@ null_pointer_constant_p (const_tree expr)
> >    /* This should really operate on c_expr structures, but they aren't
> >       yet available everywhere required.  */
> >    tree type = TREE_TYPE (expr);
> > +
> > +  /* An integer constant expression with the value 0, such an expression
> > +     cast to type void*, or the predefined constant nullptr, are a null
> > +     pointer constant.  */
> > +  if (NULLPTR_TYPE_P (type))
> > +    return true;
> 
> That looks wrong.  You need to distinguish null pointer constants of type 
> nullptr_t (nullptr, possibly enclosed in parentheses, possibly the 
> selected alternative from _Generic) from all other expressions of type 
> nullptr_t (including (nullptr_t)nullptr, which isn't a null pointer 
> constant any more than (void *)(void *)0).

Ah, okay.  I had just copied what we do in C++ in null_ptr_cst_p and the
rest of the patch worked under that assumption.  I've added some tests
for this too.  Except I don't really understand the _Generic comment so
I only have tests for _Generic that were in the previous version.

Changing null_pointer_constant_p mean that I had to adjust
build_binary_op/EQ_EXPR.
 
> Then, for each context where it matters whether a nullptr_t value is a 
> null pointer constant, there need to be testcases that the two cases are 
> properly distinguished.  This includes at least equality comparisons with 
> a pointer that is not a null pointer constant (seem only to be allowed 
> with nullptr, not with other nullptr_t expressions).  (I think for 
> conditional expressions, conditionals between nullptr_t and an integer 
> null pointer constant are always invalid, whether or not the nullptr_t is 
> a null pointer constant, while conditionals between nullptr_t and a 
> pointer are always valid.)

I see, it's because 6.5.9 says that if one of the operands of == or !=
has type nullptr_t but the other one doesn't, the other operand has to
be an NPC, not just a null pointer.

This should be tested in c2x-nullptr-1.c:test2 and c2x-nullptr-3.c:test1.
For instance, "p == (nullptr_t)nullptr", where p is a pointer, does not
compile.
 
> > +/* The type nullptr_t shall not be converted to any type other than bool or
> > +   a pointer type.  No type other than nullptr_t shall be converted to nullptr_t.  */
> 
> That's other than *void*, bool or a pointer type.  (That's a correct fix 
> to the N3042 wording in N3047.  There are other problems in the 
> integration of nullptr in N3047 that are only fixed in my subsequent fixes 
> as part of the editorial review - and many issues with integration of 
> other papers that haven't yet been fixed, I currently have 25 open merge 
> requests resulting from editorial review.)  And of course conversions from 
> nullptr_t to void should be tested.

Thanks, tests added to c2x-nullptr-1.c:test1.  I notice that 6.3.2.4 still
says "The type nullptr_t may be converted to bool or to a pointer type";
isn't it missing the ", void" here too?
 
> > diff --git a/gcc/testsuite/gcc.dg/c2x-nullptr-4.c b/gcc/testsuite/gcc.dg/c2x-nullptr-4.c
> > new file mode 100644
> > index 00000000000..5b15e75d159
> > --- /dev/null
> > +++ b/gcc/testsuite/gcc.dg/c2x-nullptr-4.c
> > @@ -0,0 +1,10 @@
> > +/* Test that we warn about `nullptr' pre-C2X.  */
> > +/* { dg-do compile } */
> > +/* { dg-options "-std=c17 -pedantic-errors" } */
> 
> This test is wrong - it's a normal identifier pre-C2x - but tests for 
> previous standard versions shouldn't be called c2x-* in any case.

I see.  I've removed this test and instead added c17-nullptr-2.c.

Thank you for the careful review.

Bootstrap/regtest running on x86_64-pc-linux-gnu, ok for trunk?

-- >8 --
This patch implements the C23 nullptr literal:
<https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3042.htm> (with
wording fixes from N3047), which is intended to replace the problematic
definition of NULL which might be either of integer type or void*.

Since C++ has had nullptr for over a decade now, it was relatively easy
to just move the built-in node definitions from the C++ FE to the C/C++
common code.  Also, our DWARF emitter already handles NULLPTR_TYPE by
emitting DW_TAG_unspecified_type.  However, I had to handle a lot of
contexts such as ?:, comparison, conversion, etc.

There are some minor differences, e.g. in C you can do

  bool b = nullptr;

but in C++ you have to use direct-initialization:

  bool b{nullptr};

And I think that

  nullptr_t n = 0;

is only valid in C++.

Of course, C doesn't have to handle mangling, RTTI, substitution,
overloading, ...

This patch also defines nullptr_t in <stddef.h>.  However, it does not
define __STDC_VERSION_STDDEF_H__ yet, because we don't know yet what value
it should be defined to.

gcc/c-family/ChangeLog:

	* c-common.cc (c_common_reswords): Enable nullptr in C2X.
	(c_common_nodes_and_builtins): Create the built-in node for nullptr.
	* c-common.h (enum c_tree_index): Add CTI_NULLPTR, CTI_NULLPTR_TYPE.
	(struct c_common_resword): Resize the disable member.
	(D_C2X): Add.
	(nullptr_node): Define.
	(nullptr_type_node): Define.
	(NULLPTR_TYPE_P): Define.
	* c-pretty-print.cc (c_pretty_printer::simple_type_specifier): Handle
	NULLPTR_TYPE.
	(c_pretty_printer::direct_abstract_declarator): Likewise.
	(c_pretty_printer::constant): Likewise.

gcc/c/ChangeLog:

	* c-convert.cc (c_convert) <case POINTER_TYPE>: Handle NULLPTR_TYPE.
	Give a better diagnostic when converting to nullptr_t.
	* c-decl.cc (c_init_decl_processing): Perform C-specific nullptr
	initialization.
	* c-parser.cc (c_parse_init): Maybe OR D_C2X into mask.
	(c_parser_postfix_expression): Handle RID_NULLPTR.
	* c-typeck.cc (null_pointer_constant_p): Return true when expr is
	nullptr_node.
	(build_unary_op) <case TRUTH_NOT_EXPR>: Handle NULLPTR_TYPE.
	(build_conditional_expr): Handle the case when the second/third operand
	is NULLPTR_TYPE and third/second operand is POINTER_TYPE.
	(convert_for_assignment): Handle converting an expression of type
	nullptr_t to pointer/bool.
	(build_binary_op) <case TRUTH_XOR_EXPR>: Handle NULLPTR_TYPE.
	<case EQ_EXPR>: Handle comparing operands of type nullptr_t.

gcc/cp/ChangeLog:

	* cp-tree.h (enum cp_tree_index): Remove CTI_NULLPTR, CTI_NULLPTR_TYPE.
	Move it to c_tree_index.
	(nullptr_node): No longer define here.
	(nullptr_type_node): Likewise.
	(NULLPTR_TYPE_P): Likewise.
	* decl.cc (cxx_init_decl_processing): Only keep C++-specific nullptr
	initialization; move the shared code to c_common_nodes_and_builtins.

gcc/ChangeLog:

	* ginclude/stddef.h: Define nullptr_t.

gcc/testsuite/ChangeLog:

	* gcc.dg/c11-nullptr-1.c: New test.
	* gcc.dg/c17-nullptr-1.c: New test.
	* gcc.dg/c17-nullptr-2.c: New test.
	* gcc.dg/c2x-nullptr-1.c: New test.
	* gcc.dg/c2x-nullptr-2.c: New test.
	* gcc.dg/c2x-nullptr-3.c: New test.
	* gcc.dg/c2x-nullptr-4.c: New test.
	* gcc.dg/c2x-nullptr-5.c: New test.
---
 gcc/c-family/c-common.cc             |  20 ++-
 gcc/c-family/c-common.h              |  37 ++--
 gcc/c-family/c-pretty-print.cc       |   7 +
 gcc/c/c-convert.cc                   |  25 ++-
 gcc/c/c-decl.cc                      |   6 +
 gcc/c/c-parser.cc                    |  10 ++
 gcc/c/c-typeck.cc                    |  57 +++++-
 gcc/cp/cp-tree.h                     |   8 -
 gcc/cp/decl.cc                       |   8 +-
 gcc/ginclude/stddef.h                |   8 +
 gcc/testsuite/gcc.dg/c11-nullptr-1.c |  10 ++
 gcc/testsuite/gcc.dg/c17-nullptr-1.c |  10 ++
 gcc/testsuite/gcc.dg/c17-nullptr-2.c |  10 ++
 gcc/testsuite/gcc.dg/c2x-nullptr-1.c | 259 +++++++++++++++++++++++++++
 gcc/testsuite/gcc.dg/c2x-nullptr-2.c |   9 +
 gcc/testsuite/gcc.dg/c2x-nullptr-3.c |  78 ++++++++
 gcc/testsuite/gcc.dg/c2x-nullptr-4.c |  11 ++
 gcc/testsuite/gcc.dg/c2x-nullptr-5.c |  14 ++
 18 files changed, 545 insertions(+), 42 deletions(-)
 create mode 100644 gcc/testsuite/gcc.dg/c11-nullptr-1.c
 create mode 100644 gcc/testsuite/gcc.dg/c17-nullptr-1.c
 create mode 100644 gcc/testsuite/gcc.dg/c17-nullptr-2.c
 create mode 100644 gcc/testsuite/gcc.dg/c2x-nullptr-1.c
 create mode 100644 gcc/testsuite/gcc.dg/c2x-nullptr-2.c
 create mode 100644 gcc/testsuite/gcc.dg/c2x-nullptr-3.c
 create mode 100644 gcc/testsuite/gcc.dg/c2x-nullptr-4.c
 create mode 100644 gcc/testsuite/gcc.dg/c2x-nullptr-5.c


base-commit: 530f80451a9e76896a0294e0f4bd59baff1ac27f
  

Comments

Joseph Myers Aug. 25, 2022, 5:28 p.m. UTC | #1
On Wed, 24 Aug 2022, Marek Polacek via Gcc-patches wrote:

> Ah, okay.  I had just copied what we do in C++ in null_ptr_cst_p and the
> rest of the patch worked under that assumption.  I've added some tests
> for this too.  Except I don't really understand the _Generic comment so
> I only have tests for _Generic that were in the previous version.

The point is that e.g.

_Generic(0, int : nullptr)

is treated the same as nullptr (so is a null pointer constant), just as 
(nullptr) is.

> Thanks, tests added to c2x-nullptr-1.c:test1.  I notice that 6.3.2.4 still
> says "The type nullptr_t may be converted to bool or to a pointer type";
> isn't it missing the ", void" here too?

In general none of the subclauses under 6.3.2 about individual kinds of 
types tend to discuss the possibility of conversion to void.

> +/* Simple assignment.  */
> +void
> +test4 (void)
> +{
> +  /* -- the left operand has an atomic, qualified, or unqualified version of
> +     the nullptr_t type and the type of the right is nullptr_t;  */
> +  nullptr_t n1;
> +  n1 = nullptr;
> +  const nullptr_t n2 = nullptr;
> +  _Atomic nullptr_t n3 = nullptr;
> +  volatile nullptr_t n4 = nullptr;

These qualified cases are all actually initialization, not assignment; I 
think both assignment and initialization (and argument passing and return) 
should be tested for the permitted cases for assignment.

> +/* Test nullptr_t from <stddef.h..  */

Typo, "<stddef.h." should be <stddef.h>".

> +/* If a second or third operand of type nullptr_t is used that is not a null
> +   pointer constant and the other operand is not a pointer or does not have
> +   itself nullptr_t, a constraint is violated even if that other operand is
> +   a null pointer constant such as 0.  */

The "that is not a null pointer constant" in that footnote is a bit odd, 
since it's also a constraint violation (and should be tested as such) to 
have a conditional expression between e.g. nullptr and 0.
  

Patch

diff --git a/gcc/c-family/c-common.cc b/gcc/c-family/c-common.cc
index 6e41ceb38e9..82ebe7c4502 100644
--- a/gcc/c-family/c-common.cc
+++ b/gcc/c-family/c-common.cc
@@ -324,8 +324,10 @@  static bool nonnull_check_p (tree, unsigned HOST_WIDE_INT);
    if they match the mask.
 
    Masks for languages:
-   C --std=c89: D_C99 | D_CXXONLY | D_OBJC | D_CXX_OBJC
-   C --std=c99: D_CXXONLY | D_OBJC
+   C --std=c89: D_C99 | D_C2X | D_CXXONLY | D_OBJC | D_CXX_OBJC
+   C --std=c99: D_C2X | D_CXXONLY | D_OBJC
+   C --std=c17: D_C2X | D_CXXONLY | D_OBJC
+   C --std=c2x: D_CXXONLY | D_OBJC
    ObjC is like C except that D_OBJC and D_CXX_OBJC are not set
    C++ --std=c++98: D_CONLY | D_CXX11 | D_CXX20 | D_OBJC
    C++ --std=c++11: D_CONLY | D_CXX20 | D_OBJC
@@ -500,7 +502,7 @@  const struct c_common_resword c_common_reswords[] =
   { "namespace",	RID_NAMESPACE,	D_CXXONLY | D_CXXWARN },
   { "new",		RID_NEW,	D_CXXONLY | D_CXXWARN },
   { "noexcept",		RID_NOEXCEPT,	D_CXXONLY | D_CXX11 | D_CXXWARN },
-  { "nullptr",		RID_NULLPTR,	D_CXXONLY | D_CXX11 | D_CXXWARN },
+  { "nullptr",		RID_NULLPTR,	D_C2X | D_CXX11 | D_CXXWARN },
   { "operator",		RID_OPERATOR,	D_CXXONLY | D_CXXWARN },
   { "private",		RID_PRIVATE,	D_CXX_OBJC | D_CXXWARN },
   { "protected",	RID_PROTECTED,	D_CXX_OBJC | D_CXXWARN },
@@ -4723,6 +4725,18 @@  c_common_nodes_and_builtins (void)
   null_node = make_int_cst (1, 1);
   TREE_TYPE (null_node) = c_common_type_for_size (POINTER_SIZE, 0);
 
+  /* Create the built-in nullptr node.  This part of its initialization is
+     common to C and C++.  The front ends can further adjust its definition
+     in {c,cxx}_init_decl_processing.  In particular, we aren't setting the
+     alignment here for C++ backward ABI bug compatibility.  */
+  nullptr_type_node = make_node (NULLPTR_TYPE);
+  TYPE_SIZE (nullptr_type_node) = bitsize_int (GET_MODE_BITSIZE (ptr_mode));
+  TYPE_SIZE_UNIT (nullptr_type_node) = size_int (GET_MODE_SIZE (ptr_mode));
+  TYPE_UNSIGNED (nullptr_type_node) = 1;
+  TYPE_PRECISION (nullptr_type_node) = GET_MODE_BITSIZE (ptr_mode);
+  SET_TYPE_MODE (nullptr_type_node, ptr_mode);
+  nullptr_node = build_int_cst (nullptr_type_node, 0);
+
   /* Since builtin_types isn't gc'ed, don't export these nodes.  */
   memset (builtin_types, 0, sizeof (builtin_types));
 }
diff --git a/gcc/c-family/c-common.h b/gcc/c-family/c-common.h
index c06769b6f0b..e7b0fd1309d 100644
--- a/gcc/c-family/c-common.h
+++ b/gcc/c-family/c-common.h
@@ -375,6 +375,8 @@  enum c_tree_index
     CTI_DEFAULT_FUNCTION_TYPE,
 
     CTI_NULL,
+    CTI_NULLPTR,
+    CTI_NULLPTR_TYPE,
 
     /* These are not types, but we have to look them up all the time.  */
     CTI_FUNCTION_NAME_DECL,
@@ -409,7 +411,7 @@  struct c_common_resword
 {
   const char *const word;
   ENUM_BITFIELD(rid) const rid : 16;
-  const unsigned int disable   : 16;
+  const unsigned int disable   : 32;
 };
 
 /* Mode used to build pointers (VOIDmode means ptr_mode).  */
@@ -447,19 +449,20 @@  extern machine_mode c_default_pointer_mode;
 #define D_CONLY		0x0001	/* C only (not in C++).  */
 #define D_CXXONLY	0x0002	/* C++ only (not in C).  */
 #define D_C99		0x0004	/* In C, C99 only.  */
-#define D_CXX11         0x0008	/* In C++, C++11 only.  */
-#define D_EXT		0x0010	/* GCC extension.  */
-#define D_EXT89		0x0020	/* GCC extension incorporated in C99.  */
-#define D_ASM		0x0040	/* Disabled by -fno-asm.  */
-#define D_OBJC		0x0080	/* In Objective C and neither C nor C++.  */
-#define D_CXX_OBJC	0x0100	/* In Objective C, and C++, but not C.  */
-#define D_CXXWARN	0x0200	/* In C warn with -Wcxx-compat.  */
-#define D_CXX_CONCEPTS  0x0400	/* In C++, only with concepts.  */
-#define D_TRANSMEM	0X0800	/* C++ transactional memory TS.  */
-#define D_CXX_CHAR8_T	0X1000	/* In C++, only with -fchar8_t.  */
-#define D_CXX20		0x2000  /* In C++, C++20 only.  */
-#define D_CXX_COROUTINES 0x4000  /* In C++, only with coroutines.  */
-#define D_CXX_MODULES	0x8000  /* In C++, only with modules.  */
+#define D_C2X		0x0008	/* In C, C2X only.  */
+#define D_CXX11         0x0010	/* In C++, C++11 only.  */
+#define D_EXT		0x0020	/* GCC extension.  */
+#define D_EXT89		0x0040	/* GCC extension incorporated in C99.  */
+#define D_ASM		0x0080	/* Disabled by -fno-asm.  */
+#define D_OBJC		0x0100	/* In Objective C and neither C nor C++.  */
+#define D_CXX_OBJC	0x0200	/* In Objective C, and C++, but not C.  */
+#define D_CXXWARN	0x0400	/* In C warn with -Wcxx-compat.  */
+#define D_CXX_CONCEPTS  0x0800	/* In C++, only with concepts.  */
+#define D_TRANSMEM	0x1000	/* C++ transactional memory TS.  */
+#define D_CXX_CHAR8_T	0x2000	/* In C++, only with -fchar8_t.  */
+#define D_CXX20		0x4000  /* In C++, C++20 only.  */
+#define D_CXX_COROUTINES 0x8000  /* In C++, only with coroutines.  */
+#define D_CXX_MODULES	0x10000  /* In C++, only with modules.  */
 
 #define D_CXX_CONCEPTS_FLAGS D_CXXONLY | D_CXX_CONCEPTS
 #define D_CXX_CHAR8_T_FLAGS D_CXXONLY | D_CXX_CHAR8_T
@@ -534,6 +537,9 @@  extern const unsigned int num_c_common_reswords;
 
 /* The node for C++ `__null'.  */
 #define null_node                       c_global_trees[CTI_NULL]
+/* The nodes for `nullptr'.  */
+#define nullptr_node                    c_global_trees[CTI_NULLPTR]
+#define nullptr_type_node               c_global_trees[CTI_NULLPTR_TYPE]
 
 extern GTY(()) tree c_global_trees[CTI_MAX];
 
@@ -1009,6 +1015,9 @@  extern void c_parse_final_cleanups (void);
 #define DECL_UNNAMED_BIT_FIELD(NODE) \
   (DECL_C_BIT_FIELD (NODE) && !DECL_NAME (NODE))
 
+/* True iff TYPE is cv decltype(nullptr).  */
+#define NULLPTR_TYPE_P(TYPE) (TREE_CODE (TYPE) == NULLPTR_TYPE)
+
 extern tree do_case (location_t, tree, tree);
 extern tree build_stmt (location_t, enum tree_code, ...);
 extern tree build_real_imag_expr (location_t, enum tree_code, tree);
diff --git a/gcc/c-family/c-pretty-print.cc b/gcc/c-family/c-pretty-print.cc
index 71a0cb51093..efa1768f4d6 100644
--- a/gcc/c-family/c-pretty-print.cc
+++ b/gcc/c-family/c-pretty-print.cc
@@ -321,6 +321,7 @@  pp_c_pointer (c_pretty_printer *pp, tree t)
       _Bool                          -- C99
       _Complex                       -- C99
       _Imaginary                     -- C99
+      nullptr_t                      -- C23
       struct-or-union-specifier
       enum-specifier
       typedef-name.
@@ -424,6 +425,9 @@  c_pretty_printer::simple_type_specifier (tree t)
       else
 	translate_string ("<anonymous>");
       break;
+    case NULLPTR_TYPE:
+      pp_c_ws_string (this, "nullptr_t");
+      break;
 
     default:
       pp_unsupported_tree (this, t);
@@ -678,6 +682,7 @@  c_pretty_printer::direct_abstract_declarator (tree t)
     case COMPLEX_TYPE:
     case TYPE_DECL:
     case ERROR_MARK:
+    case NULLPTR_TYPE:
       break;
 
     default:
@@ -1219,6 +1224,8 @@  c_pretty_printer::constant (tree e)
 	  pp_c_character_constant (this, e);
 	else if (TREE_CODE (type) == ENUMERAL_TYPE)
 	  pp_c_enumeration_constant (this, e);
+	else if (NULLPTR_TYPE_P (type))
+	  pp_string (this, "nullptr");
 	else
 	  pp_c_integer_constant (this, e);
       }
diff --git a/gcc/c/c-convert.cc b/gcc/c/c-convert.cc
index 18083d59618..6e7491339d4 100644
--- a/gcc/c/c-convert.cc
+++ b/gcc/c/c-convert.cc
@@ -133,6 +133,20 @@  c_convert (tree type, tree expr, bool init_const)
 	(loc, type, c_objc_common_truthvalue_conversion (input_location, expr));
 
     case POINTER_TYPE:
+      /* The type nullptr_t may be converted to a pointer type.  The result is
+	 a null pointer value.  */
+      if (NULLPTR_TYPE_P (TREE_TYPE (e)))
+	{
+	  /* To make sure that (void *)nullptr is not a null pointer constant,
+	     build_c_cast will create an additional NOP_EXPR around the result
+	     of this conversion.  */
+	  if (TREE_SIDE_EFFECTS (e))
+	    ret = build2 (COMPOUND_EXPR, type, e, build_int_cst (type, 0));
+	  else
+	    ret = build_int_cst (type, 0);
+	  goto maybe_fold;
+	}
+      gcc_fallthrough ();
     case REFERENCE_TYPE:
       ret = convert_to_pointer (type, e);
       goto maybe_fold;
@@ -180,7 +194,16 @@  c_convert (tree type, tree expr, bool init_const)
       return ret;
     }
 
-  error ("conversion to non-scalar type requested");
+  /* If we are converting to nullptr_t, don't say "non-scalar type" because
+     the nullptr_t type is a scalar type.  Only nullptr_t shall be converted
+     to nullptr_t.  */
+  if (code == NULLPTR_TYPE)
+    {
+      error ("conversion from %qT to %qT", TREE_TYPE (e), type);
+      inform (input_location, "only %qT can be converted to %qT", type, type);
+    }
+  else
+    error ("conversion to non-scalar type requested");
   return error_mark_node;
 }
 
diff --git a/gcc/c/c-decl.cc b/gcc/c/c-decl.cc
index 9e590c66dae..cbba0c62f64 100644
--- a/gcc/c/c-decl.cc
+++ b/gcc/c/c-decl.cc
@@ -4531,6 +4531,12 @@  c_init_decl_processing (void)
   pushdecl (build_decl (UNKNOWN_LOCATION, TYPE_DECL, get_identifier ("_Bool"),
 			boolean_type_node));
 
+  /* C-specific nullptr initialization.  */
+  record_builtin_type (RID_MAX, "nullptr_t", nullptr_type_node);
+  /* The size and alignment of nullptr_t is the same as for a pointer to
+     character type.  */
+  SET_TYPE_ALIGN (nullptr_type_node, GET_MODE_ALIGNMENT (ptr_mode));
+
   input_location = save_loc;
 
   make_fname_decl = c_make_fname_decl;
diff --git a/gcc/c/c-parser.cc b/gcc/c/c-parser.cc
index 759f200a7eb..8520fd0e2a8 100644
--- a/gcc/c/c-parser.cc
+++ b/gcc/c/c-parser.cc
@@ -119,6 +119,8 @@  c_parse_init (void)
   mask |= D_CXXONLY;
   if (!flag_isoc99)
     mask |= D_C99;
+  if (!flag_isoc2x)
+    mask |= D_C2X;
   if (flag_no_asm)
     {
       mask |= D_ASM | D_EXT;
@@ -10243,6 +10245,14 @@  c_parser_postfix_expression (c_parser *parser)
 			 "%<depend%> clause");
 	  expr.set_error ();
 	  break;
+	/* C23 'nullptr' literal.  */
+	case RID_NULLPTR:
+	  c_parser_consume_token (parser);
+	  expr.value = nullptr_node;
+	  set_c_expr_source_range (&expr, tok_range);
+	  pedwarn_c11 (loc, OPT_Wpedantic,
+		       "ISO C does not support %qs before C2X", "nullptr");
+	  break;
 	default:
 	  c_parser_error (parser, "expected expression");
 	  expr.set_error ();
diff --git a/gcc/c/c-typeck.cc b/gcc/c/c-typeck.cc
index de8780a1502..3b5275b6e0a 100644
--- a/gcc/c/c-typeck.cc
+++ b/gcc/c/c-typeck.cc
@@ -133,6 +133,13 @@  null_pointer_constant_p (const_tree expr)
   /* This should really operate on c_expr structures, but they aren't
      yet available everywhere required.  */
   tree type = TREE_TYPE (expr);
+
+  /* An integer constant expression with the value 0, such an expression
+     cast to type void*, or the predefined constant nullptr, are a null
+     pointer constant.  */
+  if (expr == nullptr_node)
+    return true;
+
   return (TREE_CODE (expr) == INTEGER_CST
 	  && !TREE_OVERFLOW (expr)
 	  && integer_zerop (expr)
@@ -4575,7 +4582,7 @@  build_unary_op (location_t location, enum tree_code code, tree xarg,
     case TRUTH_NOT_EXPR:
       if (typecode != INTEGER_TYPE && typecode != FIXED_POINT_TYPE
 	  && typecode != REAL_TYPE && typecode != POINTER_TYPE
-	  && typecode != COMPLEX_TYPE)
+	  && typecode != COMPLEX_TYPE && typecode != NULLPTR_TYPE)
 	{
 	  error_at (location,
 		    "wrong type argument to unary exclamation mark");
@@ -5515,6 +5522,13 @@  build_conditional_expr (location_t colon_loc, tree ifexp, bool ifexp_bcp,
 	}
       result_type = type2;
     }
+  /* 6.5.15: "if one is a null pointer constant (other than a pointer) or has
+     type nullptr_t and the other is a pointer, the result type is the pointer
+     type."  */
+  else if (code1 == NULLPTR_TYPE && code2 == POINTER_TYPE)
+    result_type = type2;
+  else if (code1 == POINTER_TYPE && code2 == NULLPTR_TYPE)
+    result_type = type1;
 
   if (!result_type)
     {
@@ -7613,12 +7627,13 @@  convert_for_assignment (location_t location, location_t expr_loc, tree type,
 	error_at (location, msg);
       return error_mark_node;
     }
-  else if (codel == POINTER_TYPE && coder == INTEGER_TYPE)
+  else if (codel == POINTER_TYPE
+	   && (coder == INTEGER_TYPE || coder == NULLPTR_TYPE))
     {
-      /* An explicit constant 0 can convert to a pointer,
-	 or one that results from arithmetic, even including
-	 a cast to integer type.  */
-      if (!null_pointer_constant)
+      /* An explicit constant 0 or type nullptr_t can convert to a pointer,
+	 or one that results from arithmetic, even including a cast to
+	 integer type.  */
+      if (!null_pointer_constant && coder != NULLPTR_TYPE)
 	switch (errtype)
 	  {
 	  case ic_argpass:
@@ -7691,7 +7706,10 @@  convert_for_assignment (location_t location, location_t expr_loc, tree type,
 
       return convert (type, rhs);
     }
-  else if (codel == BOOLEAN_TYPE && coder == POINTER_TYPE)
+  else if (codel == BOOLEAN_TYPE
+	   /* The type nullptr_t may be converted to bool.  The
+	      result is false.  */
+	   && (coder == POINTER_TYPE || coder == NULLPTR_TYPE))
     {
       tree ret;
       bool save = in_late_binary_op;
@@ -12107,10 +12125,10 @@  build_binary_op (location_t location, enum tree_code code,
     case TRUTH_XOR_EXPR:
       if ((code0 == INTEGER_TYPE || code0 == POINTER_TYPE
 	   || code0 == REAL_TYPE || code0 == COMPLEX_TYPE
-	   || code0 == FIXED_POINT_TYPE)
+	   || code0 == FIXED_POINT_TYPE || code0 == NULLPTR_TYPE)
 	  && (code1 == INTEGER_TYPE || code1 == POINTER_TYPE
 	      || code1 == REAL_TYPE || code1 == COMPLEX_TYPE
-	      || code1 == FIXED_POINT_TYPE))
+	      || code1 == FIXED_POINT_TYPE || code1 ==  NULLPTR_TYPE))
 	{
 	  /* Result of these operations is always an int,
 	     but that does not mean the operands should be
@@ -12418,6 +12436,27 @@  build_binary_op (location_t location, enum tree_code code,
 	  result_type = type1;
 	  pedwarn (location, 0, "comparison between pointer and integer");
 	}
+      /* 6.5.9: One of the following shall hold:
+	 -- both operands have type nullptr_t;  */
+      else if (code0 == NULLPTR_TYPE && code1 == NULLPTR_TYPE)
+	{
+	  result_type = nullptr_type_node;
+	  /* No need to convert the operands to result_type later.  */
+	  converted = 1;
+	}
+    /* -- one operand has type nullptr_t and the other is a null pointer
+       constant.  We will have to convert the former to the type of the
+       latter, because during gimplification we can't have mismatching
+       comparison operand type.  We convert from nullptr_t to the other
+       type, since only nullptr_t can be converted to nullptr_t.  Also,
+       even a constant 0 is a null pointer constant, so we may have to
+       create a pointer type from its type.  */
+      else if (code0 == NULLPTR_TYPE && null_pointer_constant_p (orig_op1))
+	result_type = (INTEGRAL_TYPE_P (type1)
+		       ? build_pointer_type (type1) : type1);
+      else if (code1 == NULLPTR_TYPE && null_pointer_constant_p (orig_op0))
+	result_type = (INTEGRAL_TYPE_P (type0)
+		       ? build_pointer_type (type0) : type0);
       if ((TREE_CODE (TREE_TYPE (orig_op0)) == BOOLEAN_TYPE
 	   || truth_value_p (TREE_CODE (orig_op0)))
 	  ^ (TREE_CODE (TREE_TYPE (orig_op1)) == BOOLEAN_TYPE
diff --git a/gcc/cp/cp-tree.h b/gcc/cp/cp-tree.h
index 9f2ff3728b4..c897da204fe 100644
--- a/gcc/cp/cp-tree.h
+++ b/gcc/cp/cp-tree.h
@@ -187,9 +187,6 @@  enum cp_tree_index
     CPTI_NOEXCEPT_FALSE_SPEC,
     CPTI_NOEXCEPT_DEFERRED_SPEC,
 
-    CPTI_NULLPTR,
-    CPTI_NULLPTR_TYPE,
-
     CPTI_ANY_TARG,
 
     CPTI_MODULE_HWM,
@@ -254,8 +251,6 @@  extern GTY(()) tree cp_global_trees[CPTI_MAX];
 #define conv_op_marker			cp_global_trees[CPTI_CONV_OP_MARKER]
 #define abort_fndecl			cp_global_trees[CPTI_ABORT_FNDECL]
 #define current_aggr			cp_global_trees[CPTI_AGGR_TAG]
-#define nullptr_node			cp_global_trees[CPTI_NULLPTR]
-#define nullptr_type_node		cp_global_trees[CPTI_NULLPTR_TYPE]
 /* std::align_val_t */
 #define align_type_node			cp_global_trees[CPTI_ALIGN_TYPE]
 
@@ -4405,9 +4400,6 @@  get_vec_init_expr (tree t)
    || TREE_CODE (TYPE) == REAL_TYPE \
    || TREE_CODE (TYPE) == COMPLEX_TYPE)
 
-/* True iff TYPE is cv decltype(nullptr).  */
-#define NULLPTR_TYPE_P(TYPE) (TREE_CODE (TYPE) == NULLPTR_TYPE)
-
 /* [basic.types]
 
    Arithmetic types, enumeration types, pointer types,
diff --git a/gcc/cp/decl.cc b/gcc/cp/decl.cc
index 84a1a011341..d46a347a6c7 100644
--- a/gcc/cp/decl.cc
+++ b/gcc/cp/decl.cc
@@ -4793,16 +4793,10 @@  cxx_init_decl_processing (void)
 	  }
       }
 
-    nullptr_type_node = make_node (NULLPTR_TYPE);
-    TYPE_SIZE (nullptr_type_node) = bitsize_int (GET_MODE_BITSIZE (ptr_mode));
-    TYPE_SIZE_UNIT (nullptr_type_node) = size_int (GET_MODE_SIZE (ptr_mode));
-    TYPE_UNSIGNED (nullptr_type_node) = 1;
-    TYPE_PRECISION (nullptr_type_node) = GET_MODE_BITSIZE (ptr_mode);
+    /* C++-specific nullptr initialization.  */
     if (abi_version_at_least (9))
       SET_TYPE_ALIGN (nullptr_type_node, GET_MODE_ALIGNMENT (ptr_mode));
-    SET_TYPE_MODE (nullptr_type_node, ptr_mode);
     record_builtin_type (RID_MAX, "decltype(nullptr)", nullptr_type_node);
-    nullptr_node = build_int_cst (nullptr_type_node, 0);
   }
 
   if (! supports_one_only ())
diff --git a/gcc/ginclude/stddef.h b/gcc/ginclude/stddef.h
index 79e296d4a66..315ff786694 100644
--- a/gcc/ginclude/stddef.h
+++ b/gcc/ginclude/stddef.h
@@ -443,6 +443,14 @@  typedef struct {
 #endif
 #endif /* C++11.  */
 
+#if (defined (__STDC_VERSION__) && __STDC_VERSION__ > 201710L)
+#ifndef _GCC_NULLPTR_T
+#define _GCC_NULLPTR_T
+  typedef __typeof__(nullptr) nullptr_t;
+/* ??? This doesn't define __STDC_VERSION_STDDEF_H__ yet.  */
+#endif
+#endif /* C23.  */
+
 #endif /* _STDDEF_H was defined this time */
 
 #endif /* !_STDDEF_H && !_STDDEF_H_ && !_ANSI_STDDEF_H && !__STDDEF_H__
diff --git a/gcc/testsuite/gcc.dg/c11-nullptr-1.c b/gcc/testsuite/gcc.dg/c11-nullptr-1.c
new file mode 100644
index 00000000000..c4faedc2c91
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/c11-nullptr-1.c
@@ -0,0 +1,10 @@ 
+/* Test that in pre-C23 modes, nullptr is a normal identifier,
+   not a keyword.  */
+/* { dg-options "-std=c11 -pedantic-errors" } */
+
+int nullptr;
+
+void
+f (int nullptr)
+{
+}
diff --git a/gcc/testsuite/gcc.dg/c17-nullptr-1.c b/gcc/testsuite/gcc.dg/c17-nullptr-1.c
new file mode 100644
index 00000000000..92e43b9df23
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/c17-nullptr-1.c
@@ -0,0 +1,10 @@ 
+/* Test that in pre-C23 modes, nullptr is a normal identifier,
+   not a keyword.  */
+/* { dg-options "-std=c17 -pedantic-errors" } */
+
+int nullptr;
+
+void
+f (int nullptr)
+{
+}
diff --git a/gcc/testsuite/gcc.dg/c17-nullptr-2.c b/gcc/testsuite/gcc.dg/c17-nullptr-2.c
new file mode 100644
index 00000000000..a6ad7703eeb
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/c17-nullptr-2.c
@@ -0,0 +1,10 @@ 
+/* Test that we don't predefine `nullptr' pre-C2X.  */
+/* { dg-do compile } */
+/* { dg-options "-std=c17 -pedantic-errors" } */
+
+int *
+fn (int *p)
+{
+  p = nullptr; /* { dg-error "'nullptr' undeclared" } */
+  return p;
+}
diff --git a/gcc/testsuite/gcc.dg/c2x-nullptr-1.c b/gcc/testsuite/gcc.dg/c2x-nullptr-1.c
new file mode 100644
index 00000000000..ca01b3e3296
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/c2x-nullptr-1.c
@@ -0,0 +1,259 @@ 
+/* Test valid usage of C23 nullptr.  */
+/* { dg-do run } */
+/* { dg-options "-std=c2x -pedantic-errors -Wall -Wextra -Wno-unused-variable" } */
+
+#include <stdarg.h>
+
+typedef __typeof__(nullptr) nullptr_t;
+
+void f1 (nullptr_t) { }
+void f2 (int *) { }
+void f3 (_Bool) { }
+nullptr_t cmp (void) { return nullptr; }
+
+/* The type nullptr_t shall not be converted to any type other than void, bool or
+   a pointer type.  No type other than nullptr_t shall be converted to nullptr_t.  */
+void
+test1 (void)
+{
+  const nullptr_t nptr = nullptr;
+  static nullptr_t static_nptr;
+  int *p1 = nullptr;
+  void *p2 = nullptr;
+  float *p3 = nullptr;
+  void (*p4)(int) = nullptr;
+  int (*p5)[10] = nullptr;
+  int *p6 = nptr;
+  void *p7 = nptr;
+  float *p8 = nptr;
+  void (*p9)(int) = nptr;
+  int (*p10)[10] = nptr;
+  int *p11 = (int *) nullptr;
+  int *p12 = (int *) nptr;
+  if (nullptr || p1 || p2 || p3 || p4 || p5 || p6 || p7 || p8 || p9 || p10
+      || p11 || p12)
+    __builtin_abort ();
+
+  _Bool b1 = nullptr;
+  _Bool b2 = (_Bool) nullptr;
+  _Bool b3 = nptr;
+  _Bool b4 = (_Bool) nptr;
+  if (b1 || b2 || b3 || b4 || (_Bool) nullptr || (_Bool) nptr)
+   __builtin_abort ();
+
+  __auto_type a1 = nullptr;
+  __auto_type a2 = nptr;
+
+  /* We can convert nullptr_t to nullptr_t.  */
+  __typeof__(nullptr) x = nullptr;
+  f1 (x);
+  f1 (nullptr);
+  f2 (x);
+  f2 (nullptr);
+  f3 (nullptr);
+
+  const nullptr_t np1 = nullptr;
+  const nullptr_t np2 = np1;
+  (void) nullptr;
+  (void) np1;
+  (void) np2;
+  (void) cmp ();
+  (void)(nullptr_t) nullptr;
+}
+
+/* Test valid comparison.  */
+void
+test2 (int *p)
+{
+  /* If both operands have type nullptr_t or one operand has type nullptr_t
+     and the other is a null pointer constant, they compare equal.  */
+  const nullptr_t nptr = nullptr;
+  int r = 0;
+
+  /* Both operands have type nullptr_t.  */
+  r |= nullptr != nullptr;
+  r |= cmp () != nullptr;
+  r |= nullptr != cmp ();
+  r |= !(nullptr == nullptr);
+  r |= !(cmp () == nullptr);
+  r |= !(nullptr == cmp ());
+  r |= nptr != nptr;
+  r |= cmp () != nptr;
+  r |= nptr != cmp ();
+  r |= !(nptr == nptr);
+  r |= !(cmp () == nptr);
+  r |= !(nptr == cmp ());
+
+ /* One operand has type nullptr_t and the other is a null pointer constant.  */
+  r |= nullptr != (void *) 0;
+  r |= (nullptr) != (void *) 0;
+  r |= !(nullptr == (void *) 0);
+  r |= (void *) 0 != nullptr;
+  r |= (void *) 0 != (nullptr);
+  r |= !((void *) 0 == nullptr);
+  r |= nullptr != 0;
+  r |= (nullptr) != 0;
+  r |= 0 != nullptr;
+  r |= 0 != (nullptr);
+  r |= !(nullptr == 0);
+  r |= !(0 == nullptr);
+  r |= nullptr != 0u;
+  r |= 0u != nullptr;
+  r |= !(nullptr == 0u);
+  r |= !(0u == nullptr);
+  r |= nptr != (void *) 0;
+  r |= !(nptr == (void *) 0);
+  r |= (void *) 0 != nptr;
+  r |= !((void *) 0 == nptr);
+  r |= nptr != 0;
+  r |= 0 != nptr;
+  r |= !(nptr == 0);
+  r |= !(0 == nptr);
+  r |= nptr != 0u;
+  r |= 0u != nptr;
+  r |= !(nptr == 0u);
+  r |= !(0u == nptr);
+  if (r)
+    __builtin_abort ();
+
+  /* One operand is a pointer and the other is a null pointer constant.  */
+  (void) (p == nullptr);
+  (void) (p != nullptr);
+  (void) (nullptr == p);
+  (void) (nullptr != p);
+  (void) (p == (nullptr));
+  (void) (p != (nullptr));
+  (void) ((nullptr) == p);
+  (void) ((nullptr) != p);
+  (void) ((void *)nullptr == nullptr);
+  (void) ((void *)nullptr != nullptr);
+  (void) (nullptr == (void *)nullptr);
+  (void) (nullptr != (void *)nullptr);
+}
+
+/* Test ?:.  */
+void
+test3 (int *p, _Bool b)
+{
+  int x = nullptr ? 1 : 2;
+  (void) x;
+  const nullptr_t nptr = nullptr;
+  /* One of the following shall hold for the second and third operands:
+     -- both operands have nullptr_t type.  */
+  __auto_type r1 = b ? nullptr : nullptr;
+  __auto_type r2 = b ? nptr : nptr;
+  /* -- one operand is a pointer and the other is a null pointer constant
+     or has type nullptr_t;  */
+  __auto_type r3 = b ? p : nullptr;
+  __auto_type r4 = b ? nullptr : p;
+  __auto_type r5 = b ? nptr : p;
+  __auto_type r6 = b ? p : nptr;
+  __auto_type r7 = b ? 0 : p;
+  __auto_type r8 = b ? p : 0;
+  __auto_type r9 = b ? p : cmp ();
+  __auto_type r10 = b ?  cmp () : p;
+}
+
+/* Simple assignment.  */
+void
+test4 (void)
+{
+  /* -- the left operand has an atomic, qualified, or unqualified version of
+     the nullptr_t type and the type of the right is nullptr_t;  */
+  nullptr_t n1;
+  n1 = nullptr;
+  const nullptr_t n2 = nullptr;
+  _Atomic nullptr_t n3 = nullptr;
+  volatile nullptr_t n4 = nullptr;
+  /* -- the left operand is an atomic, qualified, or unqualified pointer,
+     and the type of the right is nullptr_t;  */
+  int *p1 = cmp ();
+  _Atomic int *p2 = cmp ();
+  const int *volatile p3 = cmp ();
+  const int *const *const p4 = cmp ();
+  double (*const p5)(void) = n1;
+  /* -- the left operand is an atomic, qualified, or unqualified bool, and
+     the type of the right is nullptr_t;  */
+  _Bool b1;
+  b1 = cmp ();
+  const _Bool b2 = nullptr;
+  _Atomic _Bool b3;
+  b3 = n1;
+  (void) b1;
+  (void) b3;
+}
+
+/* var_arg etc.  */
+static void
+test5 (int i, ...)
+{
+  va_list ap;
+  va_start (ap, i);
+  if (va_arg (ap, void *))
+    __builtin_abort ();
+}
+
+/* Operand of alignas, sizeof or typeof operators.  */
+void
+test6 (void)
+{
+  _Static_assert (sizeof (nullptr) == sizeof (void *), "sizeof (nullptr)");
+  _Static_assert (sizeof (nullptr_t) == sizeof (void *), "sizeof (nullptr_t)");
+  _Static_assert (sizeof (nullptr) == sizeof (char *), "sizeof (nullptr)");
+  _Static_assert (sizeof (nullptr_t) == sizeof (char *), "sizeof (nullptr_t)");
+  _Static_assert (_Alignof (nullptr_t) == _Alignof (char *), "_Alignof (nullptr_t)");
+  __typeof__(nullptr) t = nullptr;
+  f1 (t);
+  _Alignas (nullptr_t) char i1 = 'q';
+
+  _Static_assert (_Generic (nullptr, nullptr_t: 1, default: 0) == 1, "_Generic");
+  _Static_assert (_Generic (t, nullptr_t: 1, default: 0) == 1, "_Generic");
+  _Static_assert (_Generic (cmp (), nullptr_t: 1, default: 0) == 1, "_Generic");
+  _Static_assert (_Generic (0, nullptr_t: 1, int: 2, default: 0) == 2, "_Generic");
+  _Static_assert (_Generic ((void *)0, nullptr_t: 1, void *: 2, default: 0) == 2, "_Generic");
+  _Static_assert (_Generic (nullptr, nullptr_t: 1, void *: 2, default: 0) == 1, "_Generic");
+}
+
+/* Play with !, ||, &&. */
+void
+test7 (void)
+{
+  if (nullptr)
+    __builtin_abort ();
+  if (1 && nullptr)
+    __builtin_abort ();
+  if (0 || nullptr)
+    __builtin_abort ();
+  if (nullptr && 1)
+    __builtin_abort ();
+  if (nullptr || 0)
+    __builtin_abort ();
+  if (!nullptr)
+    {
+    }
+  else
+    __builtin_abort ();
+  while (nullptr)
+    __builtin_abort ();
+  int i = 0;
+  do
+    ++i;
+  while (nullptr);
+  if (i != 1)
+    __builtin_abort ();
+  for (;nullptr;)
+    __builtin_abort ();
+}
+
+int
+main (void)
+{
+  int i = 42;
+  test1 ();
+  test2 (&i);
+  test3 (&i, 0);
+  test4 ();
+  test5 (42, nullptr);
+  test6 ();
+  test7 ();
+}
diff --git a/gcc/testsuite/gcc.dg/c2x-nullptr-2.c b/gcc/testsuite/gcc.dg/c2x-nullptr-2.c
new file mode 100644
index 00000000000..2cc88353581
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/c2x-nullptr-2.c
@@ -0,0 +1,9 @@ 
+/* Test nullptr_t from <stddef.h..  */
+/* { dg-do compile } */
+/* { dg-options "-std=c2x -pedantic-errors" } */
+
+#include <stddef.h>
+
+void f(nullptr_t);
+_Static_assert (sizeof (nullptr_t) == sizeof (char *), "sizeof (nullptr_t)");
+_Static_assert (_Alignof (nullptr_t) == _Alignof (char *), "_Alignof (nullptr_t)");
diff --git a/gcc/testsuite/gcc.dg/c2x-nullptr-3.c b/gcc/testsuite/gcc.dg/c2x-nullptr-3.c
new file mode 100644
index 00000000000..91e9b1eb883
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/c2x-nullptr-3.c
@@ -0,0 +1,78 @@ 
+/* Test wrong usage of C23 nullptr.  */
+/* { dg-do compile } */
+/* { dg-options "-std=c2x -pedantic-errors -Wall -Wextra -Wno-unused-variable" } */
+
+typedef __typeof__(nullptr) nullptr_t;
+
+void g (nullptr_t); /* { dg-message "expected .nullptr_t. but argument is of type .int." } */
+nullptr_t cmp (void);
+
+void
+test1 (int *p)
+{
+  (void) (p > nullptr); /* { dg-error "ordered comparison" } */
+  (void) (p >= nullptr); /* { dg-error "ordered comparison" } */
+  (void) (p < nullptr); /* { dg-error "ordered comparison" } */
+  (void) (p <= nullptr); /* { dg-error "ordered comparison" } */
+  (void) (nullptr == 1); /* { dg-error "invalid operands" } */
+  (void) (1 == nullptr); /* { dg-error "invalid operands" } */
+  (void) (nullptr != 1); /* { dg-error "invalid operands" } */
+  (void) (1 != nullptr); /* { dg-error "invalid operands" } */
+  (void) (1 > nullptr); /* { dg-error "invalid operands" } */
+
+  /* "(nullptr_t)nullptr" has type nullptr_t but isn't an NPC.  */
+  (void) ((nullptr_t)nullptr == p); /* { dg-error "invalid operands" } */
+  (void) ((nullptr_t)nullptr != p); /* { dg-error "invalid operands" } */
+  (void) (p == (nullptr_t)nullptr); /* { dg-error "invalid operands" } */
+  (void) (p != (nullptr_t)nullptr); /* { dg-error "invalid operands" } */
+  (void) (cmp () == p); /* { dg-error "invalid operands" } */
+  (void) (cmp () != p); /* { dg-error "invalid operands" } */
+  (void) (p == cmp ()); /* { dg-error "invalid operands" } */
+  (void) (p != cmp ()); /* { dg-error "invalid operands" } */
+  /* "(void *)nullptr" is not an NPC, either.  */
+  (void) ((void *)nullptr == cmp ()); /* { dg-error "invalid operands" } */
+  (void) ((void *)nullptr != cmp ()); /* { dg-error "invalid operands" } */
+  (void) (cmp () == (void *)nullptr); /* { dg-error "invalid operands" } */
+  (void) (cmp () != (void *)nullptr); /* { dg-error "invalid operands" } */
+}
+
+void
+test2 (void)
+{
+  const nullptr_t nptr = nullptr;
+  int p = nullptr; /* { dg-error "incompatible types" } */
+  float d = nullptr; /* { dg-error "incompatible types" } */
+  char arr[10] = { nullptr }; /* { dg-error "incompatible types" } */
+
+  /* No type other than nullptr_t shall be converted to nullptr_t.  */
+  const nullptr_t n = 0; /* { dg-error "invalid initializer" } */
+  +(nullptr_t) 0; /* { dg-error "conversion from .int. to .nullptr_t." } */
+
+  g (0); /* { dg-error "incompatible type" } */
+
+  int i = 42 + nullptr; /* { dg-error "invalid operands" } */
+
+  /* The assignment of an object of type nullptr_t with a value of another
+     type, even if the value is a null pointer constant, is a constraint
+     violation.  */
+  nullptr_t m;
+  m = 0; /* { dg-error "incompatible types" } */
+  (void) m;
+  nullptr_t o = 0; /* { dg-error "invalid initializer" } */
+
+  switch (nullptr); /* { dg-error "switch quantity not an integer" } */
+}
+
+/* If a second or third operand of type nullptr_t is used that is not a null
+   pointer constant and the other operand is not a pointer or does not have
+   itself nullptr_t, a constraint is violated even if that other operand is
+   a null pointer constant such as 0.  */
+void
+test3 (_Bool b, int i)
+{
+  const nullptr_t nptr = nullptr;
+  __auto_type a1 = b ? nptr : i; /* { dg-error "type mismatch" } */
+  __auto_type a2 = b ? i : nptr; /* { dg-error "type mismatch" } */
+  __auto_type a3 = b ? nptr : 0; /* { dg-error "type mismatch" } */
+  __auto_type a4 = b ? 0 : nptr; /* { dg-error "type mismatch" } */
+}
diff --git a/gcc/testsuite/gcc.dg/c2x-nullptr-4.c b/gcc/testsuite/gcc.dg/c2x-nullptr-4.c
new file mode 100644
index 00000000000..7479ab4ea1d
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/c2x-nullptr-4.c
@@ -0,0 +1,11 @@ 
+/* Test that -Wc11-c2x-compat issues a warning (not a pedwarn) about
+   `nullptr' in C2X.  */
+/* { dg-do compile } */
+/* { dg-options "-std=c2x -pedantic-errors -Wc11-c2x-compat" } */
+
+int *
+fn (int *p)
+{
+  p = nullptr; /* { dg-warning "ISO C does not support .nullptr. before C2X" } */
+  return p;
+}
diff --git a/gcc/testsuite/gcc.dg/c2x-nullptr-5.c b/gcc/testsuite/gcc.dg/c2x-nullptr-5.c
new file mode 100644
index 00000000000..27803f7d03f
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/c2x-nullptr-5.c
@@ -0,0 +1,14 @@ 
+/* Test that we don't lose side-effects when converting from nullptr_t.  */
+/* { dg-do run } */
+/* { dg-options "-std=c2x -pedantic-errors" } */
+
+int i;
+nullptr_t fn () { ++i; return nullptr; }
+
+int
+main ()
+{
+  int *p = fn ();
+  if (i != 1)
+    __builtin_abort ();
+}