Skip to content

Commit c0b6ce2

Browse files
authored
Merge pull request #5194 from sjrd/no-touint-in-lib
Remove all user-space asUint/toUint in the libraries.
2 parents ab9ba4a + 5fb3aab commit c0b6ce2

File tree

9 files changed

+112
-35
lines changed

9 files changed

+112
-35
lines changed

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

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

301+
@inline private[lang] def toUnsignedDouble(x: Int): scala.Double =
302+
toUnsignedLong(x).toDouble
303+
301304
// Wasm intrinsic
302305
def bitCount(i: scala.Int): scala.Int = {
303306
/* See http://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetParallel
@@ -405,11 +408,6 @@ object Integer {
405408

406409
@inline private[this] def toStringBase(i: scala.Int, base: scala.Int): String = {
407410
import js.JSNumberOps.enableJSNumberOps
408-
asUint(i).toString(base)
409-
}
410-
411-
@inline private def asUint(n: scala.Int): scala.Double = {
412-
import js.DynamicImplicits.number2dynamic
413-
(n.toDouble >>> 0).asInstanceOf[scala.Double]
411+
toUnsignedDouble(i).toString(base)
414412
}
415413
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ package java.lang
1515
import scala.annotation.{switch, tailrec}
1616

1717
import java.lang.constant.{Constable, ConstantDesc}
18-
import java.lang.Utils.toUint
1918
import java.util.ScalaOps._
2019

2120
import scala.scalajs.js
@@ -169,7 +168,7 @@ object Long {
169168
if (hi == 0) {
170169
// It's an unsigned int32
171170
import js.JSNumberOps.enableJSNumberOps
172-
Utils.toUint(lo).toString(radix)
171+
Integer.toUnsignedDouble(lo).toString(radix)
173172
} else {
174173
toUnsignedStringInternalLarge(lo, hi, radix)
175174
}
@@ -186,7 +185,8 @@ object Long {
186185
}
187186

188187
val TwoPow32 = (1L << 32).toDouble
189-
val approxNum = toUint(hi) * TwoPow32 + toUint(lo)
188+
val approxNum =
189+
Integer.toUnsignedDouble(hi) * TwoPow32 + Integer.toUnsignedDouble(lo)
190190

191191
if ((hi & 0xffe00000) == 0) { // see RuntimeLong.isUnsignedSafeDouble
192192
// (lo, hi) is small enough to be a Double, so approxNum is exact

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: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,15 +79,19 @@ object JSNumberOps {
7979
implicit def enableJSNumberOps(x: Double): js.JSNumberOps =
8080
x.asInstanceOf[js.JSNumberOps]
8181

82+
@deprecated("Use Integer.toUnsignedLong(x).toDouble instead of toUint.", since = "1.20.0")
8283
implicit def enableJSNumberExtOps(x: Int): ExtOps =
8384
new ExtOps(x.asInstanceOf[js.Dynamic])
8485

86+
@deprecated("Use Integer.toUnsignedLong(x).toDouble instead of toUint.", since = "1.20.0")
8587
implicit def enableJSNumberExtOps(x: Double): ExtOps =
8688
new ExtOps(x.asInstanceOf[js.Dynamic])
8789

90+
@deprecated("Use Integer.toUnsignedLong(x).toDouble instead of toUint.", since = "1.20.0")
8891
final class ExtOps private[JSNumberOps] (private val self: js.Dynamic)
8992
extends AnyVal {
9093

94+
@deprecated("Use Integer.toUnsignedLong(x).toDouble instead.", since = "1.20.0")
9195
@inline def toUint: Double =
9296
(self >>> 0.asInstanceOf[js.Dynamic]).asInstanceOf[Double]
9397
}

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
@@ -671,7 +671,7 @@ object RuntimeLong {
671671
val divisorInv = 1.0 / divisor.toDouble
672672

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

@@ -695,16 +695,15 @@ object RuntimeLong {
695695
a.lo
696696

697697
@inline
698-
def toDouble(a: RuntimeLong): Double =
699-
toDouble(a.lo, a.hi)
700-
701-
private def toDouble(lo: Int, hi: Int): Double = {
698+
def toDouble(a: RuntimeLong): Double = {
699+
val lo = a.lo
700+
val hi = a.hi
702701
if (hi < 0) {
703-
// We do asUint() on the hi part specifically for MinValue
702+
// We need unsignedToDoubleApprox specifically for MinValue
704703
val neg = inline_negate(lo, hi)
705-
-(asUint(neg.hi) * TwoPow32 + asUint(neg.lo))
704+
-unsignedToDoubleApprox(neg.lo, neg.hi)
706705
} else {
707-
hi * TwoPow32 + asUint(lo)
706+
nonNegativeToDoubleApprox(lo, hi)
708707
}
709708
}
710709

@@ -761,8 +760,7 @@ object RuntimeLong {
761760
if (isUnsignedSafeDouble(abs.hi) || (abs.lo & 0xffff) == 0) abs.lo
762761
else (abs.lo & ~0xffff) | 0x8000
763762

764-
// We do asUint() on the hi part specifically for MinValue
765-
val absRes = (asUint(abs.hi) * TwoPow32 + asUint(compressedAbsLo))
763+
val absRes = unsignedToDoubleApprox(compressedAbsLo, abs.hi)
766764

767765
(if (hi < 0) -absRes else absRes).toFloat
768766
}
@@ -1205,7 +1203,7 @@ object RuntimeLong {
12051203

12061204
/** Converts an unsigned safe double into its Double representation. */
12071205
@inline def asUnsignedSafeDouble(lo: Int, hi: Int): Double =
1208-
hi * TwoPow32 + asUint(lo)
1206+
nonNegativeToDoubleApprox(lo, hi)
12091207

12101208
/** Converts an unsigned safe double into its RuntimeLong representation. */
12111209
@inline def fromUnsignedSafeDouble(x: Double): RuntimeLong =
@@ -1219,10 +1217,27 @@ object RuntimeLong {
12191217
@inline def unsignedSafeDoubleHi(x: Double): Int =
12201218
rawToInt(x / TwoPow32)
12211219

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

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: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -646,7 +646,40 @@ private[optimizer] abstract class OptimizerCore(
646646
JSUnaryOp(op, transformExpr(lhs))
647647

648648
case JSBinaryOp(op, lhs, rhs) =>
649-
JSBinaryOp(op, transformExpr(lhs), transformExpr(rhs))
649+
val newTree = JSBinaryOp(op, transformExpr(lhs), transformExpr(rhs))
650+
651+
// Introduce casts for some idioms that are guaranteed to return certain types
652+
653+
// Is `arg` guaranteed to evaluate to a JS `number` (and hence, not a `bigint`)?
654+
def isJSNumber(arg: Tree): Boolean = arg.tpe match {
655+
case IntType | DoubleType | ByteType | ShortType | FloatType => true
656+
case _ => false
657+
}
658+
659+
newTree match {
660+
/* Unless it throws, `x | y` returns either a signed 32-bit integer
661+
* (an `Int`) or a bigint.
662+
*
663+
* The only case in which it returns a bigint is when both arguments
664+
* are (convertible to) bigint's. Custom objects can be converted to
665+
* bigint's if their `valueOf()` method returns a bigint.
666+
*
667+
* Primitive numbers cannot be implicitly converted to bigint's.
668+
* `x | y` throws if one side is a number and the other is (converted
669+
* to) a bigint. Therefore, if at least one of the arguments is known
670+
* to be a primitive number, we know that `x | y` will return a
671+
* signed 32-bit integer (or throw).
672+
*/
673+
case JSBinaryOp(JSBinaryOp.|, x, y) if isJSNumber(x) || isJSNumber(y) =>
674+
makeCast(newTree, IntType)
675+
676+
// >>> always returns a positive number in the unsigned 32-bit range (it rejects bigints)
677+
case JSBinaryOp(JSBinaryOp.>>>, _, _) =>
678+
makeCast(newTree, DoubleType)
679+
680+
case _ =>
681+
newTree
682+
}
650683

651684
case JSArrayConstr(items) =>
652685
JSArrayConstr(transformExprsOrSpreads(items))
@@ -3790,6 +3823,23 @@ private[optimizer] abstract class OptimizerCore(
37903823
PreTransLit(DoubleLiteral(v.toDouble))
37913824
case PreTransUnaryOp(IntToLong, x) =>
37923825
foldUnaryOp(IntToDouble, x)
3826+
3827+
/* (double) <toLongUnsigned>(x) --> <unsignedIntToDouble>(x)
3828+
*
3829+
* On Wasm, there is a dedicated transient. On JS, that is (x >>> 0).
3830+
*
3831+
* The latter only kicks in when using bigints for longs. When using
3832+
* RuntimeLong, we have eagerly expanded the `UnsignedIntToLong`
3833+
* operation, but further inlining and folding will yield the same
3834+
* result.
3835+
*/
3836+
case PreTransUnaryOp(UnsignedIntToLong, x) =>
3837+
val newX = finishTransformExpr(x)
3838+
val resultTree =
3839+
if (isWasm) Transient(WasmUnaryOp(WasmUnaryOp.F64ConvertI32U, newX))
3840+
else makeCast(JSBinaryOp(JSBinaryOp.>>>, newX, IntLiteral(0)), DoubleType)
3841+
resultTree.toPreTransform
3842+
37933843
case _ =>
37943844
default
37953845
}
@@ -5021,6 +5071,20 @@ private[optimizer] abstract class OptimizerCore(
50215071
case (PreTransLit(DoubleLiteral(l)), PreTransLit(DoubleLiteral(r))) =>
50225072
doubleLit(l + r)
50235073

5074+
/* ±0.0 + cast(a >>> b, DoubleType) --> cast(a >>> b, DoubleType)
5075+
*
5076+
* In general, `+0.0 + y -> y` is not a valid rewrite, because it does
5077+
* not give the same result when y is -0.0. (Paradoxically, with -0.0
5078+
* on the left it *is* a valid rewrite, though not a very useful one.)
5079+
*
5080+
* However, if y is the result of a JS `>>>` operator, we know it
5081+
* cannot be -0.0, hence the rewrite is valid. That particular shape
5082+
* appears in the inlining of `Integer.toUnsignedLong(x).toDouble`.
5083+
*/
5084+
case (PreTransLit(DoubleLiteral(0.0)), // also matches -0.0
5085+
PreTransTree(Transient(Cast(JSBinaryOp(JSBinaryOp.>>>, _, _), DoubleType)), _)) =>
5086+
rhs
5087+
50245088
case _ => default
50255089
}
50265090

project/Build.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2060,8 +2060,8 @@ object Build {
20602060
))
20612061
} else {
20622062
Some(ExpectedSizes(
2063-
fastLink = 425000 to 426000,
2064-
fullLink = 282000 to 283000,
2063+
fastLink = 426000 to 427000,
2064+
fullLink = 283000 to 284000,
20652065
fastLinkGz = 61000 to 62000,
20662066
fullLinkGz = 43000 to 44000,
20672067
))
@@ -2070,7 +2070,7 @@ object Build {
20702070
case `default213Version` =>
20712071
if (!useMinifySizes) {
20722072
Some(ExpectedSizes(
2073-
fastLink = 442000 to 443000,
2073+
fastLink = 443000 to 444000,
20742074
fullLink = 90000 to 91000,
20752075
fastLinkGz = 57000 to 58000,
20762076
fullLinkGz = 24000 to 25000,

0 commit comments

Comments
 (0)