Scala 3 Macros - quotes: how to match scala.Array(1,2,3))

I have asked this question in the metaprogramming discord channel and the Github discussion forum, but have had no response. So I am trying anew here.

Assume I have the following code:

    val s1 = ConstArray.testUnapply(Array(1, 2, 3))

I would like to match that array in the testUnapply(Array(1, 2, 3)) macro. The following match using quotes work:

      (x: Expr[Array[T]]) match
        case '{ scala.Array.apply($a:Int, $b:Int, $c:Int): scala.Array[Int]}  => 
          println(s"(a, b, c) = ($a, $b, $c)") 
          ???

However, I need to deal with a variable number of arguments. So I tried many variations that include:

        case '{ scala.Array.apply(${Varargs(Exprs(elems))}: Seq[Int])}  =>  
=>  
          Some(Array(elems*).asInstanceOf[Array[T]])
          ???

None of these work. I have also noticed that the following fails:

        case '{ scala.Array.apply[Int]($a:Int, $b:Int, $c:Int): scala.Array[Int]}  => 
          println(s"(a, b, c) = ($a, $b, $c)") 
          ???

Why does this fail? Do I have to add something related to to ClassTag? The following also fails:

      val fct = summon[FromExpr[ClassTag[Int]]]
      (x: Expr[Array[T]]) match
        case '{ scala.Array.apply( ${varArgs} : Seq[Int])(using $fct) }  => 
          ???

Note that a show prints :

scala.Array.apply(1, 2, 3)

and the structure is:

Apply(Select(Ident("Array"), "apply"), List(Literal(IntConstant(1)), Typed(Repeated(List(Literal(IntConstant(2)), Literal(IntConstant(3))), Inferred()), Inferred())))

I have used the same technique for Seq and List with no problems.

Any help is appreciated.

TIA

2 Likes
given ArrayIntFromExpr: FromExpr[Array[Int]] with
  def unapply(x: Expr[Array[Int]])(using Quotes) = x match
    case '{ scala.Array($h: Int, (${Varargs[Int](Exprs[Int](elems))}: Seq[Int])*) } => Some(Array(h.valueOrAbort, elems *))
    case _ => None

As you showed in the tree, Array(1,2,3) is apply(Int, Int*).

The generic array needs the class tag, including for empty array.

given ArrayFromExpr[T](using Type[T], FromExpr[T])(using reflect.ClassTag[T]): FromExpr[Array[T]] with
  def unapply(x: Expr[Array[T]])(using Quotes) = x match
    case '{ scala.Array[T](${Varargs[T](Exprs[T](elems))}*)($ct) } => Some(Array(elems*))
    case _ => None

This is my first time learning the API, so thanks for the impetus.

def _arraysum(xs: Expr[Array[Int]])(using Quotes): Expr[Int] =
  xs match
  case Expr(vs) => Expr(vs.sum)
  case '{ $ys: Array[Int] } => '{ $ys.sum }

In this case, it could do elems.sum directly since the array is not of interest.

Tree at inlining showing both cases in a unit test; also showing that JUnit 4 does not have assertEquals(Int, Int).

[info]         org.junit.Assert.assertEquals(10L, Int.int2long(10:Int))
[info]         org.junit.Assert.assertEquals(10L,
[info]           Int.int2long(
[info]             wrapIntArray(Array.apply(1, [2,3,this.i : Int]*)).sum[Int](
[info]               scala.math.Numeric.IntIsIntegral):Int
[info]           )
[info]         )
1 Like

@som-snytt Thank you very much for taking the time to look at this. I must admit that I don’t understand how your test gets to both match cases. I am assuming that test is not shown.

However I have some questions. The first is that I had the match case you show in ArrayFromExpr. I copied and pasted again and it worked. However, we have the $ct evidence and I do not see where it is obtained or declared (I had this previously explicitly declared/summoned and it works as expected).

The second questions is that _arraysum expects an Expr[Array[Int]] . This is what has me stumped. My input can be any T. To better understand, I use this test code:

  transparent inline def testUnapply[T](x: Array[T]) = 
    ${testUnapplyImpl('x)}

  def testUnapplyImpl[T:Type](x: Expr[Array[T]])(using q:Quotes): Expr[String] =
    x match
      case ConstArray(e) => 
        val c = e.getClass.getName()
        val t = Type.show[T]
        Expr(s"$e:$c[$t]")
      case _ => 
        Expr("")

The above unapply is equivalent to your _arraysum but accepts any T and returns an Array[T]. What I wanted was to have a single match as used by ArrayFromExpr to handle all cases of the primitive types. Currently I can use the ArrayIntFromExpr-like match to process the primitive types (one for each type) and ArrayFromExpr to match all others. I see now that it may not be possible to use a single generic match because we have specific declarations for those. If this is incorrect, please correct me.

Thanks for the help.

TIA

To respond to #2, I couldn’t get it to match the primitive signatures generically. (Probably you tried the same permutations of features.)

My probably wrong intuition is that I just want to match the shape of the tree; I don’t care about the types or which method is selected when compiling Array.apply.

def _arraylen(xs: Expr[Array[?]])(using Quotes): Expr[Int] =
  xs match
  //case '{ Array(${_}: A, (${Varargs(elems)}: Seq[A])*) } => Expr(elems.length + 1)
  case '{ Array(${_}: Int, (${Varargs(elems)}: Seq[Int])*) } => Expr(elems.length + 1)
  case '{ Array[a]((${Varargs(elems)}: Seq[a])*)($_) } => Expr(elems.length)
  case _ => Expr(-1)

loosely

def _arraylen(xs: Expr[Array[?]])(using Quotes): Expr[Int] =
  import quotes.reflect.*
  xs match
  case '{ Array[a]((${Varargs(elems)}: Seq[a])*)($_) } => Expr(elems.length)
  case _ =>
    xs.asTerm match
    case Inlined(_, _, Apply(_, List(elem, Typed(Repeated(elems, _), _)))) => Expr(elems.length + 1)
    case _ => Expr(-1)

For #1, there is an incoming class tag. Here are the overloads as listed by an error message:

[error] 33 |  case '{ Array(${_}: Int, (${Varargs(elems)}: Seq[?])*) } => Expr(elems.length + 1)
[error]    |          ^^^^^
[error]    |None of the overloaded alternatives of method apply in object Array with types
[error]    | (x: Unit, xs: Unit*): Array[Unit]
[error]    | (x: Double, xs: Double*): Array[Double]
[error]    | (x: Float, xs: Float*): Array[Float]
[error]    | (x: Long, xs: Long*): Array[Long]
[error]    | (x: Int, xs: Int*): Array[Int]
[error]    | (x: Char, xs: Char*): Array[Char]
[error]    | (x: Short, xs: Short*): Array[Short]
[error]    | (x: Byte, xs: Byte*): Array[Byte]
[error]    | (x: Boolean, xs: Boolean*): Array[Boolean]
[error]    | [T](xs: T*)(implicit evidence$5: scala.reflect.ClassTag[T]): Array[T]
[error]    |match arguments (Int, *)

In _arraylen, the implicit arg is matched by $_.

That arg is not terribly useful if I want to create an array in the macro, however. Probably it’s necessary to switch on the Type, or maybe it’s possible to get a ClassTag from a Type.

I see they have tutorial materials and examples, which is easier than just reading the reference, and I also meant to follow Nicolas Stucki. There’s Madrid and also his defense. With a so-called academic language, it’s a good idea to hold off learning new stuff until after they defend their dissertation.

Thanks for the feedback.

In regards to #2 I also considered the AST. In fact I had a “length” function pretty much the same as your second example. As per the “best practice”, I am trying to avoid ASTs.

As for #1, yes I can can match on the type. The issue I wanted to avoid is repeatedly matching on the same quoted patterns whose only difference are the types. I could do this for other collection types.

Yes, you can get the class tag from the type (although you cannot get it from the call site directly). But this does not help with matching on the primitive types generically.

The links you provide, I had already explored. Only the defense I had not seen before.

I guess I will do matching for the primitive types.

Thanks once again.