Fix complex repr to use scientific notation for large integer-valued components#7634
Conversation
…components
repr of a complex number whose real or imaginary part is an integer-valued
float with |x| >= 1e16 emitted the full decimal expansion instead of
scientific notation, diverging from CPython:
Before (RustPython):
repr(1e100 + 1e100j)
(10000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000+1000000000000000
000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000j)
After / CPython:
(1e+100+1e+100j)
Root cause in crates/literal/src/complex.rs::to_string — it bifurcated
each component by .fract() == 0.0:
if im.fract() == 0.0 { im.to_string() } // Rust's default Display
else { float::to_string(im) } // scientific for large/small
Rust's Display never uses scientific notation, so any integer-valued f64
(including 1e16, 1e17, 1e100 which are exactly representable as integers)
routed through the wrong branch and produced the full decimal expansion.
Non-integer magnitudes reached float::to_string and rendered correctly.
The fix is to use one helper per component that implements CPython's
actual PyOS_double_to_string(format='r') rule: scientific notation when
|x| < 1e-4 or |x| >= 1e16, otherwise Rust's default Display (which drops
the trailing '.0' for integer-valued floats — matching CPython's
(1+2j) convention rather than (1.0+2.0j)). The threshold matches
float::to_string; the only behavioral difference is that complex
components render 1.0 as "1" rather than "1.0".
Verified:
* 29 CPython reference cases (normal / boundary / extremes / special /
signed-zero) — all byte-identical after fix.
* 18 additional edge cases (subnormal 5e-324, f64::MAX, MIN_POSITIVE,
DBL_EPSILON, threshold-straddling values) — all byte-identical.
* Lib/test/test_complex.py::test_repr_str /
test_negative_zero_repr_str / test_repr_roundtrip — all pass.
* cargo run -- -m test test_complex — 37 passed.
* cargo run -- -m test test_float test_long — 101 passed.
* ast.unparse() round-trip of source containing complex literals
(e.g. 1e100 + 1e-100j, 1e17 + 1j) produces CPython-identical output.
* extra_tests/snippets/builtin_complex.py — 20+ new regression cases.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAdded a Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@extra_tests/snippets/builtin_complex.py`:
- Around line 254-257: The comment above the three asserts using repr is
misleading: it currently claims "Values at the threshold boundaries must stay in
non-scientific form" but the test expects 1e-5 to use scientific notation;
update that comment to clearly state the rule (e.g., values >= 1e-4 should
remain in non-scientific decimal form while values < 1e-4 should use scientific
notation) and reference the three assertions (assert repr(1e15 + 1j), assert
repr(1e-4 + 1j), assert repr(1e-5 + 1j)) so readers understand why 1e-4 stays
non-scientific and 1e-5 becomes "1e-05".
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: 57228947-ad0b-4fe8-ac67-2850365963c0
📒 Files selected for processing (2)
crates/literal/src/complex.rsextra_tests/snippets/builtin_complex.py
The comment claimed all three assertions stay in non-scientific form, but the 1e-5 case explicitly verifies scientific notation (since |1e-5| < 1e-4 falls outside the decimal-form range). Reworded the header to describe the axis being tested (threshold boundary) and added per-case inline notes indicating each assertion's expected form.
Summary
repr()on a complex number whose real or imaginary part is an integer-valued float with|x| >= 1e16emitted the full decimal expansion instead of scientific notation. CPython uses scientific on the same input.Root cause
crates/literal/src/complex.rs::to_stringbifurcated each component by.fract() == 0.0:Integer-valued f64 values — including
1e16,1e17,1e100which are all exactly representable as integers — took theim.to_string()branch and rendered as full decimal expansion. Non-integer magnitudes tookfloat::to_stringand rendered correctly. The bifurcation didn't capture CPython's actual rule ("scientific iff|x| < 1e-4or|x| >= 1e16"); it happened to be correct only when the two conditions coincidentally aligned.Fix
Replace the two-branch logic with one helper that implements CPython's actual
PyOS_double_to_string(format='r')rule:The threshold matches
float::to_string; the only behavioral difference is that complex components render1.0as\"1\"rather than\"1.0\"— matching CPython's(1+2j)convention rather than(1.0+2.0j).Verification
Byte-identical CPython parity
Test suites
ast unparse
The other caller of
literal::complex::to_stringiscodegen::unparse.rs, which generates Python source from an AST. Round-trip of source containing complex literals:Regression snippet
extra_tests/snippets/builtin_complex.pynow pins expected output at the threshold boundaries, at extremes (1e100,-1e100), with mixed magnitudes, and at the integer-vs-float rendering boundary (1+2j,1.0+2.0j→ both(1+2j)).Scope
crates/literal/src/complex.rs::to_string(the single function that renders complexrepr/str/AST constants). Both callers (PyComplex::Representableandcodegen::unparse) receive the fix transitively.crates/common/src/format.rs::format_complex— this handles format-spec types ('{:.5e}'.format(c)) and is an independent code path. Its current output already matches CPython byte-identically across boundary precisions (verified in a separate audit). Not touched here.Related
PyComplex::repr_strnoted the intent to consolidate complex formatting; keeping the helper inrustpython_literal::complexmatches where existing callers already look for it.Summary by CodeRabbit
Bug Fixes
Tests