Skip to content

asInstanceOf inlining causes the difference in result #13103

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

Closed
REASY opened this issue May 9, 2025 · 4 comments
Closed

asInstanceOf inlining causes the difference in result #13103

REASY opened this issue May 9, 2025 · 4 comments

Comments

@REASY
Copy link

REASY commented May 9, 2025

The code below generates bytecode that makes asOptBoolean1 and asOptBoolean2 to not produce the same result in case if null is provided. asOptBoolean1 will never return None.

class TestRow(values: Array[Any]) {
  def get(i: Int): Any = values(i)

  def getAs[T](i: Int): T = get(i).asInstanceOf[T]
}

object Main {
  private def asOptBoolean1(r: TestRow, index: Int): Option[Boolean] = {
    val bool = r.getAs[Boolean](index)
    Option(bool)
  }

  private def asOptBoolean2(r: TestRow, index: Int): Option[Boolean] = {
    Option(r.getAs[Boolean](index))
  }

  def main(args: Array[String]): Unit = {
    val row    = new TestRow(Array(null, true, false))
    println(s"asOptBoolean1 at 0: ${asOptBoolean1(row, 0)}")
    println(s"asOptBoolean1 at 1: ${asOptBoolean1(row, 1)}")

    println(s"asOptBoolean2 at 0: ${asOptBoolean2(row, 0)}")
    println(s"asOptBoolean2 at 1: ${asOptBoolean2(row, 1)}")
  }
}

It produces the following output

asOptBoolean1 at 0: Some(false)
asOptBoolean1 at 1: Some(true)
asOptBoolean2 at 0: None
asOptBoolean2 at 1: Some(true)

The generated bytecode https://godbolt.org/z/sGhz9WYh9, as you can see there is boxing/unboxing in asOptBoolean1 that causes null to become false as primitive

Image

I use Scala 2.13.15

@REASY REASY changed the title asInstanceOf inlining? asInstanceOf inlining causes the difference in result May 9, 2025
@sjrd
Copy link
Member

sjrd commented May 9, 2025

Wrong asInstanceOf are undefined behavior. They can do whatever they want, including at a distance.

Your asInstanceOf[Boolean] is wrong for a null value, so that's on you. You have to use java.lang.Boolean instead, which admits null as a valid value.

@sjrd sjrd closed this as not planned Won't fix, can't repro, duplicate, stale May 9, 2025
@REASY
Copy link
Author

REASY commented May 9, 2025

Hi, @sjrd

I quickly tried what you suggested java.lang.Boolean, however, it does not become None because of implicit scala.Predef.Boolean2boolean, https://godbolt.org/z/hqx7TzMhP

class TestRow(values: Array[Any]) {
  def get(i: Int): Any = values(i)

  def getAs[T](i: Int): T = get(i).asInstanceOf[T]
}

object Main {
  private def asOptBoolean1(r: TestRow, index: Int): Option[Boolean] = {
    val bool = r.getAs[Boolean](index)
    Option(bool)
  }

  private def asOptBoolean2(r: TestRow, index: Int): Option[Boolean] = {
    Option(r.getAs[Boolean](index))
  }

  private def asOptJavaBoolean1(r: TestRow, index: Int): Option[Boolean] = {
    val bool = r.getAs[java.lang.Boolean](index)
    Option(bool)
  }

  private def asOptJavaBoolean2(r: TestRow, index: Int): Option[Boolean] = {
    Option(r.getAs[java.lang.Boolean](index))
  }

  def main(args: Array[String]): Unit = {
    val row    = new TestRow(Array(null, true, false))
    println(s"asOptBoolean1 at 0: ${asOptBoolean1(row, 0)}")
    println(s"asOptBoolean1 at 1: ${asOptBoolean1(row, 1)}")
    println()

    println(s"asOptBoolean2 at 0: ${asOptBoolean2(row, 0)}")
    println(s"asOptBoolean2 at 1: ${asOptBoolean2(row, 1)}")
    println()

    println(s"asOptJavaBoolean1 at 0: ${asOptJavaBoolean1(row, 0)}")
    println(s"asOptJavaBoolean1 at 1: ${asOptJavaBoolean1(row, 1)}")
    println()

    println(s"asOptJavaBoolean2 at 0: ${asOptJavaBoolean2(row, 0)}")
    println(s"asOptJavaBoolean2 at 1: ${asOptJavaBoolean2(row, 1)}")
  }
}

Produces

asOptBoolean1 at 0: Some(false)
asOptBoolean1 at 1: Some(true)

asOptBoolean2 at 0: None
asOptBoolean2 at 1: Some(true)

asOptJavaBoolean1 at 0: Some(false)
asOptJavaBoolean1 at 1: Some(true)

asOptJavaBoolean2 at 0: Some(false)
asOptJavaBoolean2 at 1: Some(true)

@Ichoran
Copy link

Ichoran commented May 9, 2025

To unpack this a little bit more, asInstanceOf is type hint; it doesn't (usually) do any work at runtime. However, when you assign to a variable, it reads the type, goes, "Oh, okay, Boolean", makes space for a Boolean, and then, because it knows it has an Object actually because it's working with generic code, has to convert the generic into a Boolean. All fine so far.

But then the question is, if you've promised a Boolean stored in an Object, what might it be? Well, it might be an uninitialized value of type Boolean. But an uninitialized object is null. So if you're promised a Boolean and you find null, it's probably supposed to be false. Sensible enough default behavior. (And yes, it is relied upon.)

Now, what if you want to prove at runtime that you have something of a type? You use ClassTag for that. And that will map null to None. So

def getAs[T](i: Int)(implicit tag: reflect.ClassTag[T]) = tag.unapply(get(i))

is probably what you want.

@som-snytt
Copy link

Unboxing behavior is consistent with the definition of null in the spec https://scala-lang.org/files/archive/spec/2.13/06-expressions.html#the-null-value.

ClassTag#unapply is consistent with pattern matching https://scala-lang.org/files/archive/spec/2.13/08-pattern-matching.html#type-patterns.

scala> def f[A: reflect.ClassTag](a: A) = a match { case x: A => x case _ => ??? }
1 warning found
-- [E121] Pattern Match Warning: -----------------------------------------------
1 |def f[A: reflect.ClassTag](a: A) = a match { case x: A => x case _ => ??? }
  |                                                                 ^
  |Unreachable case except for null (if this is intentional, consider writing case null => instead).
def f[A](a: A)(using evidence$1: scala.reflect.ClassTag[A]): A

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants