Skip to content

Fix formatting, numeric, unicode, codec, complex and marshal mismatches found by differential testing - #8600

Merged
youknowone merged 11 commits into
RustPython:mainfrom
youknowone:compiler-cpython-parity
Aug 29, 2026
Merged

Fix formatting, numeric, unicode, codec, complex and marshal mismatches found by differential testing#8600
youknowone merged 11 commits into
RustPython:mainfrom
youknowone:compiler-cpython-parity

Conversation

@youknowone

@youknowone youknowone commented Aug 27, 2026

Copy link
Copy Markdown
Member

Differential testing against CPython 3.14 over generated corpora (numeric literals, format specs, %-formatting, str methods, re, codecs, marshal, complex arithmetic) surfaced the following mismatches. Each commit fixes one area.

crates/common

  • ccee8074d%-formatting of bytes precision, width and %c errors. A bare . precision panicked instead of truncating to zero; width padding underflowed; %r/%a on bytes did not go through ascii(); %c error messages did not name the argument.
  • 5e156f22f — format spec handling of grouping, bool, 'c' and complex. Unknown format types skipped the grouping check; format(True, ...) did not validate the spec against Decimal; 'c' ignored fill/align and did not raise OverflowError for out-of-range ints; complex 'g' handling was wrong.
  • 6ac591664round() at negative ndigits. Divide-then-round loses the exact decimal; now rounds half-to-even from the exact decimal digits at the requested power of ten.
  • 7dc90f4f3int() literal errors. The error message reported the resolved base rather than the requested one for base 0, and a bare '-'/'+' took the old-octal path.
  • a65fbbd2c — whitespace set. split/rsplit/strip defaults and expandtabs now use the Py_UNICODE_ISSPACE set; str[i] gained the "string index out of range" message.

crates/unicode

  • 6dc80a1ba — numeric values as exact rationals. UnicodeData field 8 carries numerator/denominator; these were being read as integers, so unicodedata.numeric() was wrong for every fractional code point. Also picks up the Unicode 3.2 fraction table.

crates/vm

  • fadd4cdc2Pattern.groupindex returns a mappingproxy (and a plain dict when there are no named groups).
  • d3491485d — complex multiplication, division and power. Implements the C11 Annex G.5.1/G.5.2 nan/infinity recovery for * and /, Smith's algorithm for the quotient, the dedicated real-numerator quotient, repeated squaring for integral exponents in [-100, 100], OverflowError("complex exponentiation"), and the real-operand fast paths.
  • 01758138e — interning of code string constants. Was gated on length ≤ 20; now on all_name_chars. Single-character latin-1 strings are interned singletons.
  • 2ad40f3f7marshal version handling. Versions < 2 write 'f'/'x' with 17 significant digits; FLAG_REF/TYPE_INTERNED only from version 3; the short-ascii, ascii and small-tuple forms only from version 4. The three EOF conditions now produce their distinct messages ("EOF read where object expected" / "EOF read where not expected" / "marshal data too short").

Lib/

  • 7a685bc19 — utf-16/32/7 codec error handling. The utf-32 decoder did not call the error handler for code points above 0x10FFFF; encoding names passed to handlers dropped the byte order; a bytes replacement from a handler was mishandled; the utf-16 encoder did not split astral code points into surrogate pairs; the utf-7 handler name was wrong.

Tests

Five @unittest.expectedFailure markers removed (test_complex, test_re, test_codeccallbacks) — they now pass.

Verification

  • workspace clippy with CI flags: clean
  • wasm clippy: clean
  • cargo test: pass
  • CPython suite: 418 tests OK

Residual differences that remain in the sweeps were traced to the CPython build itself and are not fixed here: FMA contraction in its complex division, hash(nan) being id-based, an errno leak in _Py_c_abs, refcount-dependent FLAG_REF in marshal, and the UCD 16 ↔ 17 version gap.

drafted by Claude

Summary by CodeRabbit

  • Bug Fixes

    • Improved Unicode-aware whitespace handling and tab expansion, including support for non-UTF-8 string data.
    • Corrected rounding, byte formatting, integer conversion errors, and sign-only numeric parsing.
    • Improved complex-number arithmetic for infinities, signed zero, powers, and division-by-zero cases.
    • Fixed Unicode numeric values to preserve exact fractions.
  • Compatibility

    • Added support for legacy and version 4 marshal formats with clearer truncated-data errors.
    • Updated regular-expression groupindex behavior to provide read-only mappings for named groups.
    • Improved string constant handling and formatting error messages.

@coderabbitai

coderabbitai Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro Plus

Run ID: 911de195-d733-4f76-b39a-afe5cf716dbc

📥 Commits

Reviewing files that changed from the base of the PR and between ec6fec2 and a5fbe53.

⛔ Files ignored due to path filters (4)
  • Lib/_pycodecs.py is excluded by !Lib/**
  • Lib/test/test_codeccallbacks.py is excluded by !Lib/**
  • Lib/test/test_complex.py is excluded by !Lib/**
  • Lib/test/test_re.py is excluded by !Lib/**
📒 Files selected for processing (16)
  • crates/common/src/cformat.rs
  • crates/common/src/float_ops.rs
  • crates/common/src/format.rs
  • crates/common/src/int.rs
  • crates/common/src/str.rs
  • crates/compiler-core/src/marshal.rs
  • crates/unicode/build.rs
  • crates/unicode/src/data.rs
  • crates/vm/src/builtins/code.rs
  • crates/vm/src/builtins/complex.rs
  • crates/vm/src/builtins/str.rs
  • crates/vm/src/cformat.rs
  • crates/vm/src/format.rs
  • crates/vm/src/stdlib/_sre.rs
  • crates/vm/src/stdlib/marshal.rs
  • crates/vm/src/vm/context.rs

📝 Walkthrough

Walkthrough

Changes

Formatting and numeric behavior

Layer / File(s) Summary
Numeric formatting and rounding
crates/common/src/cformat.rs, crates/common/src/float_ops.rs, crates/common/src/format.rs, crates/vm/src/cformat.rs, crates/vm/src/format.rs
Formatting validates grouping types, rounds negative float digits with decimal half-to-even logic, handles byte precision safely, and reports oversized integer-to-character conversions as OverflowError.
Integer literal validation
crates/common/src/int.rs
Base-0 sign-only literals are rejected, and invalid-literal errors preserve the originally requested base.

String and Unicode data handling

Layer / File(s) Summary
Exact Unicode numeric values
crates/unicode/build.rs, crates/unicode/src/data.rs
Unicode numeric generation parses fractional fields and tests exact values for one-third and one-seventh.
WTF-8 string operations
crates/common/src/str.rs, crates/vm/src/builtins/str.rs
Tab expansion uses WTF-8 buffers, whitespace operations use Unicode classification, offsets use separator lengths, and string indexing handles out-of-range positions.

Marshal formats and read errors

Layer / File(s) Summary
Marshal error classification
crates/compiler-core/src/marshal.rs, crates/vm/src/stdlib/marshal.rs
Marshal readers distinguish unexpected object EOF from data truncated inside a value and expose separate error messages.
Versioned marshal encoding
crates/vm/src/stdlib/marshal.rs
Legacy float and complex encodings, version-4 ASCII encodings, and short-tuple encodings are selected according to the marshal version.

Complex arithmetic and parsing

Layer / File(s) Summary
Special-value arithmetic
crates/vm/src/builtins/complex.rs
Complex multiplication and division recover infinities, signed zeros, and NaNs, while zero divisors produce explicit errors.
Complex exponentiation and literal validation
crates/vm/src/builtins/complex.rs
Small integer powers use repeated squaring, general powers use revised magnitude and angle calculations, and misplaced underscores are rejected.

Object exposure and interning

Layer / File(s) Summary
Name-based string interning
crates/vm/src/builtins/code.rs, crates/vm/src/vm/context.rs
ASCII name-character constants and Latin-1 cache entries are interned through the string pool.
Regex group index exposure
crates/vm/src/stdlib/_sre.rs
Pattern.groupindex returns a plain dictionary when empty and a mapping proxy otherwise.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PythonOperation
  participant ComplexRealBinop
  participant ComplexArithmetic
  participant ErrorMapping
  PythonOperation->>ComplexRealBinop: dispatch multiply or true_divide
  ComplexRealBinop->>ComplexArithmetic: call prod, quot, or rc_quot
  ComplexArithmetic-->>ComplexRealBinop: recovered result or zero-divisor result
  ComplexRealBinop->>ErrorMapping: map zero-divisor result to ZeroDivisionError
Loading

Suggested reviewers: shaharnaveh

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

📦 Library Dependencies

The following Lib/ modules were modified. Here are their dependencies:

[x] lib: cpython/Lib/codecs.py
[x] test: cpython/Lib/test/test_charmapcodec.py
[ ] test: cpython/Lib/test/test_codeccallbacks.py (TODO: 7)
[x] test: cpython/Lib/test/test_codecencodings_cn.py (TODO: 4)
[x] test: cpython/Lib/test/test_codecencodings_hk.py (TODO: 1)
[x] test: cpython/Lib/test/test_codecencodings_iso2022.py (TODO: 5)
[x] test: cpython/Lib/test/test_codecencodings_jp.py (TODO: 7)
[x] test: cpython/Lib/test/test_codecencodings_kr.py (TODO: 3)
[x] test: cpython/Lib/test/test_codecencodings_tw.py (TODO: 1)
[ ] test: cpython/Lib/test/test_codecmaps_cn.py (TODO: 3)
[ ] test: cpython/Lib/test/test_codecmaps_hk.py (TODO: 1)
[ ] test: cpython/Lib/test/test_codecmaps_jp.py (TODO: 6)
[ ] test: cpython/Lib/test/test_codecmaps_kr.py (TODO: 3)
[ ] test: cpython/Lib/test/test_codecmaps_tw.py (TODO: 3)
[ ] test: cpython/Lib/test/test_codecs.py (TODO: 11)
[ ] test: cpython/Lib/test/test_multibytecodec.py (TODO: 1)
[x] test: cpython/Lib/test/testcodec.py

dependencies:

  • codecs

dependent tests: (161 tests)

  • codecs: test_charmapcodec test_codeccallbacks test_codecs test_eof test_exceptions test_importlib test_inspect test_io test_json test_locale test_logging test_multibytecodec test_os test_pdb test_plistlib test_sax test_str test_sys
    • encodings: test_pydoc
      • locale: test__locale test_builtin test_c_locale_coercion test_calendar test_decimal test_float test_format test_re test_regrtest test_strftime test_strptime test_types test_utf8_mode
    • json: test_embed test_pyrepl test_subprocess test_sysconfig test_tomllib test_tools test_traceback test_zoneinfo
      • importlib.metadata: test_importlib
      • multiprocessing.resource_tracker: test_concurrent_futures
    • pickle: test_annotationlib test_argparse test_array test_ast test_asyncio test_bool test_bytes test_bz2 test_collections test_concurrent_futures test_configparser test_coroutines test_csv test_ctypes test_defaultdict test_deque test_descr test_dict test_dictviews test_email test_enum test_enumerate test_fractions test_functools test_generators test_genericalias test_http_cookies test_ipaddress test_iter test_itertools test_list test_lzma test_memoryio test_memoryview test_minidom test_opcache test_operator test_ordered_dict test_pathlib test_pickle test_picklebuffer test_pickletools test_platform test_positional_only_arg test_posix test_random test_range test_set test_shelve test_slice test_socket test_statistics test_string test_structseq test_super test_time test_trace test_tuple test_turtle test_type_aliases test_type_params test_typing test_unittest test_uuid test_xml_dom_minicompat test_xml_etree test_xpickle test_zipfile test_zlib test_zoneinfo
      • tracemalloc: test_tracemalloc
    • plistlib:
      • platform: test__osx_support test_asyncio test_baseexception test_cmath test_ctypes test_fcntl test_math test_mimetypes test_shutil test_ssl test_winreg test_wsgiref
    • tokenize: test_linecache test_peg_generator test_tabnanny test_tokenize test_unparse
      • inspect: test_abc test_asyncgen test_buffer test_clinic test_code test_grammar test_monitoring test_ntpath test_patma test_posixpath test_signal test_sqlite3 test_type_annotations test_yield_from test_zipimport test_zipimport_support
      • linecache: test_bdb
      • traceback: test_asyncio test_code_module test_contextlib test_contextlib_async test_dictcomps test_http_cookiejar test_importlib test_listcomps test_pyexpat test_setcomps test_threadedtempfile test_threading test_unittest test_with

[x] test: cpython/Lib/test/test_complex.py (TODO: 2)

dependencies:

dependent tests: (no tests depend on complex)

[x] lib: cpython/Lib/re
[x] lib: cpython/Lib/sre_compile.py
[x] lib: cpython/Lib/sre_constants.py
[x] lib: cpython/Lib/sre_parse.py
[ ] test: cpython/Lib/test/test_re.py (TODO: 7)
[x] test: cpython/Lib/test/re_tests.py

dependencies:

  • re

dependent tests: (82 tests)

  • re: test_android test_ast test_asyncio test_binascii test_builtin test_bytes test_clinic test_cmd_line test_ctypes test_dict test_dis test_docxmlrpc test_dtrace test_email test_embed test_faulthandler test_filecmp test_fileinput test_fnmatch test_format test_frame test_fstring test_functools test_future_stmt test_generated_cases test_genericalias test_glob test_hashlib test_http_cookiejar test_httplib test_httpservers test_imaplib test_importlib test_inspect test_ipaddress test_launcher test_logging test_mailbox test_mmap test_optparse test_ordered_dict test_platform test_pprint test_pydoc test_pyexpat test_pyrepl test_re test_regrtest test_runpy test_set test_site test_smtplib test_socket test_ssl test_strftime test_string test_strptime test_strtod test_structseq test_symtable test_syntax test_sys test_sysconfig test_tarfile test_tempfile test_tokenize test_tools test_traceback test_turtle test_typing test_unittest test_unparse test_venv test_webbrowser test_winapi test_with test_wsgiref test_xmlrpc test_zipfile test_zipimport test_zoneinfo test_zstd

Legend:

  • [+] path exists in CPython
  • [x] up-to-date, [ ] outdated

@youknowone
youknowone force-pushed the compiler-cpython-parity branch from 2ad40f3 to 7758500 Compare August 28, 2026 09:08
`format_bytes` subtracted the byte length from the field width without
saturating, so `b'%1s' % (b'abcde',)` aborted with a capacity overflow.
A precision written as a bare dot now truncates to nothing, and `%r`/`%a`
on bytes now honour the precision and width like the other conversions.

`%c` with a bytes, bytearray or str argument that is not one unit long
now names the argument by its length instead of by its type.

Assisted-by: Claude Code:claude-opus-5
A grouping option next to an unrecognised presentation type now reports
"Cannot specify ',' with 'a'." instead of falling through to the type
check. `format(True, spec)` now validates the spec the way an int does.

The 'c' type now pads and aligns its character, and an integer outside
the C long range raises OverflowError rather than reporting a code point
out of range.

A complex formatted with the 'n'/'g' types or with a precision and no
type now goes through the general format without forcing a fraction.

Assisted-by: Claude Code:claude-opus-5
Dividing by a power of ten rounded twice: once when the quotient was
stored and again at the tie, so e.g. `round(2.5e2, -2)` came out as 300.0.
The digits now come from the exact decimal expansion of the value.

Assisted-by: Claude Code:claude-opus-5
An invalid literal parsed with base 0 named the base that the prefix
turned out to mean rather than 0. A string of nothing but a sign was
taken for the old octal form and returned 0 instead of raising.

Assisted-by: Claude Code:claude-opus-5
`str.split`, `rsplit`, `strip`, `lstrip` and `rstrip` with no separator
used Rust's whitespace predicate, which differs from the 29 code points
`Py_UNICODE_ISSPACE` names: U+001C-U+001F and U+0085 were skipped and
U+200B was treated as a separator.

`expandtabs` went through `str`, so it dropped the lone surrogates of a
WTF-8 string; it now works on `Wtf8` throughout.

Indexing a str out of range now says "string index out of range".

Assisted-by: Claude Code:claude-opus-5
`DerivedNumericValues.txt` field 1 carries a rounded decimal, so
`unicodedata.numeric('⅐')` and its neighbours came back short of the
value CPython reports. The generator now reads the rational in field 3,
and takes the 3.2 values from field 8 of `UnicodeData-3.2.0.txt`.

Assisted-by: Claude Code:claude-opus-5
`pattern_groupindex` wraps the group index in a dict proxy, and hands
back a plain dict only when the pattern has no named group.

Assisted-by: Claude Code:claude-opus-5
The utf-32 decoder dropped a code point above U+10FFFF instead of calling
the error handler, and both the utf-16 and utf-32 handlers were told the
encoding was "utf-16"/"utf-32" whichever byte order the data was in.

The encoders now take a bytes replacement from the handler as it stands,
rejecting one that does not fill whole code units, rather than iterating
over it as text; a str replacement from the utf-16 encoder now splits an
astral code point into a surrogate pair.

The utf-7 decoder names its encoding "utf7", as its codec does.

Assisted-by: Claude Code:claude-opus-5
Division now divides by whichever part of the divisor is larger, the way
`_Py_c_quot` does, and both it and multiplication recover the infinities
that the plain formulas turn into nan, following C11 Annex G. An operand
that is a real number is now multiplied and divided component-wise, which
keeps the sign of a zero and the other part of an infinity; a real
numerator has its own quotient so that its zero keeps a sign too.

An integer exponent no greater than 100 in magnitude is now reached by
repeated multiplication rather than through the polar form, so e.g.
`(1+2j) ** 2` is exact. A zero exponent gives 1, and an exponent with no
imaginary part no longer reaches for a logarithm that turns an infinite
magnitude into a nan. A result that came out infinite raises
OverflowError.

`complex() arg is a malformed string` is replaced by `could not convert
string to complex: ...` when the string carries a misplaced underscore,
`complex division by zero` by `division by zero`, and the message for
zero raised to a negative or complex power no longer names the base.

Assisted-by: Claude Code:claude-opus-5
A code object's string constants were interned by length, so a short
string that is not a name was interned and a long name was not. They are
now picked out the way `all_name_chars` does. The one-character latin-1
strings are interned as well, so that every route to one of them lands on
the same object.

Assisted-by: Claude Code:claude-opus-5
Versions below 2 now write a float or complex as its digits under
TYPE_FLOAT/TYPE_COMPLEX, versions from 4 on write a tuple of fewer than
256 items under TYPE_SMALL_TUPLE and a long ascii string under
TYPE_ASCII, and a string is marked interned only when it is.

`marshal.loads` now tells apart the three ways its data can run out:
"EOF read where object expected" where an object was to start, "EOF read
where not expected" where a single byte was, and "marshal data too short"
where a run of bytes was cut short.

Assisted-by: Claude Code:claude-opus-5
@youknowone
youknowone force-pushed the compiler-cpython-parity branch from 7758500 to a5fbe53 Compare August 29, 2026 11:52
@youknowone
youknowone marked this pull request as ready for review August 29, 2026 13:41
@youknowone
youknowone merged commit 28815d1 into RustPython:main Aug 29, 2026
27 of 28 checks passed
@youknowone
youknowone deleted the compiler-cpython-parity branch August 29, 2026 13:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants