Skip to content

Commit 702ea79

Browse files
committed
Use LLVM minimum/maximum intrinsics for float min/max
The conditional implementation (`if this < y then this else y end`) is asymmetric when NaN is involved: IEEE 754 comparisons with NaN return false, so the else branch always fires when `this` is NaN but not when `y` is NaN. The result depends on argument order, which is wrong. LLVM's `llvm.minimum`/`llvm.maximum` intrinsics implement IEEE 754-2019 NaN-propagating semantics — if either operand is NaN, the result is NaN. Closes #5089
1 parent ab9f2d5 commit 702ea79

File tree

3 files changed

+38
-4
lines changed

3 files changed

+38
-4
lines changed

.release-notes/next-release.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,31 @@ The return type is now `(A, I32)` instead of `(A, U32)`. If you destructure the
117117
(let mantissa, let exp: I32) = my_float.frexp()
118118
```
119119

120+
## Fix asymmetric NaN handling in F32/F64 min and max
121+
122+
`F32.min` and `F64.min` (and `max`) gave different results depending on which argument was NaN. `F32.nan().min(5.0)` returned `5.0`, but `F32(5.0).min(F32.nan())` returned `NaN`. The result of a min/max operation shouldn't depend on argument order.
123+
124+
The root cause was the conditional implementation `if this < y then this else y end`. IEEE 754 comparisons involving NaN always return `false`, so the `else` branch fires whenever `this` is NaN but not when only `y` is NaN.
125+
126+
## Use LLVM intrinsics for NaN-propagating float min and max
127+
128+
Float `min` and `max` now use LLVM's `llvm.minimum` and `llvm.maximum` intrinsics instead of conditional comparisons. These implement IEEE 754-2019 semantics: if either operand is NaN, the result is NaN.
129+
130+
This is a breaking change. Code that relied on `min`/`max` to silently discard a NaN operand will now get NaN back. That said, the old behavior was order-dependent and unreliable, so anyone depending on it was already getting inconsistent results.
131+
132+
Before:
133+
134+
```pony
135+
// Old behavior: result depended on argument order
136+
F32.nan().min(F32(5.0)) // => 5.0
137+
F32(5.0).min(F32.nan()) // => NaN
138+
```
139+
140+
After:
141+
142+
```pony
143+
// New behavior: NaN propagates regardless of position
144+
F32.nan().min(F32(5.0)) // => NaN
145+
F32(5.0).min(F32.nan()) // => NaN
146+
```
147+

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ All notable changes to the Pony compiler and standard library will be documented
1313
- Fix segfault when using Generator.map with PonyCheck shrinking ([PR #5006](https://github.com/ponylang/ponyc/pull/5006))
1414
- Fix pony-lint blank-lines rule false positives on multi-line docstrings ([PR #5109](https://github.com/ponylang/ponyc/pull/5109))
1515
- Fix `FloatingPoint.frexp` returning unsigned exponent ([PR #5113](https://github.com/ponylang/ponyc/pull/5113))
16+
- Fix asymmetric NaN handling in F32/F64 min and max ([PR #5114](https://github.com/ponylang/ponyc/pull/5114))
1617

1718
### Added
1819

@@ -23,6 +24,7 @@ All notable changes to the Pony compiler and standard library will be documented
2324
- Remove support for Alpine 3.20 ([PR #5094](https://github.com/ponylang/ponyc/pull/5094))
2425
- Remove docgen pass ([PR #5097](https://github.com/ponylang/ponyc/pull/5097))
2526
- Change `FloatingPoint.frexp` exponent return type from `U32` to `I32` ([PR #5113](https://github.com/ponylang/ponyc/pull/5113))
27+
- Use LLVM intrinsics for NaN-propagating float min and max ([PR #5114](https://github.com/ponylang/ponyc/pull/5114))
2628

2729
## [0.62.1] - 2026-03-28
2830

packages/builtin/float.pony

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ use @asinh[F64](x: F64)
5656
use @atanh[F64](x: F64)
5757
use @"llvm.copysign.f32"[F32](x: F32, sign: F32)
5858
use @"llvm.copysign.f64"[F64](x: F64, sign: F64)
59+
use @"llvm.minimum.f32"[F32](x: F32, y: F32)
60+
use @"llvm.minimum.f64"[F64](x: F64, y: F64)
61+
use @"llvm.maximum.f32"[F32](x: F32, y: F32)
62+
use @"llvm.maximum.f64"[F64](x: F64, y: F64)
5963
use @frexp[F64](value: F64, exponent: Pointer[I32])
6064
use @ldexpf[F32](value: F32, exponent: I32)
6165
use @ldexp[F64](value: F64, exponent: I32)
@@ -147,8 +151,8 @@ primitive F32 is FloatingPoint[F32]
147151
fun round(): F32 => @"llvm.round.f32"(this)
148152
fun trunc(): F32 => @"llvm.trunc.f32"(this)
149153

150-
fun min(y: F32): F32 => if this < y then this else y end
151-
fun max(y: F32): F32 => if this > y then this else y end
154+
fun min(y: F32): F32 => @"llvm.minimum.f32"(this, y)
155+
fun max(y: F32): F32 => @"llvm.maximum.f32"(this, y)
152156

153157
fun fld(y: F32): F32 =>
154158
(this / y).floor()
@@ -364,8 +368,8 @@ primitive F64 is FloatingPoint[F64]
364368
fun round(): F64 => @"llvm.round.f64"(this)
365369
fun trunc(): F64 => @"llvm.trunc.f64"(this)
366370

367-
fun min(y: F64): F64 => if this < y then this else y end
368-
fun max(y: F64): F64 => if this > y then this else y end
371+
fun min(y: F64): F64 => @"llvm.minimum.f64"(this, y)
372+
fun max(y: F64): F64 => @"llvm.maximum.f64"(this, y)
369373

370374
fun fld(y: F64): F64 =>
371375
(this / y).floor()

0 commit comments

Comments
 (0)