wgsl reader: Allow 0X as a hex prefix for ints and floats

Fixed: tint:1353
Change-Id: I8252cc9d055657e361c4bceadcc59f7703f98c23
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/74380
Reviewed-by: Antonio Maiorano <amaiorano@google.com>
Auto-Submit: David Neto <dneto@google.com>
Reviewed-by: David Neto <dneto@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
Commit-Queue: David Neto <dneto@google.com>
diff --git a/src/reader/wgsl/lexer.cc b/src/reader/wgsl/lexer.cc
index ada6580..7e521d1 100644
--- a/src/reader/wgsl/lexer.cc
+++ b/src/reader/wgsl/lexer.cc
@@ -342,7 +342,7 @@
   auto source = begin_source();
 
   // clang-format off
-  // -?0x([0-9a-fA-F]*.?[0-9a-fA-F]+ | [0-9a-fA-F]+.[0-9a-fA-F]*)(p|P)(+|-)?[0-9]+  // NOLINT
+  // -?0[xX]([0-9a-fA-F]*.?[0-9a-fA-F]+ | [0-9a-fA-F]+.[0-9a-fA-F]*)(p|P)(+|-)?[0-9]+  // NOLINT
   // clang-format on
 
   // -?
@@ -351,8 +351,8 @@
     sign_bit = 1;
     end++;
   }
-  // 0x
-  if (matches(end, "0x")) {
+  // 0[xX]
+  if (matches(end, "0x") || matches(end, "0X")) {
     end += 2;
   } else {
     return {};
@@ -663,7 +663,7 @@
     end++;
   }
 
-  if (matches(end, "0x")) {
+  if (matches(end, "0x") || matches(end, "0X")) {
     end += 2;
   } else {
     return {};
diff --git a/src/reader/wgsl/lexer_test.cc b/src/reader/wgsl/lexer_test.cc
index fbfea7a..4db3a83 100644
--- a/src/reader/wgsl/lexer_test.cc
+++ b/src/reader/wgsl/lexer_test.cc
@@ -394,11 +394,17 @@
     IntegerTest_HexSigned,
     testing::Values(
         HexSignedIntData{"0x0", 0},
+        HexSignedIntData{"0X0", 0},
         HexSignedIntData{"0x42", 66},
+        HexSignedIntData{"0X42", 66},
         HexSignedIntData{"-0x42", -66},
+        HexSignedIntData{"-0X42", -66},
         HexSignedIntData{"0xeF1Abc9", 250719177},
+        HexSignedIntData{"0XeF1Abc9", 250719177},
         HexSignedIntData{"-0x80000000", std::numeric_limits<int32_t>::min()},
-        HexSignedIntData{"0x7FFFFFFF", std::numeric_limits<int32_t>::max()}));
+        HexSignedIntData{"-0X80000000", std::numeric_limits<int32_t>::min()},
+        HexSignedIntData{"0x7FFFFFFF", std::numeric_limits<int32_t>::max()},
+        HexSignedIntData{"0X7FFFFFFF", std::numeric_limits<int32_t>::max()}));
 
 TEST_F(LexerTest, HexPrefixOnly_IsError) {
   // Could be the start of a hex integer or hex float, but is neither.
@@ -411,6 +417,17 @@
             "integer or float hex literal has no significant digits");
 }
 
+TEST_F(LexerTest, HexPrefixUpperCaseOnly_IsError) {
+  // Could be the start of a hex integer or hex float, but is neither.
+  Source::FileContent content("0X");
+  Lexer l("test.wgsl", &content);
+
+  auto t = l.next();
+  ASSERT_TRUE(t.Is(Token::Type::kError));
+  EXPECT_EQ(t.to_str(),
+            "integer or float hex literal has no significant digits");
+}
+
 TEST_F(LexerTest, NegativeHexPrefixOnly_IsError) {
   // Could be the start of a hex integer or hex float, but is neither.
   Source::FileContent content("-0x");
@@ -422,6 +439,17 @@
             "integer or float hex literal has no significant digits");
 }
 
+TEST_F(LexerTest, NegativeHexPrefixUpperCaseOnly_IsError) {
+  // Could be the start of a hex integer or hex float, but is neither.
+  Source::FileContent content("-0X");
+  Lexer l("test.wgsl", &content);
+
+  auto t = l.next();
+  ASSERT_TRUE(t.Is(Token::Type::kError));
+  EXPECT_EQ(t.to_str(),
+            "integer or float hex literal has no significant digits");
+}
+
 TEST_F(LexerTest, IntegerTest_HexSignedTooLarge) {
   Source::FileContent content("0x80000000");
   Lexer l("test.wgsl", &content);
diff --git a/src/reader/wgsl/parser_impl_const_literal_test.cc b/src/reader/wgsl/parser_impl_const_literal_test.cc
index 9e6a88a..e91051f 100644
--- a/src/reader/wgsl/parser_impl_const_literal_test.cc
+++ b/src/reader/wgsl/parser_impl_const_literal_test.cc
@@ -17,6 +17,8 @@
 #include <cmath>
 #include <cstring>
 
+#include "gmock/gmock.h"
+
 namespace tint {
 namespace reader {
 namespace wgsl {
@@ -115,9 +117,19 @@
   ASSERT_EQ(c.value, nullptr);
 }
 
+// Returns true if the given non-Nan float numbers are equal.
+bool FloatEqual(float a, float b) {
+  // Avoid Clang complaining about equality test on float.
+  // -Wfloat-equal.
+  return (a <= b) && (a >= b);
+}
+
 struct FloatLiteralTestCase {
-  const char* input;
+  std::string input;
   float expected;
+  bool operator==(const FloatLiteralTestCase& other) const {
+    return (input == other.input) && FloatEqual(expected, other.expected);
+  }
 };
 
 inline std::ostream& operator<<(std::ostream& out, FloatLiteralTestCase data) {
@@ -140,192 +152,233 @@
   EXPECT_FLOAT_EQ(c->As<ast::FloatLiteralExpression>()->value, params.expected);
 }
 
-FloatLiteralTestCase float_literal_test_cases[] = {
-    {"0.0", 0.0f},                         // Zero
-    {"1.0", 1.0f},                         // One
-    {"-1.0", -1.0f},                       // MinusOne
-    {"1000000000.0", 1e9f},                // Billion
-    {"-0.0", std::copysign(0.0f, -5.0f)},  // NegativeZero
-    {"0.0", MakeFloat(0, 0, 0)},           // Zero
-    {"-0.0", MakeFloat(1, 0, 0)},          // NegativeZero
-    {"1.0", MakeFloat(0, 127, 0)},         // One
-    {"-1.0", MakeFloat(1, 127, 0)},        // NegativeOne
-};
+using FloatLiteralTestCaseList = std::vector<FloatLiteralTestCase>;
+
+FloatLiteralTestCaseList DecimalFloatCases() {
+  return FloatLiteralTestCaseList{
+      {"0.0", 0.0f},                         // Zero
+      {"1.0", 1.0f},                         // One
+      {"-1.0", -1.0f},                       // MinusOne
+      {"1000000000.0", 1e9f},                // Billion
+      {"-0.0", std::copysign(0.0f, -5.0f)},  // NegativeZero
+      {"0.0", MakeFloat(0, 0, 0)},           // Zero
+      {"-0.0", MakeFloat(1, 0, 0)},          // NegativeZero
+      {"1.0", MakeFloat(0, 127, 0)},         // One
+      {"-1.0", MakeFloat(1, 127, 0)},        // NegativeOne
+  };
+}
+
 INSTANTIATE_TEST_SUITE_P(ParserImplFloatLiteralTest_Float,
                          ParserImplFloatLiteralTest,
-                         testing::ValuesIn(float_literal_test_cases));
+                         testing::ValuesIn(DecimalFloatCases()));
 
 const float NegInf = MakeFloat(1, 255, 0);
 const float PosInf = MakeFloat(0, 255, 0);
-FloatLiteralTestCase hexfloat_literal_test_cases[] = {
-    // Regular numbers
-    {"0x0p+0", 0.f},
-    {"0x1p+0", 1.f},
-    {"0x1p+1", 2.f},
-    {"0x1.8p+1", 3.f},
-    {"0x1.99999ap-4", 0.1f},
-    {"0x1p-1", 0.5f},
-    {"0x1p-2", 0.25f},
-    {"0x1.8p-1", 0.75f},
-    {"-0x0p+0", -0.f},
-    {"-0x1p+0", -1.f},
-    {"-0x1p-1", -0.5f},
-    {"-0x1p-2", -0.25f},
-    {"-0x1.8p-1", -0.75f},
+FloatLiteralTestCaseList HexFloatCases() {
+  return FloatLiteralTestCaseList{
+      // Regular numbers
+      {"0x0p+0", 0.f},
+      {"0x1p+0", 1.f},
+      {"0x1p+1", 2.f},
+      {"0x1.8p+1", 3.f},
+      {"0x1.99999ap-4", 0.1f},
+      {"0x1p-1", 0.5f},
+      {"0x1p-2", 0.25f},
+      {"0x1.8p-1", 0.75f},
+      {"-0x0p+0", -0.f},
+      {"-0x1p+0", -1.f},
+      {"-0x1p-1", -0.5f},
+      {"-0x1p-2", -0.25f},
+      {"-0x1.8p-1", -0.75f},
 
-    // Large numbers
-    {"0x1p+9", 512.f},
-    {"0x1p+10", 1024.f},
-    {"0x1.02p+10", 1024.f + 8.f},
-    {"-0x1p+9", -512.f},
-    {"-0x1p+10", -1024.f},
-    {"-0x1.02p+10", -1024.f - 8.f},
+      // Large numbers
+      {"0x1p+9", 512.f},
+      {"0x1p+10", 1024.f},
+      {"0x1.02p+10", 1024.f + 8.f},
+      {"-0x1p+9", -512.f},
+      {"-0x1p+10", -1024.f},
+      {"-0x1.02p+10", -1024.f - 8.f},
 
-    // Small numbers
-    {"0x1p-9", 1.0f / 512.f},
-    {"0x1p-10", 1.0f / 1024.f},
-    {"0x1.02p-3", 1.0f / 1024.f + 1.0f / 8.f},
-    {"-0x1p-9", 1.0f / -512.f},
-    {"-0x1p-10", 1.0f / -1024.f},
-    {"-0x1.02p-3", 1.0f / -1024.f - 1.0f / 8.f},
+      // Small numbers
+      {"0x1p-9", 1.0f / 512.f},
+      {"0x1p-10", 1.0f / 1024.f},
+      {"0x1.02p-3", 1.0f / 1024.f + 1.0f / 8.f},
+      {"-0x1p-9", 1.0f / -512.f},
+      {"-0x1p-10", 1.0f / -1024.f},
+      {"-0x1.02p-3", 1.0f / -1024.f - 1.0f / 8.f},
 
-    // Near lowest non-denorm
-    {"0x1p-124", std::ldexp(1.f * 8.f, -127)},
-    {"0x1p-125", std::ldexp(1.f * 4.f, -127)},
-    {"-0x1p-124", -std::ldexp(1.f * 8.f, -127)},
-    {"-0x1p-125", -std::ldexp(1.f * 4.f, -127)},
+      // Near lowest non-denorm
+      {"0x1p-124", std::ldexp(1.f * 8.f, -127)},
+      {"0x1p-125", std::ldexp(1.f * 4.f, -127)},
+      {"-0x1p-124", -std::ldexp(1.f * 8.f, -127)},
+      {"-0x1p-125", -std::ldexp(1.f * 4.f, -127)},
 
-    // Lowest non-denorm
-    {"0x1p-126", std::ldexp(1.f * 2.f, -127)},
-    {"-0x1p-126", -std::ldexp(1.f * 2.f, -127)},
+      // Lowest non-denorm
+      {"0x1p-126", std::ldexp(1.f * 2.f, -127)},
+      {"-0x1p-126", -std::ldexp(1.f * 2.f, -127)},
 
-    // Denormalized values
-    {"0x1p-127", std::ldexp(1.f, -127)},
-    {"0x1p-128", std::ldexp(1.f / 2.f, -127)},
-    {"0x1p-129", std::ldexp(1.f / 4.f, -127)},
-    {"0x1p-130", std::ldexp(1.f / 8.f, -127)},
-    {"-0x1p-127", -std::ldexp(1.f, -127)},
-    {"-0x1p-128", -std::ldexp(1.f / 2.f, -127)},
-    {"-0x1p-129", -std::ldexp(1.f / 4.f, -127)},
-    {"-0x1p-130", -std::ldexp(1.f / 8.f, -127)},
+      // Denormalized values
+      {"0x1p-127", std::ldexp(1.f, -127)},
+      {"0x1p-128", std::ldexp(1.f / 2.f, -127)},
+      {"0x1p-129", std::ldexp(1.f / 4.f, -127)},
+      {"0x1p-130", std::ldexp(1.f / 8.f, -127)},
+      {"-0x1p-127", -std::ldexp(1.f, -127)},
+      {"-0x1p-128", -std::ldexp(1.f / 2.f, -127)},
+      {"-0x1p-129", -std::ldexp(1.f / 4.f, -127)},
+      {"-0x1p-130", -std::ldexp(1.f / 8.f, -127)},
 
-    {"0x1.8p-127", std::ldexp(1.f, -127) + (std::ldexp(1.f, -127) / 2.f)},
-    {"0x1.8p-128", std::ldexp(1.f, -127) / 2.f + (std::ldexp(1.f, -127) / 4.f)},
+      {"0x1.8p-127", std::ldexp(1.f, -127) + (std::ldexp(1.f, -127) / 2.f)},
+      {"0x1.8p-128",
+       std::ldexp(1.f, -127) / 2.f + (std::ldexp(1.f, -127) / 4.f)},
 
-    {"0x1p-149", MakeFloat(0, 0, 1)},                 // +SmallestDenormal
-    {"0x1p-148", MakeFloat(0, 0, 2)},                 // +BiggerDenormal
-    {"0x1.fffffcp-127", MakeFloat(0, 0, 0x7fffff)},   // +LargestDenormal
-    {"-0x1p-149", MakeFloat(1, 0, 1)},                // -SmallestDenormal
-    {"-0x1p-148", MakeFloat(1, 0, 2)},                // -BiggerDenormal
-    {"-0x1.fffffcp-127", MakeFloat(1, 0, 0x7fffff)},  // -LargestDenormal
+      {"0x1p-149", MakeFloat(0, 0, 1)},                 // +SmallestDenormal
+      {"0x1p-148", MakeFloat(0, 0, 2)},                 // +BiggerDenormal
+      {"0x1.fffffcp-127", MakeFloat(0, 0, 0x7fffff)},   // +LargestDenormal
+      {"-0x1p-149", MakeFloat(1, 0, 1)},                // -SmallestDenormal
+      {"-0x1p-148", MakeFloat(1, 0, 2)},                // -BiggerDenormal
+      {"-0x1.fffffcp-127", MakeFloat(1, 0, 0x7fffff)},  // -LargestDenormal
 
-    {"0x1.2bfaf8p-127", MakeFloat(0, 0, 0xcafebe)},   // +Subnormal
-    {"-0x1.2bfaf8p-127", MakeFloat(1, 0, 0xcafebe)},  // -Subnormal
-    {"0x1.55554p-130", MakeFloat(0, 0, 0xaaaaa)},     // +Subnormal
-    {"-0x1.55554p-130", MakeFloat(1, 0, 0xaaaaa)},    // -Subnormal
+      {"0x1.2bfaf8p-127", MakeFloat(0, 0, 0xcafebe)},   // +Subnormal
+      {"-0x1.2bfaf8p-127", MakeFloat(1, 0, 0xcafebe)},  // -Subnormal
+      {"0x1.55554p-130", MakeFloat(0, 0, 0xaaaaa)},     // +Subnormal
+      {"-0x1.55554p-130", MakeFloat(1, 0, 0xaaaaa)},    // -Subnormal
 
-    // Nan -> Infinity
-    {"0x1.8p+128", PosInf},
-    {"0x1.0002p+128", PosInf},
-    {"0x1.0018p+128", PosInf},
-    {"0x1.01ep+128", PosInf},
-    {"0x1.fffffep+128", PosInf},
-    {"-0x1.8p+128", NegInf},
-    {"-0x1.0002p+128", NegInf},
-    {"-0x1.0018p+128", NegInf},
-    {"-0x1.01ep+128", NegInf},
-    {"-0x1.fffffep+128", NegInf},
+      // Nan -> Infinity
+      {"0x1.8p+128", PosInf},
+      {"0x1.0002p+128", PosInf},
+      {"0x1.0018p+128", PosInf},
+      {"0x1.01ep+128", PosInf},
+      {"0x1.fffffep+128", PosInf},
+      {"-0x1.8p+128", NegInf},
+      {"-0x1.0002p+128", NegInf},
+      {"-0x1.0018p+128", NegInf},
+      {"-0x1.01ep+128", NegInf},
+      {"-0x1.fffffep+128", NegInf},
 
-    // Infinity
-    {"0x1p+128", PosInf},
-    {"-0x1p+128", NegInf},
-    {"0x32p+127", PosInf},
-    {"0x32p+500", PosInf},
-    {"-0x32p+127", NegInf},
-    {"-0x32p+500", NegInf},
+      // Infinity
+      {"0x1p+128", PosInf},
+      {"-0x1p+128", NegInf},
+      {"0x32p+127", PosInf},
+      {"0x32p+500", PosInf},
+      {"-0x32p+127", NegInf},
+      {"-0x32p+500", NegInf},
 
-    // Overflow -> Infinity
-    {"0x1p+129", PosInf},
-    {"0x1.1p+128", PosInf},
-    {"-0x1p+129", NegInf},
-    {"-0x1.1p+128", NegInf},
-    {"0x1.0p2147483520", PosInf},  // INT_MAX - 127 (largest valid exponent)
+      // Overflow -> Infinity
+      {"0x1p+129", PosInf},
+      {"0x1.1p+128", PosInf},
+      {"-0x1p+129", NegInf},
+      {"-0x1.1p+128", NegInf},
+      {"0x1.0p2147483520", PosInf},  // INT_MAX - 127 (largest valid exponent)
 
-    // Underflow -> Zero
-    {"0x1p-500", 0.f},  // Exponent underflows
-    {"-0x1p-500", -0.f},
-    {"0x0.00000000001p-126", 0.f},  // Fraction causes underflow
-    {"-0x0.0000000001p-127", -0.f},
-    {"0x0.01p-142", 0.f},
-    {"-0x0.01p-142", -0.f},    // Fraction causes additional underflow
-    {"0x1.0p-2147483520", 0},  // -(INT_MAX - 127) (smallest valid exponent)
+      // Underflow -> Zero
+      {"0x1p-500", 0.f},  // Exponent underflows
+      {"-0x1p-500", -0.f},
+      {"0x0.00000000001p-126", 0.f},  // Fraction causes underflow
+      {"-0x0.0000000001p-127", -0.f},
+      {"0x0.01p-142", 0.f},
+      {"-0x0.01p-142", -0.f},    // Fraction causes additional underflow
+      {"0x1.0p-2147483520", 0},  // -(INT_MAX - 127) (smallest valid exponent)
 
-    // Zero with non-zero exponent -> Zero
-    {"0x0p+0", 0.f},
-    {"0x0p+1", 0.f},
-    {"0x0p-1", 0.f},
-    {"0x0p+9999999999", 0.f},
-    {"0x0p-9999999999", 0.f},
-    // Same, but with very large positive exponents that would cause overflow
-    // if the mantissa were non-zero.
-    {"0x0p+4000000000", 0.f},    // 4 billion:
-    {"0x0p+40000000000", 0.f},   // 40 billion
-    {"-0x0p+40000000000", 0.f},  // As above 2, but negative mantissa
-    {"-0x0p+400000000000", 0.f},
-    {"0x0.00p+4000000000", 0.f},  // As above 4, but with fractional part
-    {"0x0.00p+40000000000", 0.f},
-    {"-0x0.00p+40000000000", 0.f},
-    {"-0x0.00p+400000000000", 0.f},
-    {"0x0p-4000000000", 0.f},  // As above 8, but with negative exponents
-    {"0x0p-40000000000", 0.f},
-    {"-0x0p-40000000000", 0.f},
-    {"-0x0p-400000000000", 0.f},
-    {"0x0.00p-4000000000", 0.f},
-    {"0x0.00p-40000000000", 0.f},
-    {"-0x0.00p-40000000000", 0.f},
-    {"-0x0.00p-400000000000", 0.f},
+      // Zero with non-zero exponent -> Zero
+      {"0x0p+0", 0.f},
+      {"0x0p+1", 0.f},
+      {"0x0p-1", 0.f},
+      {"0x0p+9999999999", 0.f},
+      {"0x0p-9999999999", 0.f},
+      // Same, but with very large positive exponents that would cause overflow
+      // if the mantissa were non-zero.
+      {"0x0p+4000000000", 0.f},    // 4 billion:
+      {"0x0p+40000000000", 0.f},   // 40 billion
+      {"-0x0p+40000000000", 0.f},  // As above 2, but negative mantissa
+      {"-0x0p+400000000000", 0.f},
+      {"0x0.00p+4000000000", 0.f},  // As above 4, but with fractional part
+      {"0x0.00p+40000000000", 0.f},
+      {"-0x0.00p+40000000000", 0.f},
+      {"-0x0.00p+400000000000", 0.f},
+      {"0x0p-4000000000", 0.f},  // As above 8, but with negative exponents
+      {"0x0p-40000000000", 0.f},
+      {"-0x0p-40000000000", 0.f},
+      {"-0x0p-400000000000", 0.f},
+      {"0x0.00p-4000000000", 0.f},
+      {"0x0.00p-40000000000", 0.f},
+      {"-0x0.00p-40000000000", 0.f},
+      {"-0x0.00p-400000000000", 0.f},
 
-    // Test parsing
-    {"0x0p0", 0.f},
-    {"0x0p-0", 0.f},
-    {"0x0p+000", 0.f},
-    {"0x00000000000000p+000000000000000", 0.f},
-    {"0x00000000000000p-000000000000000", 0.f},
-    {"0x00000000000001p+000000000000000", 1.f},
-    {"0x00000000000001p-000000000000000", 1.f},
-    {"0x0000000000000000000001.99999ap-000000000000000004", 0.1f},
-    {"0x2p+0", 2.f},
-    {"0xFFp+0", 255.f},
-    {"0x0.8p+0", 0.5f},
-    {"0x0.4p+0", 0.25f},
-    {"0x0.4p+1", 2 * 0.25f},
-    {"0x0.4p+2", 4 * 0.25f},
-    {"0x123Ep+1", 9340.f},
-    {"-0x123Ep+1", -9340.f},
-    {"0x1a2b3cP12", 7.024656e+09f},
-    {"-0x1a2b3cP12", -7.024656e+09f},
+      // Test parsing
+      {"0x0p0", 0.f},
+      {"0x0p-0", 0.f},
+      {"0x0p+000", 0.f},
+      {"0x00000000000000p+000000000000000", 0.f},
+      {"0x00000000000000p-000000000000000", 0.f},
+      {"0x00000000000001p+000000000000000", 1.f},
+      {"0x00000000000001p-000000000000000", 1.f},
+      {"0x0000000000000000000001.99999ap-000000000000000004", 0.1f},
+      {"0x2p+0", 2.f},
+      {"0xFFp+0", 255.f},
+      {"0x0.8p+0", 0.5f},
+      {"0x0.4p+0", 0.25f},
+      {"0x0.4p+1", 2 * 0.25f},
+      {"0x0.4p+2", 4 * 0.25f},
+      {"0x123Ep+1", 9340.f},
+      {"-0x123Ep+1", -9340.f},
+      {"0x1a2b3cP12", 7.024656e+09f},
+      {"-0x1a2b3cP12", -7.024656e+09f},
 
-    // Examples without a binary exponent part.
-    {"0x1.", 1.0f},
-    {"0x.8", 0.5f},
-    {"0x1.8", 1.5f},
-    {"-0x1.", -1.0f},
-    {"-0x.8", -0.5f},
-    {"-0x1.8", -1.5f},
+      // Examples without a binary exponent part.
+      {"0x1.", 1.0f},
+      {"0x.8", 0.5f},
+      {"0x1.8", 1.5f},
+      {"-0x1.", -1.0f},
+      {"-0x.8", -0.5f},
+      {"-0x1.8", -1.5f},
 
-    // Examples with a binary exponent and a 'f' suffix.
-    {"0x1.p0f", 1.0f},
-    {"0x.8p2f", 2.0f},
-    {"0x1.8p-1f", 0.75f},
-    {"0x2p-2f", 0.5f},  // No binary point
-    {"-0x1.p0f", -1.0f},
-    {"-0x.8p2f", -2.0f},
-    {"-0x1.8p-1f", -0.75f},
-    {"-0x2p-2f", -0.5f},  // No binary point
-};
+      // Examples with a binary exponent and a 'f' suffix.
+      {"0x1.p0f", 1.0f},
+      {"0x.8p2f", 2.0f},
+      {"0x1.8p-1f", 0.75f},
+      {"0x2p-2f", 0.5f},  // No binary point
+      {"-0x1.p0f", -1.0f},
+      {"-0x.8p2f", -2.0f},
+      {"-0x1.8p-1f", -0.75f},
+      {"-0x2p-2f", -0.5f},  // No binary point
+  };
+}
 INSTANTIATE_TEST_SUITE_P(ParserImplFloatLiteralTest_HexFloat,
                          ParserImplFloatLiteralTest,
-                         testing::ValuesIn(hexfloat_literal_test_cases));
+                         testing::ValuesIn(HexFloatCases()));
+
+// Now test all the same hex float cases, but with 0X instead of 0x
+template <typename ARR>
+std::vector<FloatLiteralTestCase> UpperCase0X(const ARR& cases) {
+  std::vector<FloatLiteralTestCase> result;
+  result.reserve(cases.size());
+  for (const auto& c : cases) {
+    result.emplace_back(c);
+    auto& input = result.back().input;
+    const auto where = input.find("0x");
+    if (where != std::string::npos) {
+      input[where+1] = 'X';
+    }
+  }
+  return result;
+}
+
+using UpperCase0XTest = ::testing::Test;
+TEST_F(UpperCase0XTest, Samples) {
+  const auto cases = FloatLiteralTestCaseList{
+      {"absent", 0.0}, {"0x", 1.0},      {"0X", 2.0},      {"-0x", 3.0},
+      {"-0X", 4.0},    {"  0x1p1", 5.0}, {"  -0x1p", 6.0}, {" examine ", 7.0}};
+  const auto expected = FloatLiteralTestCaseList{
+      {"absent", 0.0}, {"0X", 1.0},      {"0X", 2.0},      {"-0X", 3.0},
+      {"-0X", 4.0},    {"  0X1p1", 5.0}, {"  -0X1p", 6.0}, {" examine ", 7.0}};
+
+  auto result = UpperCase0X(cases);
+  EXPECT_THAT(result, ::testing::ElementsAreArray(expected));
+}
+
+INSTANTIATE_TEST_SUITE_P(ParserImplFloatLiteralTest_HexFloat_UpperCase0X,
+                         ParserImplFloatLiteralTest,
+                         testing::ValuesIn(UpperCase0X(HexFloatCases())));
 
 struct InvalidLiteralTestCase {
   const char* input;