round(x, ndigits) ran ndigits through __index__ before calling
__round__, so a user-defined __round__ never saw a non-integer ndigits
and an __index__ object reached it already converted to int. CPython's
builtin_round_impl() forwards ndigits as-is and leaves the conversion
to float.__round__ and int.__round__.
Forward the object, and take ndigits as ArgIndex in both slots so they
apply __index__ themselves. Calling the slots directly with an
__index__ object now works as in CPython instead of raising TypeError.
Assisted-by: Claude Code:claude-opus-5
Assisted-by: Claude Code:claude-opus-5-5
int.__round__ returned the receiver unchanged on the no-rounding path, so
round() on an int subclass handed back an instance of that subclass:
round(True) was True, not 1. CPython routes both no-op paths (ndigits is
None, and ndigits >= 0) through long_long(), which reuses the reference
only when the value is an exact int and otherwise copies it into a fresh
one.
Return zelf.__int__(vm).into_pyref(), which goes through into_exact_or()
and is the same normalization the neighbouring __trunc__, __floor__ and
__ceil__ already perform.
Assisted-by: Claude Code:claude-opus-5
* Round float at the decimal level to match CPython's _Py_dg_dtoa
CPython's `float.__round__` (Objects/floatobject.c) routes through
`_Py_dg_dtoa` and rounds at the decimal level. The previous
`round_float_digits` multiplied by 10**ndigits and rounded at the
IEEE 754 binary level, which diverges for values that aren't exactly
representable. For example, 2.675 stores as 2.67499...; dtoa correctly
rounds it down to 2.67, but `(2.675 * 100.0).round() / 100.0` lands on
2.68 because the multiplication produces a phantom 267.5 tie that
round-half-to-even snaps up.
Rust's `{:.*}` float formatting uses dtoa-style algorithms (Grisu3 +
Dragon4 fallback) and matches CPython's `_Py_dg_dtoa` byte-for-byte.
Replace the multiply-then-round step with `format!` + `parse` for
ndigits >= 0. The ndigits < 0 path is unchanged because dividing
typical inputs by 10**|ndigits| produces genuine ties rather than
synthesizing them.
Verified byte-identical with CPython 3.14.4 over a 108-case random
fuzz plus targeted half-tie probes. Unmasks
`test_float.RoundTestCase.test_matches_float_format` and
`test_previous_round_bugs`.
* Use #[expect] with reason for float_cmp suppression
Co-authored-by: ShaharNaveh <50263213+ShaharNaveh@users.noreply.github.com>
---------
Co-authored-by: ShaharNaveh <50263213+ShaharNaveh@users.noreply.github.com>