unicode : add opt-in NFC normalization for Unigram tokenizers (#30390)

* unicode : add opt-in NFC normalization for Unigram tokenizers

Note: used help from Codex, Sol 6.1 Medium

* Fixing initial bot comments + linter error (using existing out instead of print)

* removed test_nfc_tokenizer
This commit is contained in:
Sandro Hanea authored and GitHub committed 2026-10-14 08:33:24 +03:00
1 parent eca5458ce0
commit 10da8c463f
12 files changed
+4165 -7

No files matched your search

+86
View File
@@ -1279,6 +1279,92 @@ std::vector<uint32_t> unicode_cpts_normalize_nfd(const std::vector<uint32_t> & c
return result;
}
static uint8_t unicode_canonical_class(uint32_t cpt) {
const auto it = unicode_combining_class.find(cpt);
return it == unicode_combining_class.end() ? 0 : it->second;
}
static void unicode_canonical_decompose(uint32_t cpt, std::vector<uint32_t> & result, std::vector<uint32_t> & pending) {
pending.push_back(cpt);
while (!pending.empty()) {
cpt = pending.back();
pending.pop_back();
// Hangul syllables use algorithmic decomposition (UAX #15).
if (cpt >= 0xAC00 && cpt < 0xAC00 + 11172) {
const uint32_t index = cpt - 0xAC00;
result.push_back(0x1100 + index / 588);
result.push_back(0x1161 + (index % 588) / 28);
if (index % 28) {
result.push_back(0x11A7 + index % 28);
}
continue;
}
const auto it = unicode_decomposition.find(cpt);
if (it == unicode_decomposition.end()) {
result.push_back(cpt);
continue;
}
if (it->second.second) {
pending.push_back(it->second.second);
}
pending.push_back(it->second.first);
}
}
static uint32_t unicode_canonical_compose(uint32_t first, uint32_t second) {
if (first >= 0x1100 && first < 0x1100 + 19 && second >= 0x1161 && second < 0x1161 + 21) {
return 0xAC00 + ((first - 0x1100) * 21 + second - 0x1161) * 28;
}
if (first >= 0xAC00 && first < 0xAC00 + 11172 && (first - 0xAC00) % 28 == 0 && second > 0x11A7 && second < 0x11A7 + 28) {
return first + second - 0x11A7;
}
const auto it = unicode_composition.find((uint64_t(first) << 32) | second);
return it == unicode_composition.end() ? 0 : it->second;
}
std::vector<uint32_t> unicode_cpts_normalize_nfc(const std::vector<uint32_t> & cpts) {
std::vector<uint32_t> decomposed;
decomposed.reserve(cpts.size());
std::vector<uint32_t> pending;
for (const uint32_t cpt : cpts) {
unicode_canonical_decompose(cpt, decomposed, pending);
}
// Sort each run of combining marks; equal classes retain their order.
for (size_t i = 0; i < decomposed.size();) {
if (unicode_canonical_class(decomposed[i]) == 0) {
++i;
}
const size_t begin = i;
while (i < decomposed.size() && unicode_canonical_class(decomposed[i]) != 0) {
++i;
}
std::stable_sort(decomposed.begin() + begin, decomposed.begin() + i, [](uint32_t a, uint32_t b) {
return unicode_canonical_class(a) < unicode_canonical_class(b);
});
}
std::vector<uint32_t> result;
result.reserve(decomposed.size());
size_t starter = 0;
uint8_t previous_class = 0;
for (const uint32_t cpt : decomposed) {
const uint8_t current_class = unicode_canonical_class(cpt);
const uint32_t composite = !result.empty() && (previous_class == 0 || previous_class < current_class)
? unicode_canonical_compose(result[starter], cpt) : 0;
if (composite) {
result[starter] = composite;
} else {
if (current_class == 0) {
starter = result.size();
}
result.push_back(cpt);
previous_class = current_class;
}
}
return result;
}
std::vector<uint32_t> unicode_cpts_from_utf8(const std::string & utf8) {
std::vector<uint32_t> result;
result.reserve(utf8.size());