mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 18:59:04 +08:00
* Move str casing into rustpython-unicode::case and drop icu from vm
Add code-point simple mappings (simple_lowercase/uppercase/titlecase/fold),
casing predicates (is_lowercase/is_uppercase/is_titlecase/is_cased/
is_case_ignorable), and the string-level capitalize/title/swapcase/casefold
helpers to crates/unicode/src/case.rs, moving the titlecase segmentation and
final-sigma logic out of vm/builtins/str.rs verbatim. str.rs and anystr.rs now
call through the crate; the is_cased kernel takes plain fn(char)->bool
predicates instead of icu BinaryProperty generics.
Remove icu_casemap/icu_locale/icu_properties/writeable from crates/vm and the
now-unused VecFmtWriter helper. crates/unicode is the only workspace member
with a direct icu dependency. str.lower/upper keep using Wtf8::to_lowercase/
to_uppercase.
Assisted-by: Claude
* Route sre is_uni_space through unicode::classify::is_space
SRE_UNI_IS_SPACE is Py_UNICODE_ISSPACE. Replace the hand-rolled BMP code-point
list with classify::is_space, which is differential-tested against CPython. A
full-range sweep confirms the two agree on every code point.
Assisted-by: Claude
* Use simple case mappings and the Cased property for sre IGNORECASE
lower_unicode/upper_unicode took the first char of the full case mapping, so
code points with a full mapping but no simple one were miscased (e.g.
upper_unicode('ß') returned 'S'). Route them through case::simple_lowercase/
simple_uppercase, matching Py_UNICODE_TOLOWER/TOUPPER.
_sre.unicode_iscased derived casedness from those mappings, which only held
while the full mapping was used; with simple mappings a cased code point that
maps to itself (e.g. the ſt/st ligatures, U+FB05/06) read as uncased and lost its
_casefix equivalence. Query the Cased property directly via case::is_cased.
Assisted-by: Claude
* Sweep casing predicates and simple lowercase mapping against CPython
Extend the differential harness to cover is_lowercase/is_uppercase/is_titlecase/
is_cased over the full scalar range, sourced from str.islower/isupper, the Lt
category, and _sre.unicode_iscased. Add a parallel sweep of the simple lowercase
mapping (Py_UNICODE_TOLOWER via _sre.unicode_tolower) with its own version-skew
allow-list; CPython exposes no simple-uppercase oracle, so toupper stays on the
SRE unit tests.
Record the U+0295 Ll->Lo recategorization (Unicode 16.0.0 -> 17.0.0) as a known
reverse-direction divergence so the skew regenerator still rejects genuine
regressions.
Assisted-by: Claude
115 lines
4.0 KiB
Python
115 lines
4.0 KiB
Python
#!/usr/bin/env python3.14
|
|
"""Generate the CPython reference dataset for the differential Unicode sweep.
|
|
|
|
Run with a CPython interpreter whose ``unicodedata.unidata_version`` matches the
|
|
Unicode release this crate targets (16.0.0 for CPython 3.14). The output is a
|
|
compact run-length encoding of every predicate's true-set over the full scalar
|
|
range, consumed by ``tests/differential.rs``.
|
|
|
|
Usage:
|
|
python3.14 crates/unicode/tests/generate_reference.py
|
|
|
|
Writes ``tests/data/cpython3.14_predicates.txt``. Commit the result.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import _sre
|
|
import pathlib
|
|
import sys
|
|
import unicodedata
|
|
|
|
MAX = 0x110000
|
|
|
|
# str predicates: name -> single-char method.
|
|
STR_PREDICATES = {
|
|
"isalpha": str.isalpha,
|
|
"isalnum": str.isalnum,
|
|
"isdecimal": str.isdecimal,
|
|
"isdigit": str.isdigit,
|
|
"isnumeric": str.isnumeric,
|
|
"isspace": str.isspace,
|
|
"isprintable": str.isprintable,
|
|
"isidentifier": str.isidentifier,
|
|
}
|
|
|
|
# Casing predicates, keyed by the crate function each one exercises. Each is
|
|
# sourced from the exact property that function computes:
|
|
# * is_lowercase / is_uppercase mirror Py_UNICODE_ISLOWER / ISUPPER, which for a
|
|
# single character are str.islower() / str.isupper().
|
|
# * is_titlecase is the Lt general category (NOT str.istitle(), which also
|
|
# reports plain uppercase letters as titlecased).
|
|
# * is_cased is the Cased property (Py_UNICODE_ISCASED), via _sre.
|
|
CASE_PREDICATES = {
|
|
"is_lowercase": lambda c: c.islower(),
|
|
"is_uppercase": lambda c: c.isupper(),
|
|
"is_titlecase": lambda c: unicodedata.category(c) == "Lt",
|
|
"is_cased": lambda c: _sre.unicode_iscased(ord(c)),
|
|
}
|
|
|
|
# Simple one-to-one lowercase mapping (Py_UNICODE_TOLOWER via _sre). This is the
|
|
# mapping the regex IGNORECASE path depends on. Emitted as `cp:mapping,...` for
|
|
# code points that map to something other than themselves. CPython exposes no
|
|
# Python-level simple-uppercase oracle (_sre has unicode_tolower only), so
|
|
# toupper is left to the SRE unit tests.
|
|
CASE_MAPPINGS = {
|
|
"tolower": _sre.unicode_tolower,
|
|
}
|
|
|
|
|
|
def encode_ranges(is_true) -> list[tuple[int, int]]:
|
|
"""Collapse the true-set of ``is_true`` into inclusive ``[start, end]`` runs."""
|
|
ranges: list[tuple[int, int]] = []
|
|
start: int | None = None
|
|
for cp in range(MAX):
|
|
if is_true(cp):
|
|
if start is None:
|
|
start = cp
|
|
elif start is not None:
|
|
ranges.append((start, cp - 1))
|
|
start = None
|
|
if start is not None:
|
|
ranges.append((start, MAX - 1))
|
|
return ranges
|
|
|
|
|
|
def main() -> int:
|
|
if unicodedata.unidata_version != "16.0.0":
|
|
sys.stderr.write(
|
|
f"warning: unidata_version is {unicodedata.unidata_version}, "
|
|
"expected 16.0.0 (CPython 3.14); regenerating anyway\n"
|
|
)
|
|
|
|
data = pathlib.Path(__file__).parent / "data"
|
|
data.mkdir(parents=True, exist_ok=True)
|
|
|
|
lines = [f"# unidata_version {unicodedata.unidata_version}"]
|
|
for name, method in STR_PREDICATES.items():
|
|
ranges = encode_ranges(lambda cp, m=method: m(chr(cp)))
|
|
packed = ",".join(f"{s:X}:{e:X}" for s, e in ranges)
|
|
lines.append(f"{name} {packed}")
|
|
for name, method in CASE_PREDICATES.items():
|
|
ranges = encode_ranges(lambda cp, m=method: m(chr(cp)))
|
|
packed = ",".join(f"{s:X}:{e:X}" for s, e in ranges)
|
|
lines.append(f"{name} {packed}")
|
|
|
|
predicates = data / "cpython3.14_predicates.txt"
|
|
predicates.write_text("\n".join(lines) + "\n")
|
|
print(f"wrote {predicates} ({predicates.stat().st_size} bytes)")
|
|
|
|
mapping_lines = [f"# unidata_version {unicodedata.unidata_version}"]
|
|
for name, method in CASE_MAPPINGS.items():
|
|
pairs = [
|
|
f"{cp:X}:{mapped:X}" for cp in range(MAX) if (mapped := method(cp)) != cp
|
|
]
|
|
mapping_lines.append(f"{name} {','.join(pairs)}")
|
|
|
|
mappings = data / "cpython3.14_mappings.txt"
|
|
mappings.write_text("\n".join(mapping_lines) + "\n")
|
|
print(f"wrote {mappings} ({mappings.stat().st_size} bytes)")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|