Skip to content

docs(arange): document numpy-consistency + numpy-comparison tests (#1083)#1088

Open
yingjerkao wants to merge 2 commits into
masterfrom
fix/1083-arange-numpy-docs
Open

docs(arange): document numpy-consistency + numpy-comparison tests (#1083)#1088
yingjerkao wants to merge 2 commits into
masterfrom
fix/1083-arange-numpy-docs

Conversation

@yingjerkao

Copy link
Copy Markdown
Collaborator

Problem

Follow-up to #1077, capturing @ianmccul's approval-time requests. #1077 reworked arange to Nelem = ceil((end - start) / step), which matches numpy.arange — but:

  • the docs still described the range as strictly half-open with the endpoint "exclusive" / "the last element is the largest one smaller than 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]);
  • there were no tests confirming consistency with numpy.

Fix (docs + tests only — no behavior change)

  • Documentation: reframed the Generator.hpp docstring and the Generator.cpp comment to state that arange follows numpy.arange, with the nominal half-open caveat and the endpoint-inclusion example spelled out.
  • Overflow guard: kept (Ian flagged it as over-defensive), but added a comment explaining it is not paranoia — count can overflow to +inf for finite start/end/step (a huge span with a tiny step), and casting a non-representable double to uint64 is UB. So it guards a reachable path, not just an un-allocatable size.

Testing

  • pytests/arange_numpy_test.py (new): parametrized differential tests against numpy.arange (an independent oracle) — element count and values across ascending/descending, fractional, the endpoint-inclusion FP cases, small scales, single-element, empty ranges, and the arange(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.

…#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>
@yingjerkao
yingjerkao requested a review from ianmccul July 19, 2026 02:29

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/Generator.cpp Outdated
Comment on lines 67 to 68
cytnx_error_msg(
nelem > static_cast<cytnx_double>(std::numeric_limits<cytnx_uint64>::max()),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Casting a floating-point value that is greater than or equal to $2^{64}$ (i.e., 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 $2^{64}$ and perfectly representable in double).

Suggested change
cytnx_error_msg(
nelem > static_cast<cytnx_double>(std::numeric_limits<cytnx_uint64>::max()),
cytnx_error_msg(
nelem >= 18446744073709551616.0,

@codecov

codecov Bot commented Jul 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 72.67%. Comparing base (a16e804) to head (4bafb02).
⚠️ Report is 9 commits behind head on master.
✅ All tests successful. No failed tests found.

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           
Flag Coverage Δ
cpp 72.79% <100.00%> (ø)
python 64.13% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
C++ backend 71.89% <100.00%> (ø)
Python bindings 77.70% <ø> (ø)
Python package 64.13% <ø> (ø)
Files with missing lines Coverage Δ
src/Generator.cpp 91.66% <100.00%> (ø)

Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update a16e804...4bafb02. Read the comment docs.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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>
@yingjerkao

Copy link
Copy Markdown
Collaborator Author

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())

UINT64_MAX (2^64 - 1) is not representable as a double, so static_cast<double>(UINT64_MAX) rounds up to 2^64 (18446744073709551616.0). The test was therefore effectively nelem > 2^64, which lets nelem == 2^64 pass through into static_cast<cytnx_uint64>(nelem) — and per [conv.fpint] that cast is UB (the truncated value 2^64 doesn't fit in uint64_t). So the guard failed to prevent the exact UB it exists for, at the 2^64 boundary.

Quick confirmation:

(double)UINT64_MAX = 18446744073709551616.0   # == 2^64, not 2^64-1
nelem >  (double)UINT64_MAX , nelem = 2^64 -> fires? 0   # slips through
nelem >= (double)UINT64_MAX , nelem = 2^64 -> fires? 1

The fix. Compare with >= against 2^64 written explicitly as 0x1p64 (the first double not representable as uint64_t). It rejects no valid count — the largest double strictly below 2^64 is 2^64 - 2048. Also dropped the now-unused <limits> include.

Test. New Arange.OverflowingCountThrows asserts arange(0, 2^64, 1) and a span/step whose count overflows to +inf both throw. Arange suite: 13 passed.

So the original comment's UB claim was correct — and checking it turned up this off-by-one at the boundary.

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.

arange: add numpy-comparison pytests + document numpy-consistency (follow-up to #1077)

1 participant