Skip to content

Commit 509e955

Browse files
committed
Remove all user-space asUint/toUint in the libraries.
Instead, we use `Integer.toUnsignedLong(x).toDouble`, which is semantically equivalent. The only remaining use of `x >>> 0` is in the JS-only runtime libraries, notably `RuntimeLong`. We add some optimizations to generate the best possible code on all targets. For JS with `RuntimeLong`, we need to mark `RuntimeLong.toDouble` as fully inline. That is fine, as its body is actually very short (shorter than most methods of `RuntimeLong`). Moreover, we need a tailored rewrite in the optimizer to get rid of a `Double` addition of `0.0 + y` when `y` is provably non-negative. For JS with `bigint`s, we directly fold `(double) <toLongUnsigned>(x)` into `x >>> 0`. Otherwise, we unnecessarily go through a `bigint` with `Number(BigInt(x >>> 0))`. For Wasm, we fold the same shape into a single operation `f64.convert_i32_u`, which we add in `WasmTransients`.
1 parent 190785a commit 509e955

File tree

9 files changed

+74
-33
lines changed

9 files changed

+74
-33
lines changed

javalib/src/main/scala/java/lang/Integer.scala

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,9 @@ object Integer {
201201
@inline def toUnsignedLong(x: Int): scala.Long =
202202
throw new Error("stub") // body replaced by the compiler back-end
203203

204+
@inline private[lang] def toUnsignedDouble(x: Int): scala.Double =
205+
toUnsignedLong(x).toDouble
206+
204207
// Wasm intrinsic
205208
def bitCount(i: scala.Int): scala.Int = {
206209
/* See http://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetParallel
@@ -308,16 +311,11 @@ object Integer {
308311

309312
@inline private[this] def toStringBase(i: scala.Int, base: scala.Int): String = {
310313
import js.JSNumberOps.enableJSNumberOps
311-
asUint(i).toString(base)
314+
toUnsignedDouble(i).toString(base)
312315
}
313316

314317
@inline private def asInt(n: scala.Double): scala.Int = {
315318
import js.DynamicImplicits.number2dynamic
316319
(n | 0).asInstanceOf[Int]
317320
}
318-
319-
@inline private def asUint(n: scala.Int): scala.Double = {
320-
import js.DynamicImplicits.number2dynamic
321-
(n.toDouble >>> 0).asInstanceOf[scala.Double]
322-
}
323321
}

javalib/src/main/scala/java/lang/Long.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ object Long {
158158
if ((i >>> 32).toInt == 0) {
159159
// It's an unsigned int32
160160
import js.JSNumberOps.enableJSNumberOps
161-
Utils.toUint(i.toInt).toString(radix)
161+
Integer.toUnsignedDouble(i.toInt).toString(radix)
162162
} else {
163163
toUnsignedStringInternalLarge(i, radix)
164164
}

javalib/src/main/scala/java/lang/Utils.scala

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -187,9 +187,4 @@ private[java] object Utils {
187187
false
188188
// scalastyle:on return
189189
}
190-
191-
@inline def toUint(x: scala.Double): scala.Double = {
192-
import js.DynamicImplicits.number2dynamic
193-
(x >>> 0).asInstanceOf[scala.Double]
194-
}
195190
}

library/src/main/scala/scala/scalajs/js/JSNumberOps.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ object JSNumberOps {
8888
final class ExtOps private[JSNumberOps] (private val self: js.Dynamic)
8989
extends AnyVal {
9090

91+
@deprecated("Use Integer.toUnsignedLong(x).toDouble instead.", since = "1.20.0")
9192
@inline def toUint: Double =
9293
(self >>> 0.asInstanceOf[js.Dynamic]).asInstanceOf[Double]
9394
}

linker-private-library/src/main/scala/org/scalajs/linker/runtime/FloatingPointBitsPolyfills.scala

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ object FloatingPointBitsPolyfills {
8686
val fbits = 52
8787
val hifbits = fbits - 32
8888
val hi = (bits >>> 32).toInt
89-
val lo = toUint(bits.toInt)
89+
val lo = (bits & 0xffffffffL).toDouble
9090
val sign = (hi >> 31) | 1 // -1 or 1
9191
val e = (hi >> hifbits) & ((1 << ebits) - 1)
9292
val f = (hi & ((1 << hifbits) - 1)).toDouble * 0x100000000L.toDouble + lo
@@ -181,9 +181,6 @@ object FloatingPointBitsPolyfills {
181181
}
182182
}
183183

184-
@inline private def toUint(x: Int): Double =
185-
(x.asInstanceOf[js.Dynamic] >>> 0.asInstanceOf[js.Dynamic]).asInstanceOf[Double]
186-
187184
@inline private def rawToInt(x: Double): Int =
188185
(x.asInstanceOf[js.Dynamic] | 0.asInstanceOf[js.Dynamic]).asInstanceOf[Int]
189186

linker-private-library/src/main/scala/org/scalajs/linker/runtime/RuntimeLong.scala

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -667,7 +667,7 @@ object RuntimeLong {
667667
val divisorInv = 1.0 / divisor.toDouble
668668

669669
// initial approximation of the quotient and remainder
670-
val approxNum = asUint(hi) * TwoPow32 + asUint(lo)
670+
val approxNum = unsignedToDoubleApprox(lo, hi)
671671
var approxQuot = scala.scalajs.js.Math.floor(approxNum * divisorInv)
672672
var approxRem = lo - divisor * unsignedSafeDoubleLo(approxQuot)
673673

@@ -691,16 +691,15 @@ object RuntimeLong {
691691
a.lo
692692

693693
@inline
694-
def toDouble(a: RuntimeLong): Double =
695-
toDouble(a.lo, a.hi)
696-
697-
private def toDouble(lo: Int, hi: Int): Double = {
694+
def toDouble(a: RuntimeLong): Double = {
695+
val lo = a.lo
696+
val hi = a.hi
698697
if (hi < 0) {
699-
// We do asUint() on the hi part specifically for MinValue
698+
// We need unsignedToDoubleApprox specifically for MinValue
700699
val neg = inline_negate(lo, hi)
701-
-(asUint(neg.hi) * TwoPow32 + asUint(neg.lo))
700+
-unsignedToDoubleApprox(neg.lo, neg.hi)
702701
} else {
703-
hi * TwoPow32 + asUint(lo)
702+
nonNegativeToDoubleApprox(lo, hi)
704703
}
705704
}
706705

@@ -757,8 +756,7 @@ object RuntimeLong {
757756
if (isUnsignedSafeDouble(abs.hi) || (abs.lo & 0xffff) == 0) abs.lo
758757
else (abs.lo & ~0xffff) | 0x8000
759758

760-
// We do asUint() on the hi part specifically for MinValue
761-
val absRes = (asUint(abs.hi) * TwoPow32 + asUint(compressedAbsLo))
759+
val absRes = unsignedToDoubleApprox(compressedAbsLo, abs.hi)
762760

763761
(if (hi < 0) -absRes else absRes).toFloat
764762
}
@@ -1201,7 +1199,7 @@ object RuntimeLong {
12011199

12021200
/** Converts an unsigned safe double into its Double representation. */
12031201
@inline def asUnsignedSafeDouble(lo: Int, hi: Int): Double =
1204-
hi * TwoPow32 + asUint(lo)
1202+
nonNegativeToDoubleApprox(lo, hi)
12051203

12061204
/** Converts an unsigned safe double into its RuntimeLong representation. */
12071205
@inline def fromUnsignedSafeDouble(x: Double): RuntimeLong =
@@ -1215,10 +1213,27 @@ object RuntimeLong {
12151213
@inline def unsignedSafeDoubleHi(x: Double): Int =
12161214
rawToInt(x / TwoPow32)
12171215

1216+
/** Approximates an unsigned (lo, hi) with a Double. */
1217+
@inline def unsignedToDoubleApprox(lo: Int, hi: Int): Double =
1218+
uintToDouble(hi) * TwoPow32 + uintToDouble(lo)
1219+
1220+
/** Approximates a non-negative (lo, hi) with a Double.
1221+
*
1222+
* If `hi` is known to be non-negative, this method is equivalent to
1223+
* `unsignedToDoubleApprox`, but it can fold away part of the computation if
1224+
* `hi` is in fact constant.
1225+
*/
1226+
@inline def nonNegativeToDoubleApprox(lo: Int, hi: Int): Double =
1227+
hi.toDouble * TwoPow32 + uintToDouble(lo)
1228+
12181229
/** Interprets an `Int` as an unsigned integer and returns its value as a
12191230
* `Double`.
1231+
*
1232+
* In user space, this would be `Integer.toUnsignedLong(x).toDouble`.
1233+
* However, we cannot use that, since it would circle back here into an
1234+
* infinite recursion.
12201235
*/
1221-
@inline def asUint(x: Int): Double = {
1236+
@inline def uintToDouble(x: Int): Double = {
12221237
import scala.scalajs.js.DynamicImplicits.number2dynamic
12231238
(x.toDouble >>> 0).asInstanceOf[Double]
12241239
}

linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/WasmTransients.scala

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ object WasmTransients {
6060
case F64Floor => wa.F64Floor
6161
case F64Nearest => wa.F64Nearest
6262
case F64Sqrt => wa.F64Sqrt
63+
64+
case F64ConvertI32U => wa.F64ConvertI32U
6365
}
6466

6567
def printIR(out: IRTreePrinter): Unit = {
@@ -87,6 +89,8 @@ object WasmTransients {
8789
final val F64Nearest = 9
8890
final val F64Sqrt = 10
8991

92+
final val F64ConvertI32U = 11
93+
9094
def resultTypeOf(op: Code): Type = (op: @switch) match {
9195
case I32Ctz | I32Popcnt =>
9296
IntType
@@ -97,7 +101,7 @@ object WasmTransients {
97101
case F32Abs =>
98102
FloatType
99103

100-
case F64Abs | F64Ceil | F64Floor | F64Nearest | F64Sqrt =>
104+
case F64Abs | F64Ceil | F64Floor | F64Nearest | F64Sqrt | F64ConvertI32U =>
101105
DoubleType
102106
}
103107
}

linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3804,6 +3804,23 @@ private[optimizer] abstract class OptimizerCore(
38043804
PreTransLit(DoubleLiteral(v.toDouble))
38053805
case PreTransUnaryOp(IntToLong, x) =>
38063806
foldUnaryOp(IntToDouble, x)
3807+
3808+
/* (double) <toLongUnsigned>(x) --> <unsignedIntToDouble>(x)
3809+
*
3810+
* On Wasm, there is a dedicated transient. On JS, that is (x >>> 0).
3811+
*
3812+
* The latter only kicks in when using bigints for longs. When using
3813+
* RuntimeLong, we have eagerly expanded the `UnsignedIntToLong`
3814+
* operation, but further inlining and folding will yield the same
3815+
* result.
3816+
*/
3817+
case PreTransUnaryOp(UnsignedIntToLong, x) =>
3818+
val newX = finishTransformExpr(x)
3819+
val resultTree =
3820+
if (isWasm) Transient(WasmUnaryOp(WasmUnaryOp.F64ConvertI32U, newX))
3821+
else makeCast(JSBinaryOp(JSBinaryOp.>>>, newX, IntLiteral(0)), DoubleType)
3822+
resultTree.toPreTransform
3823+
38073824
case _ =>
38083825
default
38093826
}
@@ -5035,6 +5052,20 @@ private[optimizer] abstract class OptimizerCore(
50355052
case (PreTransLit(DoubleLiteral(l)), PreTransLit(DoubleLiteral(r))) =>
50365053
doubleLit(l + r)
50375054

5055+
/* ±0.0 + cast(a >>> b, DoubleType) --> cast(a >>> b, DoubleType)
5056+
*
5057+
* In general, `+0.0 + y -> y` is not a valid rewrite, because it does
5058+
* not give the same result when y is -0.0. (Paradoxically, with -0.0
5059+
* on the left it *is* a valid rewrite, though not a very useful one.)
5060+
*
5061+
* However, if y is the result of a JS `>>>` operator, we know it
5062+
* cannot be -0.0, hence the rewrite is valid. That particular shape
5063+
* appears in the inlining of `Integer.toUnsignedLong(x).toDouble`.
5064+
*/
5065+
case (PreTransLit(DoubleLiteral(0.0)), // also matches -0.0
5066+
PreTransTree(Transient(Cast(JSBinaryOp(JSBinaryOp.>>>, _, _), DoubleType)), _)) =>
5067+
rhs
5068+
50385069
case _ => default
50395070
}
50405071

project/Build.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2053,16 +2053,16 @@ object Build {
20532053
case `default212Version` =>
20542054
if (!useMinifySizes) {
20552055
Some(ExpectedSizes(
2056-
fastLink = 625000 to 626000,
2056+
fastLink = 626000 to 627000,
20572057
fullLink = 94000 to 95000,
20582058
fastLinkGz = 75000 to 79000,
20592059
fullLinkGz = 24000 to 25000,
20602060
))
20612061
} else {
20622062
Some(ExpectedSizes(
2063-
fastLink = 425000 to 426000,
2063+
fastLink = 426000 to 427000,
20642064
fullLink = 283000 to 284000,
2065-
fastLinkGz = 60000 to 61000,
2065+
fastLinkGz = 61000 to 62000,
20662066
fullLinkGz = 43000 to 44000,
20672067
))
20682068
}
@@ -2077,7 +2077,7 @@ object Build {
20772077
))
20782078
} else {
20792079
Some(ExpectedSizes(
2080-
fastLink = 301000 to 302000,
2080+
fastLink = 302000 to 303000,
20812081
fullLink = 259000 to 260000,
20822082
fastLinkGz = 47000 to 48000,
20832083
fullLinkGz = 42000 to 43000,

0 commit comments

Comments
 (0)