* sre_engine: document that StrDrive positions are character indices
StrDrive carried no documentation, so nothing recorded that a cursor's
position is a character index rather than a byte offset. Every implementation
satisfies it -- skip(n) advances position by exactly n in all three -- and the
engine depends on it: _count bounds a repeat with position + max_count and
reports the repeat length as a difference of positions, ASSERT compares
position against a lookbehind width, and search_info recovers a match start as
position - (len - 1). A drive over a variable-width encoding that stored byte
offsets would leave those type-correct and wrong, and would index a lookbehind
out of bounds.
Write the invariant down on StringCursor and on each trait method.
Also walk the group's own cursor in GROUPREF instead of counting to the
group's width, so the loop bound is the thing being stepped. Generated
machine code is unchanged.
Assisted-by: Claude
* _sre: drive an all-ASCII str subject over its bytes
with_sre_str handed every str subject to the &Wtf8 drive, which answers
count() by counting every code point and create_cursor(n) by stepping over the
first n. Both run once per Request, so a scan that restarts at successive
positions walked the subject again on every call: finditer over an ASCII
subject of n tokens was quadratic in n.
PyStr already records whether it is ASCII -- StrKind is decided when the
string is built -- and for ASCII a character index is a byte index, so the
&[u8] drive's cursor arithmetic already applies: count() is the byte length
and create_cursor() is a pointer offset. Add AsciiStr, which delegates every
StrDrive method to that impl and differs only in slice(), which reslices the
span and returns str rather than bytes.
Matching is unaffected: StrDrive carries no unicode semantics, because the
engine keys every unicode decision on the compiled pattern's opcode rather
than on the subject type.
Also bind the subject once in with_sre_str, so callers passing a temporary
(`&x.clone()`) build it once rather than per arm.
finditer over an ASCII subject, n tokens, this machine:
n before after
5000 41.60ms 1.37ms
10000 217.30ms 2.68ms
20000 1206.28ms 5.17ms
40000 5016.65ms 10.31ms
Per-doubling x5.22/x5.55/x4.16 becomes x1.95/x1.93/x1.99. Collecting
m.group(0) for every match goes 5061.02ms -> 17.80ms at n=40000.
test.test_re is unchanged at 166 tests, OK (skipped=14, expected failures=6),
and a 2731-line differential over the is_ascii() boundary -- findall,
finditer spans and groups, sub, split, match, search, fullmatch across
subjects that are empty, ASCII, non-ASCII, or mixed -- is byte-identical to
CPython 3.14.6. Introducing an off-by-one in AsciiStr::slice moves 1224 of
those lines, so the comparison reaches the new code.
Assisted-by: Claude
`search_info_literal`'s len>1 arm reset the tail-match cursor by advancing one
character past the matched prefix. That position equals the INFO block's
prefix_skip boundary only when the prefix ends where the skip does; for a
pattern like `ab(cd)`, `_get_literal_prefix` reports prefix_len=4 with
prefix_skip=2, so the tail resumed two characters too far.
`state.cursor` already holds `req.start + skip`, which is what
`sre_lib.h` SRE(search) computes as `ptr - (prefix_len - prefix_skip - 1)`,
and which the len==1 arm above already uses. Take it unconditionally.
Searching "xabcdcd" for `ab(cd)` returned span (1, 7) instead of (1, 5);
over a 600-case corpus stamped with CPython's own answers, 94 cases disagreed
before this change and none after. The wrong resume also dropped matches
outright, not only widened them.
Assisted-by: Claude
* Fix SymbolUsge::Iter
* fix os.rename
* fix sre
* Re-export rename from crate::posix on Windows
posix_windows.rs is mounted as the `posix` module on Windows, so
`crate::posix_windows` does not resolve.
Assisted-by: Claude
* Apply rustfmt to sre_engine tests
Assisted-by: Claude
* Update test expectations for the sre and tokenize fixes
- test_re: drop expectedFailure from test_word_boundaries,
test_possessive_quantifiers and test_bug_gh101955
- test_inspect, test_pydoc: drop expectedFailure from the signature
tests that now pass
- test_pdb: drop expectedFailure from the two file-modification tests
and +EXPECTED_FAILURE from the test_post_mortem_chained and
test_pdb_asynctask doctests
- test_unittest: mark test_autospec_on_bound_builtin_function as
expectedFailure; inspect.signature() now succeeds on time.ctime
because of its auto-generated __text_signature__
Assisted-by: Claude
* 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
* Add round-half-even hex float parser to common float_ops
Add float_ops::from_hex returning Result<f64, HexFloatError>, which parses
the coefficient and exponent, rounds round-half-even over the full exponent
range, and detects overflow. Route float.fromhex through it: raise
OverflowError for values too large to represent and ValueError for invalid
input. Remove the expectedFailure on test_from_hex.
Assisted-by: Claude
* Address review: Option-returning byte helpers, const fns, drop test_ prefix
Assisted-by: Claude
* Whitelist fdigits and inity in cspell
Assisted-by: Claude
* Home is_py_ascii_whitespace in rustpython-wtf8; share with common and sre_engine
Assisted-by: Claude
* Use shared wtf8 is_py_ascii_whitespace in bytes_inner
Remove the private copy of is_py_ascii_whitespace in bytes_inner.rs and
import the shared rustpython_common::wtf8::is_py_ascii_whitespace.
Assisted-by: Claude
* Extract unicodedata core into rustpython-unicode crate
Move the vm-free Unicode character database access — the generated UCD 3.2.0 /
latest tables, their build-time generation, and the icu4x/unicode_names2 lookups
for category/bidirectional/combining/east_asian_width/mirrored/decomposition/
normalize/is_normalized/digit/decimal/numeric/name/lookup — into a new leaf crate
over char/CodePoint/&Wtf8. stdlib/unicodedata.rs keeps the UCD pyclass binding: it
extracts the code point, boxes results, and maps errors. The unicode data files,
build.rs table generation, and the icu_properties/icu_normalizer/unicode_names2
dependencies move out of stdlib.
Assisted-by: Claude
* Route str predicates, casefold, and printable through rustpython-unicode
Add classify/case/identifier modules to the shared crate over char/&str/&Wtf8:
isalpha/isalnum/isdecimal/isdigit/isnumeric/isspace/isprintable classification,
XID identifier predicates, and full-mapping casefold. vm/builtins/str.rs and
literal/char.rs now call these instead of icu_properties/icu_casemap directly.
String-level iteration with final-sigma handling (lower/upper/title/capitalize,
islower/isupper) stays in the runtime.
Assisted-by: Claude
* Route sre_engine char classes through rustpython-unicode
Add the regex module with the SRE character-class and case predicates
(is_word/is_space/is_digit, is_uni_* Unicode variants, ascii/locale/unicode case
folding). sre_engine/string.rs now forwards to it, keeping its public API and
behavior identical — including the ASCII-only is_uni_digit and the hardcoded
is_uni_space table.
Assisted-by: Claude
* Route \N{} name lookups through rustpython-unicode
codegen/string_parser.rs (\N{...} escapes) and common/encodings.rs (the
namereplace error handler) now resolve character names via rustpython-unicode
instead of depending on unicode_names2 directly, leaving the crate as the sole
owner of the name database.
Assisted-by: Claude
* Add differential Unicode sweep and shared-crate snippet test
tests/differential.rs sweeps the full 0..0x110000 range and compares each str
classification predicate against a committed CPython 3.14 reference dataset
(tests/data/cpython3.14_predicates.txt, produced by generate_reference.py). Code
points that differ only because the Rust std / icu4x build ships a later Unicode
release than CPython 3.14's 16.0.0 are recorded in
tests/data/version_skew_cpython3.14.txt, regenerable via
RUSTPYTHON_UNICODE_REGEN_SKEW=1; the regen refuses to record any
cpython=true/crate=false divergence, so only newly-assigned code points are
allowed. Any other divergence fails. Both data files use a run-length
`predicate start:end,...` encoding.
extra_tests/snippets/stdlib_unicode_shared.py exercises the routed surface end to
end (str predicates, casefold, identifiers, unicodedata, normalize, \N{}, and re
character classes) and passes identically on CPython 3.14 and RustPython.
Assisted-by: Claude
* Match regex \d to Unicode decimal digits
is_uni_digit previously matched only ASCII 0-9, so re's \d in Unicode mode
missed decimal digits like ٥ and ५. SRE_UNI_IS_DIGIT matches Py_UNICODE_ISDECIMAL
(category Nd), so route it through classify::is_decimal. Unmasks test_bug_6561 in
test_re and extends the shared-crate snippet with the Nd/Nl/No cases.
Assisted-by: Claude
* Replace shallow Unicode wrappers with re-exports
Address review feedback on the extraction.
Functions that only forwarded to the shared crate become `use ... as`
re-exports rather than hand-written wrappers:
- literal: is_printable is dropped; escape.rs calls
rustpython_unicode::classify::is_repr_printable directly.
- unicode::identifier::is_continue re-exports is_xid_continue.
- unicode::data::lookup_character re-exports unicode_names2::character.
- Drop the unused repr(u8) on DecompositionType.
The SRE character-class and case predicates move back into
sre_engine::string (their pre-extraction home) instead of living in a
unicode::regex module that only sre_engine and the vm _sre binding used;
is_uni_digit/is_uni_alnum still delegate to rustpython_unicode::classify.
engine.rs and _sre consume them from sre_engine::string again.
The unicode crate's tests move from a single root tests module in lib.rs
into per-module test submodules (case, classify, data, identifier,
normalize).
Assisted-by: Claude
* no-std
* Address review feedback: test module, dead variant, WTF-8 normalization
- differential.rs: move the sweep tests into a `mod tests` block and drop
the file-level allow of clippy::tests_outside_test_module and
std_instead_of_alloc; the test uses alloc collections instead.
- data.rs: remove the never-constructed DecompositionType::Canonical variant
and its allow(unused); compatibility decomposition never produces it and
canonical decomposition is handled through icu4x.
- normalize.rs: is_normalized now takes &Wtf8 and checks each UTF-8 run,
skipping lone surrogates, matching normalize's run-wise behavior.
- unicodedata.rs: pass as_wtf8() to is_normalized.
Assisted-by: Claude
Rust and Python differ in which properties they use for alphanumeric,
numeric, et cetera. Both languages list which properties are used which
makes it easy to mimic Python's behavior in Rust.
My previous patch was a bit shortsighted because I filtered out
combining characters from is_alphanumeric. Using properties is exact and
also much cleaner. It also covers edge cases that my initial approach
missed.
Besides isalnum, I also fixed isnumeric and isdigit in the same way by
using properties.
* fix: Python-Rust combining char diff in isalnum
Related to: #7518
Rust and Python differ on alphanumeric characters. Rust follows the
Unicode standard closer than Python. This means that is_alphanumeric
(char function in Rust) is different from isalnum (Python). To fix the
discrepancy, RustPython needs to mimic Python by rejecting certain
characters. Some classes of combining characters count as alphanumeric
in Rust but not Python. Combining characters are accent marks
that are combined with other characters to create a single grapheme.
It's possible that this PR is not exhaustive. I fixed the combining
character issue BUT I don't know the full range of discrepancies.
* fix: Ignore combining characters in SRE
Closes: #7518
* Updated re library + test
* Copied over generate_sre_constants from cpython/Tools
* Customized `generate_sre_constants.py` + ran to update `constants.rs`
* Clarified `dump_enum` docstring in `generate_sre_constants.py`
* * Added alloc_instead_of_core, std_instead_of_alloc, and std_instead_of_core clippy rules
* Manually changed part of the code to use core/alloc
* use clippy --fix to fix issues in stdlib
* * Used clippy --fix to fix issues in vm
* Imported Range in vm/src/anystr.rs
* * Used clippy --fix to fix issues in common