Skip to content
Merged
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
28 changes: 28 additions & 0 deletions .release-notes/next-release.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,31 @@ The return type is now `(A, I32)` instead of `(A, U32)`. If you destructure the
(let mantissa, let exp: I32) = my_float.frexp()
```

## Fix asymmetric NaN handling in F32/F64 min and max

`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.

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.

## Use LLVM intrinsics for NaN-propagating float min and max

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.

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.

Before:

```pony
// Old behavior: result depended on argument order
F32.nan().min(F32(5.0)) // => 5.0
F32(5.0).min(F32.nan()) // => NaN
```

After:

```pony
// New behavior: NaN propagates regardless of position
F32.nan().min(F32(5.0)) // => NaN
F32(5.0).min(F32.nan()) // => NaN
```

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

### Added

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

## [0.62.1] - 2026-03-28

Expand Down
12 changes: 8 additions & 4 deletions packages/builtin/float.pony
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ use @asinh[F64](x: F64)
use @atanh[F64](x: F64)
use @"llvm.copysign.f32"[F32](x: F32, sign: F32)
use @"llvm.copysign.f64"[F64](x: F64, sign: F64)
use @"llvm.minimum.f32"[F32](x: F32, y: F32)
use @"llvm.minimum.f64"[F64](x: F64, y: F64)
use @"llvm.maximum.f32"[F32](x: F32, y: F32)
use @"llvm.maximum.f64"[F64](x: F64, y: F64)
use @frexp[F64](value: F64, exponent: Pointer[I32])
use @ldexpf[F32](value: F32, exponent: I32)
use @ldexp[F64](value: F64, exponent: I32)
Expand Down Expand Up @@ -147,8 +151,8 @@ primitive F32 is FloatingPoint[F32]
fun round(): F32 => @"llvm.round.f32"(this)
fun trunc(): F32 => @"llvm.trunc.f32"(this)

fun min(y: F32): F32 => if this < y then this else y end
fun max(y: F32): F32 => if this > y then this else y end
fun min(y: F32): F32 => @"llvm.minimum.f32"(this, y)
fun max(y: F32): F32 => @"llvm.maximum.f32"(this, y)

fun fld(y: F32): F32 =>
(this / y).floor()
Expand Down Expand Up @@ -364,8 +368,8 @@ primitive F64 is FloatingPoint[F64]
fun round(): F64 => @"llvm.round.f64"(this)
fun trunc(): F64 => @"llvm.trunc.f64"(this)

fun min(y: F64): F64 => if this < y then this else y end
fun max(y: F64): F64 => if this > y then this else y end
fun min(y: F64): F64 => @"llvm.minimum.f64"(this, y)
fun max(y: F64): F64 => @"llvm.maximum.f64"(this, y)

fun fld(y: F64): F64 =>
(this / y).floor()
Expand Down
Loading