Skip to content

[SIMD] Eccentric SIMD float min/max/clamp semantics. #63962

Closed

Description

@modulovalue

Consider:

import 'dart:typed_data';

String f(double d) =>
    d.isNaN ? 'NaN' : (d == 0.0 && d.isNegative ? '-0.0' : '$d');

void main() {
  final a = Float32x4(double.nan, 5.0, -0.0, 0.0);
  final b = Float32x4(5.0, double.nan, 0.0, -0.0);
  final mn = a.min(b);
  final mx = a.max(b);
  print('min: ${f(mn.x)} ${f(mn.y)} ${f(mn.z)} ${f(mn.w)}');
  print('max: ${f(mx.x)} ${f(mx.y)} ${f(mx.z)} ${f(mx.w)}');
}

Today Float32x4.min, max & clamp behave as a lane-wise a < b ? a : b / a > b ? a : b.

================= current behavior (VM / dart2js / dart2wasm) =================
min: 5.0 NaN 0.0 -0.0
max: 5.0 NaN 0.0 -0.0

However, this is not what the WebAssembly SIMD f32x4.min / f32x4.max instructions compute. Those follow IEEE-style semantics: NaN propagates, and signed zero is handled per IEEE min/max:

================= native f32x4.min / f32x4.max (what the SIMD ops give) =================
min: NaN NaN -0.0 -0.0
max: NaN NaN 0.0 0.0

For example:

  • min(NaN, x) / max(NaN, x): naive returns x, the SIMD op returns NaN.
  • min(-0.0, +0.0): naive returns +0.0; f32x4.min returns -0.0.

The same applies to Float64x2.min / max / clamp versus f64x2.min / f64x2.max.

Because of this, vectorizing Float32x4 on dart2wasm had to keep min, max, and clamp implemented as the scalar a < b ? a : b to preserve the current cross-backend behavior.

So, what should the specified behavior be?

  1. Change min / max / clamp to the Wasm / IEEE SIMD semantics which is a breaking change. Every backend can then use the native min/max instructions, and the behavior matches what a SIMD user coming from C / Rust / hardware intrinsics probably expects. The observable change is limited to NaN and signed-zero inputs.
  2. Keep the current naive a < b ? a : b semantics which blocks SIMD lowering of these ops on dart2wasm and is arguably surprising for a type whose whole point is SIMD.

I lean towards 1. SIMD dart:typed_data types are not widely used, and it seems very unlikely that anyone depends on the exact NaN / signed-zero result of Float32x4.min.

@lrhn @mkustermann thoughts?

Activity

  1. lrhn commented on Aug 4, 2026

    @lrhn
    Member

    1, please.

    Let's not enshrine a behavior that is expensive to implement.

  2. mkustermann commented on Aug 5, 2026

    @mkustermann
    Member

    Also in favor of 1. Looking at the docs:

    The result is the lesser of the two lane values if either is lesser. The result is unspecified if either lane contains a NaN value or if the values are -0.0 and 0.0, so that neither value is smaller or greater than the other. Different platforms may give different results in those cases, but always one of the lane values.

    So the "result is unspecified if either lane contains a NaN value" which means it's unspecified and we can change it without going through breaking change process.

    Maybe we should then actually specify what it does?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions