[dart2wasm] Intrinsify math.min() and math.max() for double and int

Issue #55173.

Exposes signed min/max on WasmI64 and f64.min/f64.max on WasmF64 in
dart:_wasm, mirroring how WasmF64.sqrt is exposed today. The dart:math
min/max patches in math_patch.dart dispatch to them via runtime
`is`-checks, with @pragma('wasm:prefer-inline') so the inliner folds
the chain to the bare instruction sequence at each call site:

  T min<T extends num>(T a, T b) {
    if (a is int && b is int) return unsafeCast<T>((a as int).minS(b));
    if (a is double && b is double) return unsafeCast<T>((a as double).min(b));
    return _minSlow<T>(a, b);
  }

Wasm has no native i64 min_s/max_s, so WasmI64.minS/maxS emit the
same local.tee + i64.le_s/ge_s + select sequence. WasmF64.min/max emit
f64.min/f64.max directly.

The NaN- and signed-zero-aware fallback ladder is preserved in
`_minSlow` / `_maxSlow` (out-of-line, no pragma) and called for the
mixed and num cases. tests/lib/math/min_max_test.dart requires type
preservation between equal int and double arguments (e.g.
min(-499, -499.0) is int at line 113; max(499, 499.0) is int at
line 382), which a toDouble().max(toDouble()) fallback would not
satisfy.

Adds pkg/dart2wasm/test/ir_tests/math_min_max.dart covering min/max
for static int/int, double/double, mixed int/double, and num/num.
The .wat locks in f64.min/f64.max for the f64 paths, i64.le_s/i64.ge_s
+ select for the i64 paths, and `call $_minSlow` / `call $_maxSlow`
for mixed and num/num.

Measurements on a probe with four typed call sites (one each for
min<double>, max<double>, min<int>, max<int>, all marked
@pragma('wasm:never-inline')):

* .wasm size: 27,112 → 25,848 bytes (-4.66%). Generic $min and $max
  are eliminated by DCE.
* Runtime, 100M iterations per operation on d8, median of 10 runs:
  min<double> 543 → 213 ms (2.55x), max<double> 550 → 213 ms (2.58x),
  min<int>    552 →  65 ms (8.49x), max<int>    555 →  73 ms (7.61x).
  Checksums match between baseline and patched.

tests/lib/math/min_max_test.dart passes.

R=mkustermann@google.com, osa1@google.com

Change-Id: If8cf0a4df976f2d7f2230308905ff68491311c97
Bug: https://github.com/dart-lang/sdk/issues/55173
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/503740
Reviewed-by: Martin Kustermann <kustermann@google.com>
Reviewed-by: Kevin Moore <kevmoo@google.com>
Reviewed-by: Slava Egorov <vegorov@google.com>
Commit-Queue: Martin Kustermann <kustermann@google.com>
This commit is contained in:
Hakim Jonas Ghoula
2026-05-29 00:03:32 -07:00
committed by dart-scoped@luci-project-accounts.iam.gserviceaccount.com
parent 06bdbd777f
commit f710c4338a
5 changed files with 366 additions and 28 deletions
+36
View File
@@ -821,6 +821,26 @@ class Intrinsifier {
);
b.i64_div_s();
return w.NumType.i64;
case "minS":
case "maxS":
w.Local localA = b.addLocal(w.NumType.i64);
w.Local localB = b.addLocal(w.NumType.i64);
codeGen.translateExpression(receiver, w.NumType.i64);
b.local_tee(localA);
codeGen.translateExpression(
node.arguments.positional[0],
w.NumType.i64,
);
b.local_tee(localB);
b.local_get(localA);
b.local_get(localB);
if (name == "minS") {
b.i64_le_s();
} else {
b.i64_ge_s();
}
b.select(w.NumType.i64);
return w.NumType.i64;
default:
throw 'Unknown WasmI64 member $name';
}
@@ -854,6 +874,22 @@ class Intrinsifier {
);
b.f64_copysign();
return w.NumType.f64;
case "min":
codeGen.translateExpression(receiver, w.NumType.f64);
codeGen.translateExpression(
node.arguments.positional[0],
w.NumType.f64,
);
b.f64_min();
return w.NumType.f64;
case "max":
codeGen.translateExpression(receiver, w.NumType.f64);
codeGen.translateExpression(
node.arguments.positional[0],
w.NumType.f64,
);
b.f64_max();
return w.NumType.f64;
default:
throw 'Unknown WasmF64 member $name';
}
@@ -0,0 +1,64 @@
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
// functionFilter=test|sink
// globalFilter=DoesNotMatch
// tableFilter=DoesNotMatch
// compilerOption=-O2
import 'dart:math' as math;
final ktrue = int.parse('1') == 1;
final intA = ktrue ? 1 : 2;
final intB = ktrue ? 2 : 1;
final doubleA = ktrue ? 1.5 : 2.5;
final doubleB = ktrue ? 2.5 : 1.5;
final numIntA = ktrue ? intA : 1.2;
final numIntB = ktrue ? intB : 1.2;
@pragma('wasm:never-inline')
void main() {
testMinIntInt();
testMaxIntInt();
testMinDoubleDouble();
testMaxDoubleDouble();
testMinIntDouble();
testMaxIntDouble();
testMinNumNum();
testMaxNumNum();
}
@pragma('wasm:never-inline')
void testMinIntInt() => sinkInt(math.min(intA, intB));
@pragma('wasm:never-inline')
void testMaxIntInt() => sinkInt(math.max(intA, intB));
@pragma('wasm:never-inline')
void testMinDoubleDouble() => sinkDouble(math.min(doubleA, doubleB));
@pragma('wasm:never-inline')
void testMaxDoubleDouble() => sinkDouble(math.max(doubleA, doubleB));
@pragma('wasm:never-inline')
void testMinIntDouble() => sinkNum(math.min(intA, doubleA));
@pragma('wasm:never-inline')
void testMaxIntDouble() => sinkNum(math.max(intA, doubleA));
@pragma('wasm:never-inline')
void testMinNumNum() => sinkNum(math.min(numIntA, numIntB));
@pragma('wasm:never-inline')
void testMaxNumNum() => sinkNum(math.max(numIntA, numIntB));
@pragma('wasm:never-inline')
void sinkInt(int x) => print(x);
@pragma('wasm:never-inline')
void sinkDouble(double x) => print(x);
@pragma('wasm:never-inline')
void sinkNum(num x) => print(x);
@@ -0,0 +1,219 @@
(module $$
(type $#Top (struct
(field $field0 i32)))
(type $BoxedDouble (sub final $#Top (struct
(field $field0 i32)
(field $value f64))))
(type $BoxedInt (sub final $#Top (struct
(field $field0 i32)
(field $value i64))))
(func $doubleA implicit getter (result f64) <...>)
(func $doubleB implicit getter (result f64) <...>)
(func $intA implicit getter (result i64) <...>)
(func $intB implicit getter (result i64) <...>)
(func $numIntA implicit getter (result (ref $#Top)) <...>)
(func $numIntB implicit getter (result (ref $#Top)) <...>)
(func $"sinkDouble <noInline>" (param $var0 f64)
i32.const 92
local.get $var0
struct.new $BoxedDouble
call $print
)
(func $"sinkInt <noInline>" (param $var0 i64)
i32.const 86
local.get $var0
struct.new $BoxedInt
call $print
)
(func $"sinkNum <noInline>" (param $var0 (ref $#Top))
local.get $var0
call $print
)
(func $"testMaxDoubleDouble <noInline>"
call $"doubleA implicit getter"
call $"doubleB implicit getter"
f64.max
call $"sinkDouble <noInline>"
)
(func $"testMaxIntDouble <noInline>"
i32.const 86
call $"intA implicit getter"
struct.new $BoxedInt
i32.const 92
call $"doubleA implicit getter"
struct.new $BoxedDouble
call $_maxSlow
call $"sinkNum <noInline>"
)
(func $"testMaxIntInt <noInline>"
(local $var0 i64)
(local $var1 i64)
call $"intA implicit getter"
local.tee $var0
call $"intB implicit getter"
local.tee $var1
local.get $var0
local.get $var1
i64.ge_s
select
call $"sinkInt <noInline>"
)
(func $"testMaxNumNum <noInline>"
(local $var0 (ref $#Top))
(local $var1 (ref $#Top))
(local $var2 i64)
(local $var3 i64)
block $label0 (result (ref $#Top))
call $"numIntA implicit getter"
local.tee $var0
struct.get $#Top $field0
i32.const 86
i32.ne
call $"numIntB implicit getter"
local.tee $var1
struct.get $#Top $field0
i32.const 86
i32.ne
i32.or
i32.eqz
if
i32.const 86
local.get $var0
ref.cast $BoxedInt
struct.get $BoxedInt $value
local.tee $var2
local.get $var1
ref.cast $BoxedInt
struct.get $BoxedInt $value
local.tee $var3
local.get $var2
local.get $var3
i64.ge_s
select
struct.new $BoxedInt
br $label0
end
local.get $var0
struct.get $#Top $field0
i32.const 92
i32.ne
local.get $var1
struct.get $#Top $field0
i32.const 92
i32.ne
i32.or
i32.eqz
if
i32.const 92
local.get $var0
ref.cast $BoxedDouble
struct.get $BoxedDouble $value
local.get $var1
ref.cast $BoxedDouble
struct.get $BoxedDouble $value
f64.max
struct.new $BoxedDouble
br $label0
end
local.get $var0
local.get $var1
call $_maxSlow
end $label0
call $"sinkNum <noInline>"
)
(func $"testMinDoubleDouble <noInline>"
call $"doubleA implicit getter"
call $"doubleB implicit getter"
f64.min
call $"sinkDouble <noInline>"
)
(func $"testMinIntDouble <noInline>"
i32.const 86
call $"intA implicit getter"
struct.new $BoxedInt
i32.const 92
call $"doubleA implicit getter"
struct.new $BoxedDouble
call $_minSlow
call $"sinkNum <noInline>"
)
(func $"testMinIntInt <noInline>"
(local $var0 i64)
(local $var1 i64)
call $"intA implicit getter"
local.tee $var0
call $"intB implicit getter"
local.tee $var1
local.get $var0
local.get $var1
i64.le_s
select
call $"sinkInt <noInline>"
)
(func $"testMinNumNum <noInline>"
(local $var0 (ref $#Top))
(local $var1 (ref $#Top))
(local $var2 i64)
(local $var3 i64)
block $label0 (result (ref $#Top))
call $"numIntA implicit getter"
local.tee $var0
struct.get $#Top $field0
i32.const 86
i32.ne
call $"numIntB implicit getter"
local.tee $var1
struct.get $#Top $field0
i32.const 86
i32.ne
i32.or
i32.eqz
if
i32.const 86
local.get $var0
ref.cast $BoxedInt
struct.get $BoxedInt $value
local.tee $var2
local.get $var1
ref.cast $BoxedInt
struct.get $BoxedInt $value
local.tee $var3
local.get $var2
local.get $var3
i64.le_s
select
struct.new $BoxedInt
br $label0
end
local.get $var0
struct.get $#Top $field0
i32.const 92
i32.ne
local.get $var1
struct.get $#Top $field0
i32.const 92
i32.ne
i32.or
i32.eqz
if
i32.const 92
local.get $var0
ref.cast $BoxedDouble
struct.get $BoxedDouble $value
local.get $var1
ref.cast $BoxedDouble
struct.get $BoxedDouble $value
f64.min
struct.new $BoxedDouble
br $label0
end
local.get $var0
local.get $var1
call $_minSlow
end $label0
call $"sinkNum <noInline>"
)
(func $_maxSlow (param $var0 (ref $#Top)) (param $var1 (ref $#Top)) (result (ref $#Top)) <...>)
(func $_minSlow (param $var0 (ref $#Top)) (param $var1 (ref $#Top)) (result (ref $#Top)) <...>)
(func $print (param $var0 (ref $#Top)) <...>)
)
+17 -28
View File
@@ -3,55 +3,44 @@
// BSD-style license that can be found in the LICENSE file.
import "dart:_error_utils";
import "dart:_internal" show mix64, patch;
import "dart:_internal" show mix64, patch, unsafeCast;
import "dart:_wasm";
/// There are no parts of this patch library.
@patch
@pragma('wasm:prefer-inline')
T min<T extends num>(T a, T b) {
if (a is int && b is int) return unsafeCast<T>((a as int).minS(b));
if (a is double && b is double) return unsafeCast<T>((a as double).min(b));
return _minSlow<T>(a, b);
}
@patch
@pragma('wasm:prefer-inline')
T max<T extends num>(T a, T b) {
if (a is int && b is int) return unsafeCast<T>((a as int).maxS(b));
if (a is double && b is double) return unsafeCast<T>((a as double).max(b));
return _maxSlow<T>(a, b);
}
T _minSlow<T extends num>(T a, T b) {
if (a > b) return b;
if (a < b) return a;
if (b is double) {
// Special case for NaN and -0.0. If one argument is NaN return NaN.
// [min] must also distinguish between -0.0 and 0.0.
if (a is double) {
if (a == 0.0) {
// a is either 0.0 or -0.0. b is either 0.0, -0.0 or NaN.
// The following returns -0.0 if either a or b is -0.0, and it
// returns NaN if b is NaN.
num n = (a + b) * a * b;
return n as T;
}
}
// Check for NaN and b == -0.0.
if (a == 0 && b.isNegative || b.isNaN) return b;
return a;
}
return a;
}
@patch
T max<T extends num>(T a, T b) {
T _maxSlow<T extends num>(T a, T b) {
if (a > b) return a;
if (a < b) return b;
if (b is double) {
// Special case for NaN and -0.0. If one argument is NaN return NaN.
// [max] must also distinguish between -0.0 and 0.0.
if (a is double) {
if (a == 0.0) {
// a is either 0.0 or -0.0. b is either 0.0, -0.0, or NaN.
// The following returns 0.0 if either a or b is 0.0, and it
// returns NaN if b is NaN.
num n = a + b;
return n as T;
}
}
// Check for NaN.
if (b.isNaN) return b;
return a;
}
// max(-0.0, 0) must return 0.
if (b == 0 && a.isNegative) return b;
return a;
}
+30
View File
@@ -238,6 +238,12 @@ class WasmI64 extends _WasmBase {
/// Wasm `i64.div_s` instruction.
external WasmI64 divS(WasmI64 divisor);
/// Signed minimum via `i64.le_s` and `select`.
external WasmI64 minS(WasmI64 other);
/// Signed maximum via `i64.ge_s` and `select`.
external WasmI64 maxS(WasmI64 other);
}
/// The Wasm `f32` type.
@@ -279,6 +285,12 @@ class WasmF64 extends _WasmBase {
/// Wasm `f64.copysign` instruction.
external WasmF64 copysign(WasmF64 other);
/// Wasm `f64.min` instruction.
external WasmF64 min(WasmF64 other);
/// Wasm `f64.max` instruction.
external WasmF64 max(WasmF64 other);
}
/// The Wasm `v128` type.
@@ -766,6 +778,14 @@ extension IntWasmInstructions on int {
/// Wasm `i64.div_s` instruction.
@pragma("wasm:prefer-inline")
int divS(int divisor) => this.toWasmI64().divS(divisor.toWasmI64()).toInt();
/// Signed minimum via `i64.le_s` and `select`.
@pragma("wasm:prefer-inline")
int minS(int other) => this.toWasmI64().minS(other.toWasmI64()).toInt();
/// Signed maximum via `i64.ge_s` and `select`.
@pragma("wasm:prefer-inline")
int maxS(int other) => this.toWasmI64().maxS(other.toWasmI64()).toInt();
}
extension DoubleWasmInstructions on double {
@@ -787,6 +807,16 @@ extension DoubleWasmInstructions on double {
/// Wasm `f64.sqrt` instruction.
@pragma("wasm:prefer-inline")
double sqrt() => this.toWasmF64().sqrt().toDouble();
/// Wasm `f64.min` instruction.
@pragma("wasm:prefer-inline")
double min(double other) =>
this.toWasmF64().min(other.toWasmF64()).toDouble();
/// Wasm `f64.max` instruction.
@pragma("wasm:prefer-inline")
double max(double other) =>
this.toWasmF64().max(other.toWasmF64()).toDouble();
}
// Tests whether the given object's class is a subclass of T.