Skip to content

Introduce IR UnaryOps for floating point bit manipulation. #5158

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 21, 2025
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
10 changes: 9 additions & 1 deletion compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7421,14 +7421,22 @@ private object GenJSCode {
private lazy val JavalibMethodsWithOpBody: Map[(ClassName, MethodName), JavalibOpBody] = {
import JavalibOpBody._
import js.{UnaryOp => unop, BinaryOp => binop}
import jstpe.{BooleanRef => Z, CharRef => C, IntRef => I}
import jstpe.{BooleanRef => Z, CharRef => C, IntRef => I, LongRef => J, FloatRef => F, DoubleRef => D}
import MethodName.{apply => m}

val O = jswkn.ObjectRef
val CC = jstpe.ClassRef(jswkn.ClassClass)
val T = jstpe.ClassRef(jswkn.BoxedStringClass)

val byClass: Map[ClassName, Map[MethodName, JavalibOpBody]] = Map(
jswkn.BoxedFloatClass.withSuffix("$") -> Map(
m("floatToIntBits", List(F), I) -> ArgUnaryOp(unop.Float_toBits),
m("intBitsToFloat", List(I), F) -> ArgUnaryOp(unop.Float_fromBits)
),
jswkn.BoxedDoubleClass.withSuffix("$") -> Map(
m("doubleToLongBits", List(D), J) -> ArgUnaryOp(unop.Double_toBits),
m("longBitsToDouble", List(J), D) -> ArgUnaryOp(unop.Double_fromBits)
),
jswkn.BoxedStringClass -> Map(
m("length", Nil, I) -> ThisUnaryOp(unop.String_length),
m("charAt", List(I), C) -> ThisBinaryOp(binop.String_charAt)
Expand Down
5 changes: 5 additions & 0 deletions ir/shared/src/main/scala/org/scalajs/ir/Printers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,11 @@ object Printers {
case UnwrapFromThrowable => p("<unwrapFromThrowable>(", ")")

case Throw => p("throw ", "")

case Float_toBits => p("<floatToBits>(", ")")
case Float_fromBits => p("<floatFromBits>(", ")")
case Double_toBits => p("<doubleToBits>(", ")")
case Double_fromBits => p("<doubleFromBits>(", ")")
}

case BinaryOp(BinaryOp.Int_-, IntLiteral(0), rhs) =>
Expand Down
16 changes: 12 additions & 4 deletions ir/shared/src/main/scala/org/scalajs/ir/Trees.scala
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,14 @@ object Trees {
final val UnwrapFromThrowable = 30
final val Throw = 31

// Floating point bit manipulation, introduced in 1.20
final val Float_toBits = 32
// final val Float_toRawBits = 33 // Reserved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just out of curiosity, I believe the difference between doubleToRawBits and doubleToBits is how they handle NaN, whether they convert a given NaN to an integer using its bit pattern (raw), or if they use the bit pattern of a "normalized" NaN (like the result of 0.0 / 0.0).

If I understand correctly, are there any use cases for the toRawBits version?


Also, is it correct that "raw" NaNs typically being caused only from calculations, and as a rule of thumb, we should use normalized NaN (0.0/0.0) when we use NaNs as constants?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's correct. The specific normalized bit pattern is even spelled out in the JavaDoc.

The typical use case is if you want to implement "NaN-boxing", by storing actual data in the various payloads of NaNs. But IMO a much safer way to do that is to manipulate the Longs everywhere, and only bit-concert to Double when you know it's a number.

I have yet to see a compelling use case for the raw variants, TBH.

final val Float_fromBits = 34
final val Double_toBits = 35
// final val Double_toRawBits = 36 // Reserved
final val Double_fromBits = 37

def isClassOp(op: Code): Boolean =
op >= Class_name && op <= Class_superClass

Expand All @@ -530,13 +538,13 @@ object Trees {
case IntToShort =>
ShortType
case CharToInt | ByteToInt | ShortToInt | LongToInt | DoubleToInt |
String_length | Array_length | IdentityHashCode =>
String_length | Array_length | IdentityHashCode | Float_toBits =>
IntType
case IntToLong | DoubleToLong =>
case IntToLong | DoubleToLong | Double_toBits =>
LongType
case DoubleToFloat | LongToFloat =>
case DoubleToFloat | LongToFloat | Float_fromBits =>
FloatType
case IntToDouble | LongToDouble | FloatToDouble =>
case IntToDouble | LongToDouble | FloatToDouble | Double_fromBits =>
DoubleType
case CheckNotNull | Clone =>
argType.toNonNullable
Expand Down
5 changes: 5 additions & 0 deletions ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,11 @@ class PrintersTest {
assertPrintEquals("<wrapAsThrowable>(e)", UnaryOp(WrapAsThrowable, ref("e", AnyType)))
assertPrintEquals("<unwrapFromThrowable>(e)",
UnaryOp(UnwrapFromThrowable, ref("e", ClassType(ThrowableClass, nullable = true))))

assertPrintEquals("<floatToBits>(x)", UnaryOp(Float_toBits, ref("x", FloatType)))
assertPrintEquals("<floatFromBits>(x)", UnaryOp(Float_fromBits, ref("x", IntType)))
assertPrintEquals("<doubleToBits>(x)", UnaryOp(Double_toBits, ref("x", DoubleType)))
assertPrintEquals("<doubleFromBits>(x)", UnaryOp(Double_fromBits, ref("x", LongType)))
}

@Test def printPseudoUnaryOp(): Unit = {
Expand Down
33 changes: 27 additions & 6 deletions javalib/src/main/scala/java/lang/Double.scala
Original file line number Diff line number Diff line change
Expand Up @@ -364,14 +364,28 @@ object Double {
@inline def isFinite(d: scala.Double): scala.Boolean =
!isNaN(d) && !isInfinite(d)

/** Hash code of a number (excluding Longs).
*
* Because of the common encoding for integer and floating point values,
* the hashCode of Floats and Doubles must align with that of Ints for the
* common values.
*
* For other values, we use the hashCode specified by the JavaDoc for
* *Doubles*, even for values which are valid Float values. Because of the
* previous point, we cannot align completely with the Java specification,
* so there is no point trying to be a bit more aligned here. Always using
* the Double version requires fewer branches.
*
* We use different code paths in JS and Wasm for performance reasons.
* The two implementations compute the same results.
*/
@inline def hashCode(value: scala.Double): Int = {
if (LinkingInfo.isWebAssembly)
hashCodeForWasm(value)
else
FloatingPointBits.numberHashCode(value)
hashCodeForJS(value)
}

// See FloatingPointBits for the spec of this computation
@inline
private def hashCodeForWasm(value: scala.Double): Int = {
val bits = doubleToLongBits(value)
Expand All @@ -382,13 +396,20 @@ object Double {
Long.hashCode(bits)
}

// Wasm intrinsic
@inline
private def hashCodeForJS(value: scala.Double): Int = {
val valueInt = (value.asInstanceOf[js.Dynamic] | 0.asInstanceOf[js.Dynamic]).asInstanceOf[Int]
if (valueInt.toDouble == value && 1.0/value != scala.Double.NegativeInfinity)
valueInt
else
Long.hashCode(doubleToLongBits(value))
}

@inline def longBitsToDouble(bits: scala.Long): scala.Double =
FloatingPointBits.longBitsToDouble(bits)
throw new Error("stub") // body replaced by the compiler back-end

// Wasm intrinsic
@inline def doubleToLongBits(value: scala.Double): scala.Long =
FloatingPointBits.doubleToLongBits(value)
throw new Error("stub") // body replaced by the compiler back-end

@inline def sum(a: scala.Double, b: scala.Double): scala.Double =
a + b
Expand Down
6 changes: 2 additions & 4 deletions javalib/src/main/scala/java/lang/Float.scala
Original file line number Diff line number Diff line change
Expand Up @@ -427,13 +427,11 @@ object Float {
@inline def hashCode(value: scala.Float): Int =
Double.hashCode(value.toDouble)

// Wasm intrinsic
@inline def intBitsToFloat(bits: scala.Int): scala.Float =
FloatingPointBits.intBitsToFloat(bits)
throw new Error("stub") // body replaced by the compiler back-end

// Wasm intrinsic
@inline def floatToIntBits(value: scala.Float): scala.Int =
FloatingPointBits.floatToIntBits(value)
throw new Error("stub") // body replaced by the compiler back-end

@inline def sum(a: scala.Float, b: scala.Float): scala.Float =
a + b
Expand Down
Loading