Skip to content

Commit 2c433a5

Browse files
Escape NFC_Quick_Check=Maybe characters in escape_debug_ext
1 parent 159966c commit 2c433a5

12 files changed

Lines changed: 144 additions & 12 deletions

File tree

library/alloctests/tests/str.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1145,6 +1145,9 @@ fn test_escape_debug() {
11451145
);
11461146
assert_eq!(\u{0340}".escape_debug().to_string(), \\u{0340}");
11471147
assert_eq!("\u{0149}".escape_debug().to_string(), "\\u{0149}");
1148+
assert_eq!("\u{1100}\u{1161}".escape_debug().to_string(), "\u{1100}\\u{1161}");
1149+
assert_eq!("\u{1100}\u{11A8}".escape_debug().to_string(), "\u{1100}\\u{11A8}");
1150+
assert_eq!("\u{16D4C}\u{16D67}".escape_debug().to_string(), "\u{16D4C}\\u{16D67}");
11481151
}
11491152

11501153
#[test]

library/core/src/char/methods.rs

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -492,10 +492,15 @@ impl char {
492492
_ if self.is_control()
493493
|| self.is_private_use()
494494
|| self.is_whitespace()
495-
|| args.escape_grapheme_extender && self.is_grapheme_extender()
495+
|| args.escape_grapheme_extender_and_maybe_not_nfc
496+
&& self.is_grapheme_extender()
496497
|| self.is_default_ignorable()
497498
|| self.is_format_control()
498-
|| self.is_full_composition_exclusion()
499+
|| match self.nfc_quick_check() {
500+
QuickCheckResult::No => true,
501+
QuickCheckResult::Maybe => args.escape_grapheme_extender_and_maybe_not_nfc,
502+
QuickCheckResult::Yes => false,
503+
}
499504
|| self.is_deprecated()
500505
|| self.is_unassigned() =>
501506
{
@@ -1368,6 +1373,39 @@ impl char {
13681373
!self.is_ascii() && unicode::Deprecated(self)
13691374
}
13701375

1376+
/// Returns the value of the `NFC_Quick_Check` property for this character.
1377+
///
1378+
/// For a character `c`, this method returns:
1379+
///
1380+
/// - [`QuickCheckResult::No`] if `c.is_full_composition_exclusion()`.
1381+
/// These characters cannot ever occur in an NFC-[normalized] string.
1382+
/// - [`QuickCheckResult::Maybe`] for characters that may occur in an NFC-[normalized] string,
1383+
/// but may also be removed from a string by NFC [normalization], depending on context.
1384+
/// - [`QuickCheckResult::Yes`] for all other characters.
1385+
///
1386+
/// [normalized]: https://www.unicode.org/faq/normalization.html
1387+
/// [normalization]: https://www.unicode.org/faq/normalization.html
1388+
///
1389+
/// `NFC_Quick_Check` is [described] in Annex #15 ("Unicode Normalization Forms") of the Unicode Standard,
1390+
/// and [specified] in the Unicode Character Database [`DerivedNormalizationProps.txt`].
1391+
///
1392+
/// [described]: https://www.unicode.org/reports/tr15/#Detecting_Normalization_Forms
1393+
/// [specified]: https://www.unicode.org/reports/tr44/#NFC_Quick_Check
1394+
/// [`DerivedNormalizationProps.txt`]: https://www.unicode.org/Public/UCD/latest/ucd/DerivedNormalizationProps.txt
1395+
#[must_use]
1396+
#[inline]
1397+
fn nfc_quick_check(self) -> QuickCheckResult {
1398+
if self.is_ascii() {
1399+
QuickCheckResult::Yes
1400+
} else if self.is_full_composition_exclusion() {
1401+
QuickCheckResult::No
1402+
} else if unicode::NFC_QC_Maybe(self) {
1403+
QuickCheckResult::Maybe
1404+
} else {
1405+
QuickCheckResult::Yes
1406+
}
1407+
}
1408+
13711409
/// Returns an iterator that yields the lowercase mapping of this `char` as one or more
13721410
/// `char`s.
13731411
///
@@ -2496,8 +2534,8 @@ impl char {
24962534
}
24972535

24982536
pub(crate) struct EscapeDebugExtArgs {
2499-
/// Escape Grapheme Extender codepoints?
2500-
pub(crate) escape_grapheme_extender: bool,
2537+
/// Escape codepoints with `Grapheme_Extend` or `NFC_Quick_Check=Maybe`?
2538+
pub(crate) escape_grapheme_extender_and_maybe_not_nfc: bool,
25012539

25022540
/// Escape single quotes?
25032541
pub(crate) escape_single_quote: bool,
@@ -2508,7 +2546,7 @@ pub(crate) struct EscapeDebugExtArgs {
25082546

25092547
impl EscapeDebugExtArgs {
25102548
pub(crate) const ESCAPE_ALL: Self = Self {
2511-
escape_grapheme_extender: true,
2549+
escape_grapheme_extender_and_maybe_not_nfc: true,
25122550
escape_single_quote: true,
25132551
escape_double_quote: true,
25142552
};

library/core/src/char/mod.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -674,3 +674,11 @@ pub enum CharCase {
674674
/// Uppercase. Corresponds to the `Uppercase` Unicode property.
675675
Upper = 0b11,
676676
}
677+
678+
/// Returned by [`char::nfc_quick_check`].
679+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
680+
enum QuickCheckResult {
681+
No,
682+
Maybe,
683+
Yes,
684+
}

library/core/src/fmt/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2943,7 +2943,7 @@ impl Debug for str {
29432943
let mut chars = rest.chars();
29442944
if let Some(c) = chars.next() {
29452945
let esc = c.escape_debug_ext(EscapeDebugExtArgs {
2946-
escape_grapheme_extender: true,
2946+
escape_grapheme_extender_and_maybe_not_nfc: true,
29472947
escape_single_quote: false,
29482948
escape_double_quote: true,
29492949
});
@@ -2975,7 +2975,7 @@ impl Debug for char {
29752975
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
29762976
f.write_char('\'')?;
29772977
let esc = self.escape_debug_ext(EscapeDebugExtArgs {
2978-
escape_grapheme_extender: true,
2978+
escape_grapheme_extender_and_maybe_not_nfc: true,
29792979
escape_single_quote: true,
29802980
escape_double_quote: false,
29812981
});

library/core/src/str/lossy.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ impl fmt::Debug for Debug<'_> {
123123
let mut from = 0;
124124
for (i, c) in valid.char_indices() {
125125
let esc = c.escape_debug_ext(EscapeDebugExtArgs {
126-
escape_grapheme_extender: true,
126+
escape_grapheme_extender_and_maybe_not_nfc: true,
127127
escape_single_quote: false,
128128
escape_double_quote: true,
129129
});

library/core/src/str/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3263,7 +3263,7 @@ impl_fn_for_zst! {
32633263
#[derive(Clone)]
32643264
struct CharEscapeDebugContinue impl Fn = |c: char| -> char::EscapeDebug {
32653265
c.escape_debug_ext(EscapeDebugExtArgs {
3266-
escape_grapheme_extender: false,
3266+
escape_grapheme_extender_and_maybe_not_nfc: false,
32673267
escape_single_quote: true,
32683268
escape_double_quote: true
32693269
})

library/core/src/unicode/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ pub(crate) use unicode_data::grapheme_extend::lookup as Grapheme_Extend;
1717
pub(crate) use unicode_data::lowercase::lookup as Lowercase;
1818
pub(crate) use unicode_data::lt::lookup as Lt;
1919
pub(crate) use unicode_data::n::lookup as N;
20+
pub(crate) use unicode_data::nfc_qc_maybe::lookup as NFC_QC_Maybe;
2021
pub(crate) use unicode_data::uppercase::lookup as Uppercase;
2122
pub(crate) use unicode_data::white_space::lookup as White_Space;
2223

library/core/src/unicode/unicode_data.rs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@
1010
// Lowercase : 943 bytes, 2569 codepoints in 676 ranges (U+0000AA - U+01E944) using bitset
1111
// Lt : 33 bytes, 31 codepoints in 10 ranges (U+0001C5 - U+001FFD) using skiplist
1212
// N : 463 bytes, 1914 codepoints in 145 ranges (U+0000B2 - U+01FBFA) using skiplist
13+
// NFC_QC_Maybe : 155 bytes, 132 codepoints in 49 ranges (U+000300 - U+016D69) using skiplist
1314
// Uppercase : 799 bytes, 1980 codepoints in 659 ranges (U+0000C0 - U+01F18A) using bitset
1415
// White_Space : 256 bytes, 19 codepoints in 8 ranges (U+000085 - U+003001) using cascading
1516
// to_lower : 1112 bytes, 1462 codepoints in 185 ranges (U+0000C0 - U+01E921) using 2-level LUT
1617
// to_upper : 1998 bytes, 1554 codepoints in 299 ranges (U+0000B5 - U+01E943) using 2-level LUT
1718
// to_title : 340 bytes, 135 codepoints in 49 ranges (U+0000DF - U+00FB17) using 2-level LUT
1819
// to_casefold : 32 bytes, 174 codepoints in 5 ranges (U+000131 - U+00ABBF) using 2-level LUT
19-
// Total : 11756 bytes
20+
// Total : 11911 bytes
2021

2122
#[inline(always)]
2223
const fn bitset_search<
@@ -889,6 +890,47 @@ pub mod n {
889890
}
890891
}
891892

893+
#[rustfmt::skip]
894+
pub mod nfc_qc_maybe {
895+
use super::ShortOffsetRunHeader;
896+
897+
static SHORT_OFFSET_RUNS: [ShortOffsetRunHeader; 14] = [
898+
ShortOffsetRunHeader::new(0, 768), ShortOffsetRunHeader::new(1, 1619),
899+
ShortOffsetRunHeader::new(25, 2364), ShortOffsetRunHeader::new(27, 2878),
900+
ShortOffsetRunHeader::new(33, 4142), ShortOffsetRunHeader::new(57, 4449),
901+
ShortOffsetRunHeader::new(59, 6965), ShortOffsetRunHeader::new(63, 12441),
902+
ShortOffsetRunHeader::new(65, 69818), ShortOffsetRunHeader::new(67, 70462),
903+
ShortOffsetRunHeader::new(71, 71984), ShortOffsetRunHeader::new(93, 90398),
904+
ShortOffsetRunHeader::new(95, 93543), ShortOffsetRunHeader::new(97, 1207657),
905+
];
906+
static OFFSETS: [u8; 99] = [
907+
0, 5, 1, 7, 2, 1, 1, 1, 1, 2, 6, 1, 7, 6, 4, 2, 1, 2, 6, 1, 9, 1, 2, 1, 0, 3, 0, 1, 129, 1,
908+
24, 1, 0, 1, 23, 2, 102, 1, 24, 1, 126, 1, 107, 1, 18, 2, 103, 1, 24, 1, 114, 1, 4, 1, 15,
909+
1, 0, 1, 0, 21, 50, 27, 0, 1, 0, 2, 0, 1, 108, 1, 0, 1, 24, 1, 96, 1, 2, 1, 6, 1, 2, 1, 1,
910+
3, 230, 1, 9, 1, 2, 1, 241, 1, 0, 1, 0, 12, 0, 2, 0,
911+
];
912+
#[inline]
913+
pub fn lookup(c: char) -> bool {
914+
debug_assert!(!c.is_ascii());
915+
(c as u32) >= 0x300 && lookup_slow(c)
916+
}
917+
918+
#[inline(never)]
919+
fn lookup_slow(c: char) -> bool {
920+
const {
921+
assert!(SHORT_OFFSET_RUNS.last().unwrap().0 > char::MAX as u32);
922+
let mut i = 0;
923+
while i < SHORT_OFFSET_RUNS.len() {
924+
assert!(SHORT_OFFSET_RUNS[i].start_index() < OFFSETS.len());
925+
i += 1;
926+
}
927+
}
928+
// SAFETY: We just ensured the last element of `SHORT_OFFSET_RUNS` is greater than `std::char::MAX`
929+
// and the start indices of all elements in `SHORT_OFFSET_RUNS` are smaller than `OFFSETS.len()`.
930+
unsafe { super::skip_search(c, &SHORT_OFFSET_RUNS, &OFFSETS) }
931+
}
932+
}
933+
892934
#[rustfmt::skip]
893935
pub mod uppercase {
894936
static BITSET_CHUNKS_MAP: [u8; 125] = [

library/core/src/wtf8.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ impl fmt::Debug for Wtf8 {
147147
use crate::fmt::Write as _;
148148
for c in s.chars().flat_map(|c| {
149149
c.escape_debug_ext(EscapeDebugExtArgs {
150-
escape_grapheme_extender: true,
150+
escape_grapheme_extender_and_maybe_not_nfc: true,
151151
escape_single_quote: false,
152152
escape_double_quote: true,
153153
})

library/coretests/tests/unicode.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,12 @@ fn n() {
122122
test_boolean_property(test_data::N, char::is_numeric);
123123
}
124124

125+
#[test]
126+
#[cfg_attr(miri, ignore)] // Miri is too slow
127+
fn nfc_qc_maybe() {
128+
test_boolean_property(test_data::NFC_QC_MAYBE, unicode_data::nfc_qc_maybe::lookup);
129+
}
130+
125131
#[test]
126132
#[cfg_attr(miri, ignore)] // Miri is too slow
127133
fn uppercase() {

0 commit comments

Comments
 (0)