Type-casting inside macro

Hi,

I was looking for a way to get a precise (literal-like) tuple in a type parameter of my generic type, like:

case class Foo[T](t: T)

val foo = Foo(("foo", 42))

I want above to be Foo[("foo", 42)], not Foo[(String, Int)] without additional type ascription.

I managed to do this with the following macro:

  private def preciseImpl[T <: Tuple: Type](expr: Expr[T])(using Quotes): Expr[Any] =
    import quotes.reflect.*

    val typeRepr: TypeRepr = expr.asTerm match
      case Inlined(_, Nil, Apply(TypeApply(Select(Ident(name), _), _), innerTerms)) if name.startsWith("Tuple") =>
        val arity = name.dropWhile(c => !c.isDigit).toInt
        val innerTypes = innerTerms.collect { case Literal(a) => ConstantType(a) }

        defn.TupleClass(arity).typeRef.appliedTo(innerTypes)

    typeRepr.asType match
      case '[t] => '{ ${expr}.asInstanceOf[t] }

It works to the point where, I can use it, but I’m curious about the last expression. If I change that to (seemingly more sensible):

    typeRepr.asType match
      case '[t] => expr.asExprOf[t]

It throws an exception:

[error] Exception occurred while executing macro expansion.
[error] java.lang.Exception: Expr cast exception: scala.Tuple2.apply[java.lang.String, scala.Int]("foo", 42)
[error] of type: scala.Tuple2[java.lang.String, scala.Int]
[error] did not conform to type: scala.Tuple2["foo", 42]

Why is that?

Those two expressions are not the same. In the first case you create an expression that calls asInstanceOf[t] during run-time. The second, is done during compilation.

During compilation, Scala widens the type so you cannot cast it to the more specific type. As to why this does not fail during run-time, I do not know.

So, if I’m getting this right the macro-expansion stage happens after type-widening stage and information about more specific type is already lost. It kind of make sense even though it has the full expression of Tuple2.apply[String, Int]("foo", 42) - maybe it could defer the specific type again?

Anyways, I’m more concerned about the fact that I use this asInstanceOf-inside-quotes pattern quite often in different scenarios. I don’t see why it would fail at runtime - I’m quite certain that the type is correct and I’m wondering if there’s any downsides of that pattern that can fire back.

Yes, the types are always widened. Keeping the singleton type has been discussed and it has been explained that changing this behavior is not possible because it would require changes to the typing system.

The use of asInstanceOf may not be required. Have you tried:

 typeRepr.asType match
      case '[t] => '{ 
           val v: t = ${expr}
           v
       }