Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
[stdlib] Support nextafter at compile time
`nextafter` dispatched only to libc `nextafter`/`nextafterf` through an
`external_call`, which the comptime interpreter cannot run, so any call in a
comptime context failed to compile. That also blocked `math.ulp`, which relies
on `nextafter`.

This adds a comptime-interpreter branch that walks to the neighbouring value
straight from the IEEE-754 bit pattern and keeps the libc dispatch for runtime.
Adjacent representable floats of the same sign have adjacent integer bit
patterns, so the step toward `arg1` is a single increment or decrement, with
signed zero and NaN handled explicitly.

Fixes #6721

Assisted-by: Claude
  • Loading branch information
VihaanAgarwal committed Jul 17, 2026
commit 069e47fc412d516f464a475eb4d8e7a890333cda
34 changes: 34 additions & 0 deletions 34 mojo/stdlib/std/utils/numerics.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -998,6 +998,40 @@ def nextafter[
dtype.is_floating_point()
), "input dtype must be floating point"

if __is_run_in_comptime_interpreter:
# The runtime path dispatches to libc `nextafter(f)` through an
# `external_call`, which the comptime interpreter cannot execute. Walk
# to the neighbouring value directly from the IEEE-754 bit pattern
# instead: for a fixed sign, consecutive representable values map to
# consecutive integers when the raw bits are read as an integer, so
# stepping the magnitude is a single increment or decrement.
comptime int_dtype = _integral_type_of[dtype]()
var bits = bitcast[int_dtype, width](arg0)
# Moving toward `arg1` grows the magnitude (and the raw-bit integer)
# when it heads away from zero, and shrinks it otherwise: a larger
# value steps the bits up for a positive `arg0` but down for a negative
# one, and vice versa.
var toward_larger = arg1.gt(arg0)
var negative = arg0.lt(0)
var stepped = toward_larger.select(
negative.select(bits - 1, bits + 1),
negative.select(bits + 1, bits - 1),
)
var result = bitcast[dtype, width](stepped)
# `arg0 == 0` has no neighbour by stepping bits: the answer is the
# smallest subnormal carrying `arg1`'s sign (bit pattern `1`).
var smallest = bitcast[dtype, width](SIMD[int_dtype, width](1))
result = arg0.eq(0).select(
arg1.lt(0).select(-smallest, smallest), result
)
# Equal inputs (including +0 and -0) return `arg1` per the C contract.
result = arg0.eq(arg1).select(arg1, result)
# A NaN operand yields NaN.
result = (isnan(arg0) | isnan(arg1)).select(
SIMD[dtype, width](nan[dtype]()), result
)
return result

comptime if dtype == DType.float64:
return _simd_apply[_float64_dispatch, result_dtype=dtype](arg0, arg1)
return _simd_apply[_float32_dispatch, result_dtype=dtype](arg0, arg1)
10 changes: 10 additions & 0 deletions 10 mojo/stdlib/test/math/test_math.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,16 @@ def test_ulp() raises:
assert_equal(ulp(Float64(-5)), 8.881784197001252e-16)


def test_ulp_at_comptime() raises:
# `ulp` reaches `nextafter`, which used to fail in the comptime interpreter.
comptime u1 = ulp(Float32(1.0))
assert_equal(u1, ulp(Float32(1.0)))
comptime u5 = ulp(Float64(5))
assert_equal(u5, 8.881784197001252e-16)
comptime u_max = ulp(Float64.MAX_FINITE)
assert_equal(u_max, 1.99584030953472e292)


def test_ceildiv() raises:
# NOTE: these tests are here mostly to ensure the ceildiv method exists.
# Types that opt in to CeilDivable, should test their own dunder methods for
Expand Down
24 changes: 24 additions & 0 deletions 24 mojo/stdlib/test/utils/test_numerics.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -472,5 +472,29 @@ def test_nextafter() raises:
)


def test_nextafter_at_comptime() raises:
# `nextafter` used to dispatch to libc through an `external_call`, which the
# comptime interpreter cannot execute; these evaluate the branches at
# compile time to guard the pure-Mojo fallback.
comptime up = nextafter(Float64(1), Float64(2))
assert_almost_equal(up, 1.0000000000000002)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

These assertions are weaker than they look. assert_almost_equal defaults to atol=1e-8, rtol=1e-5, but nextafter(1, 2) = 1.0000000000000002 differs from 1.0 by only ~2.2e-16 — well inside the tolerance. So this would still pass if the function returned exactly 1.0 (a no-op bug), and likewise the subnormal cases (5e-324 is "almost equal" to 0). Since the whole point of nextafter is the 1-ULP step, please use assert_equal here for exact-bit checking. The test still serves its primary purpose (proving comptime evaluation compiles), so this is a strengthening suggestion rather than a correctness problem — note test_ulp_at_comptime already uses assert_equal, which is the pattern to follow. (The pre-existing runtime test_nextafter has the same shortcoming).

comptime down = nextafter(Float64(1), Float64(0))
assert_almost_equal(down, 0.99999999999999988)
comptime neg = nextafter(Float64(-1), Float64(0))
assert_almost_equal(neg, -0.99999999999999988)
comptime equal = nextafter(Float64(1), Float64(1))
assert_almost_equal(equal, 1)
comptime from_zero = nextafter(Float64(0), Float64(1))
assert_almost_equal(from_zero, 5e-324)
comptime from_zero_neg = nextafter(Float64(0), Float64(-1))
assert_almost_equal(from_zero_neg, -5e-324)
comptime vec = nextafter(
SIMD[DType.float64, 2](0, 1), SIMD[DType.float64, 2](1, 1)
)
assert_almost_equal(vec, SIMD[DType.float64, 2](5e-324, 1))
comptime is_nan = nextafter(nan[DType.float32](), Float32(1))
assert_true(isnan(is_nan))


def main() raises:
TestSuite.discover_tests[__functions_in_module()]().run()
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.