docs(arange): document numpy-consistency + numpy-comparison tests (#1083)#1088
docs(arange): document numpy-consistency + numpy-comparison tests (#1083)#1088yingjerkao wants to merge 2 commits into
Conversation
…#1083) Follow-up to #1077, addressing @ianmccul's approval-time requests. #1077's arange rework computes Nelem = ceil((end - start) / step), which matches numpy.arange -- but the docs still described the range as strictly half-open with the endpoint "exclusive", which is inaccurate: floating-point rounding can make the final element equal or slightly exceed the endpoint (e.g. np.arange(0.5, 0.8, 0.1) == [0.5, 0.6, 0.7, 0.8]). - Docs: reframe the Generator.hpp docstring and the Generator.cpp comment to state arange follows numpy.arange, with the *nominal* half-open caveat and the endpoint-inclusion example; link the numpy docs. - Overflow guard: kept (Ian noted it looked over-defensive), with a comment explaining it prevents UB in the double->uint64 cast when count overflows to +inf for finite start/end/step (a huge span with a tiny step) -- a reachable path, not just an un-allocatable size. Testing: - pytests/arange_numpy_test.py (new): parametrized differential tests against numpy.arange (independent oracle) -- count and values across ascending/ descending, fractional, endpoint-inclusion, small-scale, single-element, empty ranges, and the arange(N) overload. 17 passed. - C++ Arange.EndpointMayBeIncludedUnderRounding (new): documents the FP endpoint-inclusion at the C++ level. Arange suite 15 passed. No behavior change (docs + tests only). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request aligns cytnx.arange with numpy.arange's ceil-based element count and floating-point rounding behavior, including cases where the endpoint is included. It updates documentation, adds comprehensive Python and C++ tests, and introduces a guard in src/Generator.cpp to prevent undefined behavior when casting the element count from double to uint64_t. The review feedback correctly identifies a flaw in this guard: because static_cast<double>(std::numeric_limits<uint64_t>::max()) rounds up to 2^64 due to precision limits, the comparison nelem > static_cast<double>(...) evaluates to false when nelem is exactly 2^64, which still triggers undefined behavior. The reviewer's suggestion to use >= 18446744073709551616.0 is highly recommended to ensure safety.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| cytnx_error_msg( | ||
| nelem > static_cast<cytnx_double>(std::numeric_limits<cytnx_uint64>::max()), |
There was a problem hiding this comment.
Casting a floating-point value that is greater than or equal to 18446744073709551616.0) to uint64_t is undefined behavior in C++.
Because static_cast<double>(std::numeric_limits<uint64_t>::max()) rounds up to 18446744073709551616.0 due to the precision limits of 64-bit double, the comparison nelem > static_cast<double>(...) evaluates to false when nelem is exactly 18446744073709551616.0. This allows the undefined behavior cast static_cast<cytnx_uint64>(nelem) to proceed on line 72.
To prevent this, the check should use >= with 18446744073709551616.0 (which is exactly double).
| cytnx_error_msg( | |
| nelem > static_cast<cytnx_double>(std::numeric_limits<cytnx_uint64>::max()), | |
| cytnx_error_msg( | |
| nelem >= 18446744073709551616.0, |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #1088 +/- ##
=======================================
Coverage 72.67% 72.67%
=======================================
Files 226 226
Lines 28177 28177
Branches 71 71
=======================================
Hits 20479 20479
Misses 7677 7677
Partials 21 21
Flags with carried forward coverage won't be shown. Click here to find out more.
Continue to review full report in Codecov by Harness.
🚀 New features to boost your workflow:
|
The overflow guard used `nelem > static_cast<cytnx_double>(UINT64_MAX)`, but UINT64_MAX (2^64 - 1) is not representable as a double and rounds UP to 2^64. The test was therefore effectively `nelem > 2^64`, which lets `nelem == 2^64` slip through into `static_cast<cytnx_uint64>(nelem)` -- and casting a double whose truncated value is >= 2^64 to uint64_t is undefined behavior ([conv.fpint]: UB when the truncated value is not representable in the destination type). So the guard failed to prevent the very UB it existed for, at exactly the 2^64 boundary. Fix: compare with `>=` against 2^64 written explicitly as `0x1p64` (the first double value not representable as uint64_t). This rejects no valid count: the largest double strictly below 2^64 is 2^64 - 2048. Dropped the now-unused <limits> include. Testing: new Arange.OverflowingCountThrows asserts arange(0, 2^64, 1) and a span/step whose count overflows to +inf both throw. Arange suite 13 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Added a second commit (4bafb02) fixing a boundary bug in the overflow guard that surfaced while double-checking the "casting a double >= 2^64 to uint64_t is UB" claim. The bug. The guard was: nelem > static_cast<cytnx_double>(std::numeric_limits<cytnx_uint64>::max())
Quick confirmation: The fix. Compare with Test. New So the original comment's UB claim was correct — and checking it turned up this off-by-one at the boundary. |
Problem
Follow-up to #1077, capturing @ianmccul's approval-time requests. #1077 reworked
arangetoNelem = ceil((end - start) / step), which matchesnumpy.arange— but:end", which is inaccurate: floating-point rounding can make the final element equal or slightly exceed the endpoint (e.g.np.arange(0.5, 0.8, 0.1) == [0.5, 0.6, 0.7, 0.8]);Fix (docs + tests only — no behavior change)
Generator.hppdocstring and theGenerator.cppcomment to state thatarangefollowsnumpy.arange, with the nominal half-open caveat and the endpoint-inclusion example spelled out.countcan overflow to+inffor finitestart/end/step(a huge span with a tiny step), and casting a non-representable double touint64is UB. So it guards a reachable path, not just an un-allocatable size.Testing
pytests/arange_numpy_test.py(new): parametrized differential tests againstnumpy.arange(an independent oracle) — element count and values across ascending/descending, fractional, the endpoint-inclusion FP cases, small scales, single-element, empty ranges, and thearange(N)overload.Arange.EndpointMayBeIncludedUnderRounding(new C++ test): documents the FP endpoint-inclusion (arange(0.5, 0.8, 0.1)→ 4 elements incl.0.8) at the C++ level.Closes #1083.