<NME> XPathFunctionAnnotationMacro.scala
<BEF> package org.orbeon.macros

import cats.syntax.option._
import org.orbeon.oxf.util.CoreUtils.BooleanOps

import scala.annotation.{StaticAnnotation, compileTimeOnly, tailrec}
import scala.language.experimental.macros
import scala.reflect.macros.whitebox


// The purpose of this macro is to create bridges to the Saxon XPath library simply using
// the `@XPathFunction` annotation placed on a method. The macro analyzes the method's
// signature, and:
//
// - builds a call to a `register()` function that must be in scope, typically because the
//   methods are in a trait or class extending `OrbeonFunctionLibrary` or `BuiltInFunctionSet`.
//     - create one `register()` call for each arity of the function, where there can be
//       multiple arities depending on whether the method has one more more parameters with
//       default values
// - creates a new local class that extends `FunctionSupport2` and implements the `call()`
//   method
//     - parameters and the return value are converted using support in `FunctionSupport2`
// - locally scopes implicits for `XPathContext` and `XFormsFunction.Context`
//     - these are inserted only if there is at least one implicit parameter list
//
// The original methods are not removed from the original scope. (TODO)
//
// The XPath function is named by kebab-casing the Scala method name. You can override this
// by passing a `name` parameter to the annotation.
//
// Supported types so far:
//
// - `Int`
// - `Long`
// - `Double`
// - `String`
// - `Boolean`
// - `Item`
// - `NodeInfo`
// - `AtomicValue`
// - `Instant`
// - `Option`
// - `Iterable`
//
// This allows writing new XPath functions in a very lightweight and safe way.
//
// The only drawback we see if that macro annotations are not supported by Scala 3. But moving
// to Scala 3 is still quite a way off, and we hope that a solution will be available at some
// point. In the worst case scenario, XPath functions interfaces will have to be remain written
// with Scala 2 for longer.
//
// TODO:
//
// - [ ] support `Map` and `Array` types
// - [ ] rename `FunctionSupport2`
// - [ ] rename `XFormsFunction.Context`
// - [ ] avoid copying the method!
// - [ ] function flags (focus, etc.)
// - [ ] allow specifying super trait like `InstanceTrait` for `PathMapXPathAnalysisBuilder`
//
@compileTimeOnly("enable macro paradise to expand macro annotations")
class XPathFunction(name: String = null) extends StaticAnnotation {
  def macroTransform(annottees: Any*): Any = macro XPathFunctionAnnotationMacro.impl
}

object XPathFunctionAnnotationMacro {

  def impl(c: whitebox.Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {

    import c.universe._

    val results = {

      val (methodName, tpes, regularArgs, implicitArgs, returnType, body) =
        annottees.map(_.tree).toList match {
          case q"$mods def $methodName[..$tpes](..$args)(implicit ..$implicitArgs): $returnType = { ..$body }" :: Nil if implicitArgs.nonEmpty =>
            // Not sure why I am getting a match sometimes, but not always, with `implicitArgs.isEmpty`.
            // So adding above the check for `implicitArgs.nonEmpty`.
            (methodName, tpes, args, implicitArgs.some, returnType, body)
          case q"$mods def $methodName[..$tpes](...$args): $returnType = { ..$body }" :: Nil =>
            (methodName, tpes, args.flatten, None, returnType, body)
          case _ => c.abort(c.enclosingPosition, "Annotation `@XPathFunction` can be used only on methods")
        }

      val kebabMethodNameString = camelToKebab(methodName.toString)

      // See https://stackoverflow.com/questions/32631372/getting-parameters-from-scala-macro-annotation
      val resolvedFunctionName =
        c.prefix.tree match {
          case q"new XPathFunction(name = $nameArg)" => nameArg
          case q"new XPathFunction($nameArg)"        => nameArg
          case _                                     => c.abort(c.enclosingPosition, "Annotation `@XPathFunction` has incorrect parameters")
        }

      val (returnTypeIsOption, returnTypeString) =
        returnType match {
          case tq"Option[Int]"               => (true,  "int")
          case tq"Option[String]"            => (true,  "string")
          case tq"Option[Boolean]"           => (true,  "boolean")
          case tq"Option[java.time.Instant]" => (true,  "instant")
          case tq"Option[$tpe]"              => (true,  "item")
          case tq"Int"                       => (false, "int")
          case tq"String"                    => (false, "string")
          case tq"Boolean"                   => (false, "boolean")
          case tq"java.time.Instant"         => (false, "instant")
          case tq"$tpe"                      => (false, "item")
        }

      val returnTypeCard =
        if (returnTypeIsOption)
          q"""org.orbeon.saxon.functions.registry.BuiltInFunctionSet.OPT"""
        else
          q"""org.orbeon.saxon.functions.registry.BuiltInFunctionSet.ONE"""


        regularArgs.zipWithIndex map {
          case (arg: ValDef, argPosInList) =>

            val (isOption, typeString) =
              arg match {
                case q"$mods val $name: Option[Int]               = $rhs" => (true,  "int")
                case q"$mods val $name: Option[String]            = $rhs" => (true,  "string")
                case q"$mods val $name: Option[Boolean]           = $rhs" => (true,  "boolean")
                case q"$mods val $name: Option[java.time.Instant] = $rhs" => (true,  "instant")
                case q"$mods val $name: Option[$tpe]              = $rhs" => (true,  "item")
                case q"$mods val $name: Int                       = $rhs" => (false, "int")
                case q"$mods val $name: String                    = $rhs" => (false, "string")
                case q"$mods val $name: Boolean                   = $rhs" => (false, "boolean")
                case q"$mods val $name: java.time.Instant         = $rhs" => (false, "instant")
                case q"$mods val $name: $tpe                      = $rhs" => (false, "item")
              }

            val (decodeCall, defaultOpt) = {
            val (decodeCall, defaultOpt) = {
              (q"""decodeSaxonArg[$tpt](args($argPosInList))""", rhs != EmptyTree option rhs)
            }

            (argPosInList, isOption, typeString, decodeCall, defaultOpt)

          case _ => throw new IllegalArgumentException
      }

      val minArity = argumentsDetails.count(_._5.isEmpty)
      val maxArity = argumentsDetails.size

      if (argumentsDetails.takeWhile(_._5.isEmpty).size != minArity)
        throw new IllegalArgumentException(s"arguments with default values must be last")

      def getSaxonType(typeString: String) =
      def getSaxonType(typeString: String) =
        typeString match {
          case "int"     => q"""org.orbeon.saxon.model.BuiltInAtomicType.INTEGER"""
          case "string"  => q"""org.orbeon.saxon.model.BuiltInAtomicType.STRING"""
          case "boolean" => q"""org.orbeon.saxon.model.BuiltInAtomicType.BOOLEAN"""
          case "instant" => q"""org.orbeon.saxon.model.BuiltInAtomicType.DATE_TIME"""
          case "item"    => q"""org.orbeon.saxon.model.AnyItemType"""
        }

      // Register one entry per distinct arity
      for (arity <- minArity to maxArity) yield {

        val classNameWithArity          = methodName.toString + "_xpathFunction" + arity
        val flattenedArgumentsUpToArity = argumentsDetails.take(arity)

        val register =
          q"""
            register(
              $resolvedFunctionName,
              $arity,
              () => new ${TypeName(classNameWithArity)},
              ${getSaxonType(returnTypeString)},
              $returnTypeCard,
              org.orbeon.saxon.functions.registry.BuiltInFunctionSet.CITEM | org.orbeon.saxon.functions.registry.BuiltInFunctionSet.LATE
           """

        val registerWithArgs =
          flattenedArgumentsUpToArity.foldLeft(register) { case (result, (pos, isOption, typeString, _, _)) =>

            val card =
              if (isOption)
                q"""org.orbeon.saxon.functions.registry.BuiltInFunctionSet.OPT"""
              else
                q"""org.orbeon.saxon.functions.registry.BuiltInFunctionSet.ONE"""

                q"""org.orbeon.saxon.functions.registry.BuiltInFunctionSet.ONE"""

            q"""
               $result.arg(
                 $pos,
                 ${getSaxonType(typeString)},
                 $card,
             """
          }

        val flattenedTrees   = flattenedArgumentsUpToArity.map(_._4)
        val defaultArgsTrees = argumentsDetails.drop(arity).flatMap(_._5)

        val encodedCall =
          if (regularArgs.isEmpty) { // no parameter list!
        val encodedCall =
          if (regularArgs.isEmpty) { // no parameter list!
            q"""
              encodeSaxonArg[$returnType](
                fn[..$tpes]
              )
             """
          } else {
            q"""
              encodeSaxonArg[$returnType](
                fn[..$tpes](
                  ..$flattenedTrees,..$defaultArgsTrees
                )
              )
            """
          }

        // We use `...$args` which means `Iterable[Iterable[Tree]]`
        implicitArgs match {
          case None =>
            q"""
              $registerWithArgs

              class ${TypeName(classNameWithArity)} extends org.orbeon.oxf.xml.FunctionSupport2 {
                def call(
                  context: org.orbeon.saxon.expr.XPathContext,
                  args   : Array[org.orbeon.saxon.om.Sequence]
                ): org.orbeon.saxon.om.Sequence = {

                  def fn[..$tpes](..$regularArgs): $returnType = {..$body}

                  import org.orbeon.oxf.xml.FunctionSupport2._

                  $encodedCall
                }
              }
            """
          case Some(implicitArgs) =>
            q"""
              $registerWithArgs

              class ${TypeName(classNameWithArity)} extends org.orbeon.oxf.xml.FunctionSupport2 {
                def call(
                  context: org.orbeon.saxon.expr.XPathContext,
                  args   : Array[org.orbeon.saxon.om.Sequence]
                ): org.orbeon.saxon.om.Sequence = {

                  def fn[..$tpes](..$regularArgs)(..$implicitArgs): $returnType = {..$body}

                  locally {
                    implicit val xpathContext : org.orbeon.saxon.expr.XPathContext = context
                    implicit val xformsContext: org.orbeon.oxf.xforms.function.XFormsFunction.Context =
                      org.orbeon.oxf.xforms.function.XFormsFunction.context

                    import org.orbeon.oxf.xml.FunctionSupport2._

                    $encodedCall
                  }
                }
              }
            """
        }
      }
    }

    // TODO: Would like to keep `..$annottees` but that gets scope into a block?
    val result = q"""..$results"""

//    println("`XPathFunctionAnnotationMacro` result:")
//    println(result)

    c.Expr[Any](result)
  }

  // Inspired from function found online
  private def camelToKebab(name: String): String = {
    @tailrec
    def recurse(accDone: List[Char], acc: List[Char]): List[Char] = acc match {
      case Nil => accDone
      case a :: b :: c :: tail if a.isUpper && b.isUpper && c.isLower => recurse(accDone ++ List(a, '-', b, c), tail)
      case      a :: b :: tail if a.isLower && b.isUpper              => recurse(accDone ++ List(a, '-', b),    tail)
      case           a :: tail                                        => recurse(accDone :+ a,                  tail)
    }
    recurse(Nil, name.toList).mkString.toLowerCase
  }
}

<MSG> Macro: register proper types for `List` and `Iterable`
<DFF> @@ -92,23 +92,35 @@ object XPathFunctionAnnotationMacro {
           case _                                     => c.abort(c.enclosingPosition, "Annotation `@XPathFunction` has incorrect parameters")
         }
 
-      val (returnTypeIsOption, returnTypeString) =
+      val (returnTypeIsOption, returnTypeIsIterable, returnTypeString) =
         returnType match {
-          case tq"Option[Int]"               => (true,  "int")
-          case tq"Option[String]"            => (true,  "string")
-          case tq"Option[Boolean]"           => (true,  "boolean")
-          case tq"Option[java.time.Instant]" => (true,  "instant")
-          case tq"Option[$tpe]"              => (true,  "item")
-          case tq"Int"                       => (false, "int")
-          case tq"String"                    => (false, "string")
-          case tq"Boolean"                   => (false, "boolean")
-          case tq"java.time.Instant"         => (false, "instant")
-          case tq"$tpe"                      => (false, "item")
+          case tq"Option[Int]"                 => (true,  false, "int")
+          case tq"Option[String]"              => (true,  false, "string")
+          case tq"Option[Boolean]"             => (true,  false, "boolean")
+          case tq"Option[java.time.Instant]"   => (true,  false, "instant")
+          case tq"Option[$tpe]"                => (true,  false, "item")
+          case tq"Iterable[Int]"               => (false, true,  "int")
+          case tq"Iterable[String]"            => (false, true,  "string")
+          case tq"Iterable[Boolean]"           => (false, true,  "boolean")
+          case tq"Iterable[java.time.Instant]" => (false, true,  "instant")
+          case tq"Iterable[$tpe]"              => (false, true,  "item")
+          case tq"List[Int]"                   => (false, true,  "int")
+          case tq"List[String]"                => (false, true,  "string")
+          case tq"List[Boolean]"               => (false, true,  "boolean")
+          case tq"List[java.time.Instant]"     => (false, true,  "instant")
+          case tq"List[$tpe]"                  => (false, true,  "item")
+          case tq"Int"                         => (false, false, "int")
+          case tq"String"                      => (false, false, "string")
+          case tq"Boolean"                     => (false, false, "boolean")
+          case tq"java.time.Instant"           => (false, false, "instant")
+          case tq"$tpe"                        => (false, false, "item")
         }
 
       val returnTypeCard =
         if (returnTypeIsOption)
           q"""org.orbeon.saxon.functions.registry.BuiltInFunctionSet.OPT"""
+        else if (returnTypeIsIterable)
+          q"""org.orbeon.saxon.functions.registry.BuiltInFunctionSet.STAR"""
         else
           q"""org.orbeon.saxon.functions.registry.BuiltInFunctionSet.ONE"""
 
@@ -116,18 +128,28 @@ object XPathFunctionAnnotationMacro {
         regularArgs.zipWithIndex map {
           case (arg: ValDef, argPosInList) =>
 
-            val (isOption, typeString) =
+            val (isOption, isIterable, typeString) =
               arg match {
-                case q"$mods val $name: Option[Int]               = $rhs" => (true,  "int")
-                case q"$mods val $name: Option[String]            = $rhs" => (true,  "string")
-                case q"$mods val $name: Option[Boolean]           = $rhs" => (true,  "boolean")
-                case q"$mods val $name: Option[java.time.Instant] = $rhs" => (true,  "instant")
-                case q"$mods val $name: Option[$tpe]              = $rhs" => (true,  "item")
-                case q"$mods val $name: Int                       = $rhs" => (false, "int")
-                case q"$mods val $name: String                    = $rhs" => (false, "string")
-                case q"$mods val $name: Boolean                   = $rhs" => (false, "boolean")
-                case q"$mods val $name: java.time.Instant         = $rhs" => (false, "instant")
-                case q"$mods val $name: $tpe                      = $rhs" => (false, "item")
+                case q"$mods val $name: Option[Int]                 = $rhs" => (true,  false, "int")
+                case q"$mods val $name: Option[String]              = $rhs" => (true,  false, "string")
+                case q"$mods val $name: Option[Boolean]             = $rhs" => (true,  false, "boolean")
+                case q"$mods val $name: Option[java.time.Instant]   = $rhs" => (true,  false, "instant")
+                case q"$mods val $name: Option[$tpe]                = $rhs" => (true,  false, "item")
+                case q"$mods val $name: Iterable[Int]               = $rhs" => (false, true,  "int")
+                case q"$mods val $name: Iterable[String]            = $rhs" => (false, true,  "string")
+                case q"$mods val $name: Iterable[Boolean]           = $rhs" => (false, true,  "boolean")
+                case q"$mods val $name: Iterable[java.time.Instant] = $rhs" => (false, true,  "instant")
+                case q"$mods val $name: Iterable[$tpe]              = $rhs" => (false, true,  "item")
+                case q"$mods val $name: List[Int]                   = $rhs" => (false, true,  "int")
+                case q"$mods val $name: List[String]                = $rhs" => (false, true,  "string")
+                case q"$mods val $name: List[Boolean]               = $rhs" => (false, true,  "boolean")
+                case q"$mods val $name: List[java.time.Instant]     = $rhs" => (false, true,  "instant")
+                case q"$mods val $name: List[$tpe]                  = $rhs" => (false, true,  "item")
+                case q"$mods val $name: Int                         = $rhs" => (false, false, "int")
+                case q"$mods val $name: String                      = $rhs" => (false, false, "string")
+                case q"$mods val $name: Boolean                     = $rhs" => (false, false, "boolean")
+                case q"$mods val $name: java.time.Instant           = $rhs" => (false, false, "instant")
+                case q"$mods val $name: $tpe                        = $rhs" => (false, false, "item")
               }
 
             val (decodeCall, defaultOpt) = {
@@ -135,15 +157,15 @@ object XPathFunctionAnnotationMacro {
               (q"""decodeSaxonArg[$tpt](args($argPosInList))""", rhs != EmptyTree option rhs)
             }
 
-            (argPosInList, isOption, typeString, decodeCall, defaultOpt)
+            (argPosInList, isOption, isIterable, typeString, decodeCall, defaultOpt)
 
           case _ => throw new IllegalArgumentException
       }
 
-      val minArity = argumentsDetails.count(_._5.isEmpty)
+      val minArity = argumentsDetails.count(_._6.isEmpty)
       val maxArity = argumentsDetails.size
 
-      if (argumentsDetails.takeWhile(_._5.isEmpty).size != minArity)
+      if (argumentsDetails.takeWhile(_._6.isEmpty).size != minArity)
         throw new IllegalArgumentException(s"arguments with default values must be last")
 
       def getSaxonType(typeString: String) =
@@ -174,11 +196,13 @@ object XPathFunctionAnnotationMacro {
            """
 
         val registerWithArgs =
-          flattenedArgumentsUpToArity.foldLeft(register) { case (result, (pos, isOption, typeString, _, _)) =>
+          flattenedArgumentsUpToArity.foldLeft(register) { case (result, (pos, isOption, isIterable, typeString, _, _)) =>
 
             val card =
               if (isOption)
                 q"""org.orbeon.saxon.functions.registry.BuiltInFunctionSet.OPT"""
+              else if (isIterable)
+                q"""org.orbeon.saxon.functions.registry.BuiltInFunctionSet.STAR"""
               else
                 q"""org.orbeon.saxon.functions.registry.BuiltInFunctionSet.ONE"""
 
@@ -192,8 +216,8 @@ object XPathFunctionAnnotationMacro {
              """
           }
 
-        val flattenedTrees   = flattenedArgumentsUpToArity.map(_._4)
-        val defaultArgsTrees = argumentsDetails.drop(arity).flatMap(_._5)
+        val flattenedTrees   = flattenedArgumentsUpToArity.map(_._5)
+        val defaultArgsTrees = argumentsDetails.drop(arity).flatMap(_._6)
 
         val encodedCall =
           if (regularArgs.isEmpty) { // no parameter list!

{'commit': 'diff --git a/core-cross-platform/shared/src/main/scala/org/orbeon/macros/XPathFunctionAnnotationMacro.scala b/core-cross-platform/shared/src/main/scala/org/orbeon/macros/XPathFunctionAnnotationMacro.scala\nindex a93c85f5af..22d421ca5a 100644\n--- a/core-cross-platform/shared/src/main/scala/org/orbeon/macros/XPathFunctionAnnotationMacro.scala\n+++ b/core-cross-platform/shared/src/main/scala/org/orbeon/macros/XPathFunctionAnnotationMacro.scala\n@@ -92,23 +92,35 @@ object XPathFunctionAnnotationMacro {\n           case _                                     => c.abort(c.enclosingPosition, "Annotation `@XPathFunction` has incorrect parameters")\n         }\n \n-      val (returnTypeIsOption, returnTypeString) =\n+      val (returnTypeIsOption, returnTypeIsIterable, returnTypeString) =\n         returnType match {\n-          case tq"Option[Int]"               => (true,  "int")\n-          case tq"Option[String]"            => (true,  "string")\n-          case tq"Option[Boolean]"           => (true,  "boolean")\n-          case tq"Option[java.time.Instant]" => (true,  "instant")\n-          case tq"Option[$tpe]"              => (true,  "item")\n-          case tq"Int"                       => (false, "int")\n-          case tq"String"                    => (false, "string")\n-          case tq"Boolean"                   => (false, "boolean")\n-          case tq"java.time.Instant"         => (false, "instant")\n-          case tq"$tpe"                      => (false, "item")\n+          case tq"Option[Int]"                 => (true,  false, "int")\n+          case tq"Option[String]"              => (true,  false, "string")\n+          case tq"Option[Boolean]"             => (true,  false, "boolean")\n+          case tq"Option[java.time.Instant]"   => (true,  false, "instant")\n+          case tq"Option[$tpe]"                => (true,  false, "item")\n+          case tq"Iterable[Int]"               => (false, true,  "int")\n+          case tq"Iterable[String]"            => (false, true,  "string")\n+          case tq"Iterable[Boolean]"           => (false, true,  "boolean")\n+          case tq"Iterable[java.time.Instant]" => (false, true,  "instant")\n+          case tq"Iterable[$tpe]"              => (false, true,  "item")\n+          case tq"List[Int]"                   => (false, true,  "int")\n+          case tq"List[String]"                => (false, true,  "string")\n+          case tq"List[Boolean]"               => (false, true,  "boolean")\n+          case tq"List[java.time.Instant]"     => (false, true,  "instant")\n+          case tq"List[$tpe]"                  => (false, true,  "item")\n+          case tq"Int"                         => (false, false, "int")\n+          case tq"String"                      => (false, false, "string")\n+          case tq"Boolean"                     => (false, false, "boolean")\n+          case tq"java.time.Instant"           => (false, false, "instant")\n+          case tq"$tpe"                        => (false, false, "item")\n         }\n \n       val returnTypeCard =\n         if (returnTypeIsOption)\n           q"""org.orbeon.saxon.functions.registry.BuiltInFunctionSet.OPT"""\n+        else if (returnTypeIsIterable)\n+          q"""org.orbeon.saxon.functions.registry.BuiltInFunctionSet.STAR"""\n         else\n           q"""org.orbeon.saxon.functions.registry.BuiltInFunctionSet.ONE"""\n \n@@ -116,18 +128,28 @@ object XPathFunctionAnnotationMacro {\n         regularArgs.zipWithIndex map {\n           case (arg: ValDef, argPosInList) =>\n \n-            val (isOption, typeString) =\n+            val (isOption, isIterable, typeString) =\n               arg match {\n-                case q"$mods val $name: Option[Int]               = $rhs" => (true,  "int")\n-                case q"$mods val $name: Option[String]            = $rhs" => (true,  "string")\n-                case q"$mods val $name: Option[Boolean]           = $rhs" => (true,  "boolean")\n-                case q"$mods val $name: Option[java.time.Instant] = $rhs" => (true,  "instant")\n-                case q"$mods val $name: Option[$tpe]              = $rhs" => (true,  "item")\n-                case q"$mods val $name: Int                       = $rhs" => (false, "int")\n-                case q"$mods val $name: String                    = $rhs" => (false, "string")\n-                case q"$mods val $name: Boolean                   = $rhs" => (false, "boolean")\n-                case q"$mods val $name: java.time.Instant         = $rhs" => (false, "instant")\n-                case q"$mods val $name: $tpe                      = $rhs" => (false, "item")\n+                case q"$mods val $name: Option[Int]                 = $rhs" => (true,  false, "int")\n+                case q"$mods val $name: Option[String]              = $rhs" => (true,  false, "string")\n+                case q"$mods val $name: Option[Boolean]             = $rhs" => (true,  false, "boolean")\n+                case q"$mods val $name: Option[java.time.Instant]   = $rhs" => (true,  false, "instant")\n+                case q"$mods val $name: Option[$tpe]                = $rhs" => (true,  false, "item")\n+                case q"$mods val $name: Iterable[Int]               = $rhs" => (false, true,  "int")\n+                case q"$mods val $name: Iterable[String]            = $rhs" => (false, true,  "string")\n+                case q"$mods val $name: Iterable[Boolean]           = $rhs" => (false, true,  "boolean")\n+                case q"$mods val $name: Iterable[java.time.Instant] = $rhs" => (false, true,  "instant")\n+                case q"$mods val $name: Iterable[$tpe]              = $rhs" => (false, true,  "item")\n+                case q"$mods val $name: List[Int]                   = $rhs" => (false, true,  "int")\n+                case q"$mods val $name: List[String]                = $rhs" => (false, true,  "string")\n+                case q"$mods val $name: List[Boolean]               = $rhs" => (false, true,  "boolean")\n+                case q"$mods val $name: List[java.time.Instant]     = $rhs" => (false, true,  "instant")\n+                case q"$mods val $name: List[$tpe]                  = $rhs" => (false, true,  "item")\n+                case q"$mods val $name: Int                         = $rhs" => (false, false, "int")\n+                case q"$mods val $name: String                      = $rhs" => (false, false, "string")\n+                case q"$mods val $name: Boolean                     = $rhs" => (false, false, "boolean")\n+                case q"$mods val $name: java.time.Instant           = $rhs" => (false, false, "instant")\n+                case q"$mods val $name: $tpe                        = $rhs" => (false, false, "item")\n               }\n \n             val (decodeCall, defaultOpt) = {\n@@ -135,15 +157,15 @@ object XPathFunctionAnnotationMacro {\n               (q"""decodeSaxonArg[$tpt](args($argPosInList))""", rhs != EmptyTree option rhs)\n             }\n \n-            (argPosInList, isOption, typeString, decodeCall, defaultOpt)\n+            (argPosInList, isOption, isIterable, typeString, decodeCall, defaultOpt)\n \n           case _ => throw new IllegalArgumentException\n       }\n \n-      val minArity = argumentsDetails.count(_._5.isEmpty)\n+      val minArity = argumentsDetails.count(_._6.isEmpty)\n       val maxArity = argumentsDetails.size\n \n-      if (argumentsDetails.takeWhile(_._5.isEmpty).size != minArity)\n+      if (argumentsDetails.takeWhile(_._6.isEmpty).size != minArity)\n         throw new IllegalArgumentException(s"arguments with default values must be last")\n \n       def getSaxonType(typeString: String) =\n@@ -174,11 +196,13 @@ object XPathFunctionAnnotationMacro {\n            """\n \n         val registerWithArgs =\n-          flattenedArgumentsUpToArity.foldLeft(register) { case (result, (pos, isOption, typeString, _, _)) =>\n+          flattenedArgumentsUpToArity.foldLeft(register) { case (result, (pos, isOption, isIterable, typeString, _, _)) =>\n \n             val card =\n               if (isOption)\n                 q"""org.orbeon.saxon.functions.registry.BuiltInFunctionSet.OPT"""\n+              else if (isIterable)\n+                q"""org.orbeon.saxon.functions.registry.BuiltInFunctionSet.STAR"""\n               else\n                 q"""org.orbeon.saxon.functions.registry.BuiltInFunctionSet.ONE"""\n \n@@ -192,8 +216,8 @@ object XPathFunctionAnnotationMacro {\n              """\n           }\n \n-        val flattenedTrees   = flattenedArgumentsUpToArity.map(_._4)\n-        val defaultArgsTrees = argumentsDetails.drop(arity).flatMap(_._5)\n+        val flattenedTrees   = flattenedArgumentsUpToArity.map(_._5)\n+        val defaultArgsTrees = argumentsDetails.drop(arity).flatMap(_._6)\n \n         val encodedCall =\n           if (regularArgs.isEmpty) { // no parameter list!\n', 'commit_subject': 'Macro: register proper types for `List` and `Iterable`', 'repo_name': 'orbeon/orbeon-forms', 'committer': {'name': 'Erik Bruchez', 'email': 'c195b508e6f0c57aeafdb5e96947e2b79e6a4b40@bruchez.org', 'time_sec': '1632872448', 'tz_offset': '-420', 'date': {'seconds': '1632872448'}}, 'commit_tree': 'ed5a21085493effdd27c358032e36b0470652e0b', 'commit_message': 'Macro: register proper types for `List` and `Iterable`', 'language_name': 'HTML', 'license': 'lgpl-2.1', 'hunks': '@@ -92,23 +92,35 @@ object XPathFunctionAnnotationMacro {\n           case _                                     => c.abort(c.enclosingPosition, "Annotation `@XPathFunction` has incorrect parameters")\n         }\n \n-      val (returnTypeIsOption, returnTypeString) =\n+      val (returnTypeIsOption, returnTypeIsIterable, returnTypeString) =\n         returnType match {\n-          case tq"Option[Int]"               => (true,  "int")\n-          case tq"Option[String]"            => (true,  "string")\n-          case tq"Option[Boolean]"           => (true,  "boolean")\n-          case tq"Option[java.time.Instant]" => (true,  "instant")\n-          case tq"Option[$tpe]"              => (true,  "item")\n-          case tq"Int"                       => (false, "int")\n-          case tq"String"                    => (false, "string")\n-          case tq"Boolean"                   => (false, "boolean")\n-          case tq"java.time.Instant"         => (false, "instant")\n-          case tq"$tpe"                      => (false, "item")\n+          case tq"Option[Int]"                 => (true,  false, "int")\n+          case tq"Option[String]"              => (true,  false, "string")\n+          case tq"Option[Boolean]"             => (true,  false, "boolean")\n+          case tq"Option[java.time.Instant]"   => (true,  false, "instant")\n+          case tq"Option[$tpe]"                => (true,  false, "item")\n+          case tq"Iterable[Int]"               => (false, true,  "int")\n+          case tq"Iterable[String]"            => (false, true,  "string")\n+          case tq"Iterable[Boolean]"           => (false, true,  "boolean")\n+          case tq"Iterable[java.time.Instant]" => (false, true,  "instant")\n+          case tq"Iterable[$tpe]"              => (false, true,  "item")\n+          case tq"List[Int]"                   => (false, true,  "int")\n+          case tq"List[String]"                => (false, true,  "string")\n+          case tq"List[Boolean]"               => (false, true,  "boolean")\n+          case tq"List[java.time.Instant]"     => (false, true,  "instant")\n+          case tq"List[$tpe]"                  => (false, true,  "item")\n+          case tq"Int"                         => (false, false, "int")\n+          case tq"String"                      => (false, false, "string")\n+          case tq"Boolean"                     => (false, false, "boolean")\n+          case tq"java.time.Instant"           => (false, false, "instant")\n+          case tq"$tpe"                        => (false, false, "item")\n         }\n \n       val returnTypeCard =\n         if (returnTypeIsOption)\n           q"""org.orbeon.saxon.functions.registry.BuiltInFunctionSet.OPT"""\n+        else if (returnTypeIsIterable)\n+          q"""org.orbeon.saxon.functions.registry.BuiltInFunctionSet.STAR"""\n         else\n           q"""org.orbeon.saxon.functions.registry.BuiltInFunctionSet.ONE"""\n \n@@ -116,18 +128,28 @@ object XPathFunctionAnnotationMacro {\n         regularArgs.zipWithIndex map {\n           case (arg: ValDef, argPosInList) =>\n \n-            val (isOption, typeString) =\n+            val (isOption, isIterable, typeString) =\n               arg match {\n-                case q"$mods val $name: Option[Int]               = $rhs" => (true,  "int")\n-                case q"$mods val $name: Option[String]            = $rhs" => (true,  "string")\n-                case q"$mods val $name: Option[Boolean]           = $rhs" => (true,  "boolean")\n-                case q"$mods val $name: Option[java.time.Instant] = $rhs" => (true,  "instant")\n-                case q"$mods val $name: Option[$tpe]              = $rhs" => (true,  "item")\n-                case q"$mods val $name: Int                       = $rhs" => (false, "int")\n-                case q"$mods val $name: String                    = $rhs" => (false, "string")\n-                case q"$mods val $name: Boolean                   = $rhs" => (false, "boolean")\n-                case q"$mods val $name: java.time.Instant         = $rhs" => (false, "instant")\n-                case q"$mods val $name: $tpe                      = $rhs" => (false, "item")\n+                case q"$mods val $name: Option[Int]                 = $rhs" => (true,  false, "int")\n+                case q"$mods val $name: Option[String]              = $rhs" => (true,  false, "string")\n+                case q"$mods val $name: Option[Boolean]             = $rhs" => (true,  false, "boolean")\n+                case q"$mods val $name: Option[java.time.Instant]   = $rhs" => (true,  false, "instant")\n+                case q"$mods val $name: Option[$tpe]                = $rhs" => (true,  false, "item")\n+                case q"$mods val $name: Iterable[Int]               = $rhs" => (false, true,  "int")\n+                case q"$mods val $name: Iterable[String]            = $rhs" => (false, true,  "string")\n+                case q"$mods val $name: Iterable[Boolean]           = $rhs" => (false, true,  "boolean")\n+                case q"$mods val $name: Iterable[java.time.Instant] = $rhs" => (false, true,  "instant")\n+                case q"$mods val $name: Iterable[$tpe]              = $rhs" => (false, true,  "item")\n+                case q"$mods val $name: List[Int]                   = $rhs" => (false, true,  "int")\n+                case q"$mods val $name: List[String]                = $rhs" => (false, true,  "string")\n+                case q"$mods val $name: List[Boolean]               = $rhs" => (false, true,  "boolean")\n+                case q"$mods val $name: List[java.time.Instant]     = $rhs" => (false, true,  "instant")\n+                case q"$mods val $name: List[$tpe]                  = $rhs" => (false, true,  "item")\n+                case q"$mods val $name: Int                         = $rhs" => (false, false, "int")\n+                case q"$mods val $name: String                      = $rhs" => (false, false, "string")\n+                case q"$mods val $name: Boolean                     = $rhs" => (false, false, "boolean")\n+                case q"$mods val $name: java.time.Instant           = $rhs" => (false, false, "instant")\n+                case q"$mods val $name: $tpe                        = $rhs" => (false, false, "item")\n               }\n \n             val (decodeCall, defaultOpt) = {\n@@ -135,15 +157,15 @@ object XPathFunctionAnnotationMacro {\n               (q"""decodeSaxonArg[$tpt](args($argPosInList))""", rhs != EmptyTree option rhs)\n             }\n \n-            (argPosInList, isOption, typeString, decodeCall, defaultOpt)\n+            (argPosInList, isOption, isIterable, typeString, decodeCall, defaultOpt)\n \n           case _ => throw new IllegalArgumentException\n       }\n \n-      val minArity = argumentsDetails.count(_._5.isEmpty)\n+      val minArity = argumentsDetails.count(_._6.isEmpty)\n       val maxArity = argumentsDetails.size\n \n-      if (argumentsDetails.takeWhile(_._5.isEmpty).size != minArity)\n+      if (argumentsDetails.takeWhile(_._6.isEmpty).size != minArity)\n         throw new IllegalArgumentException(s"arguments with default values must be last")\n \n       def getSaxonType(typeString: String) =\n@@ -174,11 +196,13 @@ object XPathFunctionAnnotationMacro {\n            """\n \n         val registerWithArgs =\n-          flattenedArgumentsUpToArity.foldLeft(register) { case (result, (pos, isOption, typeString, _, _)) =>\n+          flattenedArgumentsUpToArity.foldLeft(register) { case (result, (pos, isOption, isIterable, typeString, _, _)) =>\n \n             val card =\n               if (isOption)\n                 q"""org.orbeon.saxon.functions.registry.BuiltInFunctionSet.OPT"""\n+              else if (isIterable)\n+                q"""org.orbeon.saxon.functions.registry.BuiltInFunctionSet.STAR"""\n               else\n                 q"""org.orbeon.saxon.functions.registry.BuiltInFunctionSet.ONE"""\n \n@@ -192,8 +216,8 @@ object XPathFunctionAnnotationMacro {\n              """\n           }\n \n-        val flattenedTrees   = flattenedArgumentsUpToArity.map(_._4)\n-        val defaultArgsTrees = argumentsDetails.drop(arity).flatMap(_._5)\n+        val flattenedTrees   = flattenedArgumentsUpToArity.map(_._5)\n+        val defaultArgsTrees = argumentsDetails.drop(arity).flatMap(_._6)\n \n         val encodedCall =\n           if (regularArgs.isEmpty) { // no parameter list!\n', 'addition_count': 52, 'deletion_count': 28, 'src_file': 'a/core-cross-platform/shared/src/main/scala/org/orbeon/macros/XPathFunctionAnnotationMacro.scala', 'tgt_file': 'b/core-cross-platform/shared/src/main/scala/org/orbeon/macros/XPathFunctionAnnotationMacro.scala', 'file_extension': '.scala', 'before_file': 'package org.orbeon.macros\n\nimport cats.syntax.option._\nimport org.orbeon.oxf.util.CoreUtils.BooleanOps\n\nimport scala.annotation.{StaticAnnotation, compileTimeOnly, tailrec}\nimport scala.language.experimental.macros\nimport scala.reflect.macros.whitebox\n\n\n// The purpose of this macro is to create bridges to the Saxon XPath library simply using\n// the `@XPathFunction` annotation placed on a method. The macro analyzes the method\'s\n// signature, and:\n//\n// - builds a call to a `register()` function that must be in scope, typically because the\n//   methods are in a trait or class extending `OrbeonFunctionLibrary` or `BuiltInFunctionSet`.\n//     - create one `register()` call for each arity of the function, where there can be\n//       multiple arities depending on whether the method has one more more parameters with\n//       default values\n// - creates a new local class that extends `FunctionSupport2` and implements the `call()`\n//   method\n//     - parameters and the return value are converted using support in `FunctionSupport2`\n// - locally scopes implicits for `XPathContext` and `XFormsFunction.Context`\n//     - these are inserted only if there is at least one implicit parameter list\n//\n// The original methods are not removed from the original scope. (TODO)\n//\n// The XPath function is named by kebab-casing the Scala method name. You can override this\n// by passing a `name` parameter to the annotation.\n//\n// Supported types so far:\n//\n// - `Int`\n// - `Long`\n// - `Double`\n// - `String`\n// - `Boolean`\n// - `Item`\n// - `NodeInfo`\n// - `AtomicValue`\n// - `Instant`\n// - `Option`\n// - `Iterable`\n//\n// This allows writing new XPath functions in a very lightweight and safe way.\n//\n// The only drawback we see if that macro annotations are not supported by Scala 3. But moving\n// to Scala 3 is still quite a way off, and we hope that a solution will be available at some\n// point. In the worst case scenario, XPath functions interfaces will have to be remain written\n// with Scala 2 for longer.\n//\n// TODO:\n//\n// - [ ] support `Map` and `Array` types\n// - [ ] rename `FunctionSupport2`\n// - [ ] rename `XFormsFunction.Context`\n// - [ ] avoid copying the method!\n// - [ ] function flags (focus, etc.)\n// - [ ] allow specifying super trait like `InstanceTrait` for `PathMapXPathAnalysisBuilder`\n//\n@compileTimeOnly("enable macro paradise to expand macro annotations")\nclass XPathFunction(name: String = null) extends StaticAnnotation {\n  def macroTransform(annottees: Any*): Any = macro XPathFunctionAnnotationMacro.impl\n}\n\nobject XPathFunctionAnnotationMacro {\n\n  def impl(c: whitebox.Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {\n\n    import c.universe._\n\n    val results = {\n\n      val (methodName, tpes, regularArgs, implicitArgs, returnType, body) =\n        annottees.map(_.tree).toList match {\n          case q"$mods def $methodName[..$tpes](..$args)(implicit ..$implicitArgs): $returnType = { ..$body }" :: Nil if implicitArgs.nonEmpty =>\n            // Not sure why I am getting a match sometimes, but not always, with `implicitArgs.isEmpty`.\n            // So adding above the check for `implicitArgs.nonEmpty`.\n            (methodName, tpes, args, implicitArgs.some, returnType, body)\n          case q"$mods def $methodName[..$tpes](...$args): $returnType = { ..$body }" :: Nil =>\n            (methodName, tpes, args.flatten, None, returnType, body)\n          case _ => c.abort(c.enclosingPosition, "Annotation `@XPathFunction` can be used only on methods")\n        }\n\n      val kebabMethodNameString = camelToKebab(methodName.toString)\n\n      // See https://stackoverflow.com/questions/32631372/getting-parameters-from-scala-macro-annotation\n      val resolvedFunctionName =\n        c.prefix.tree match {\n          case q"new XPathFunction(name = $nameArg)" => nameArg\n          case q"new XPathFunction($nameArg)"        => nameArg\n          case _                                     => c.abort(c.enclosingPosition, "Annotation `@XPathFunction` has incorrect parameters")\n        }\n\n      val (returnTypeIsOption, returnTypeString) =\n        returnType match {\n          case tq"Option[Int]"               => (true,  "int")\n          case tq"Option[String]"            => (true,  "string")\n          case tq"Option[Boolean]"           => (true,  "boolean")\n          case tq"Option[java.time.Instant]" => (true,  "instant")\n          case tq"Option[$tpe]"              => (true,  "item")\n          case tq"Int"                       => (false, "int")\n          case tq"String"                    => (false, "string")\n          case tq"Boolean"                   => (false, "boolean")\n          case tq"java.time.Instant"         => (false, "instant")\n          case tq"$tpe"                      => (false, "item")\n        }\n\n      val returnTypeCard =\n        if (returnTypeIsOption)\n          q"""org.orbeon.saxon.functions.registry.BuiltInFunctionSet.OPT"""\n        else\n          q"""org.orbeon.saxon.functions.registry.BuiltInFunctionSet.ONE"""\n\n\n        regularArgs.zipWithIndex map {\n          case (arg: ValDef, argPosInList) =>\n\n            val (isOption, typeString) =\n              arg match {\n                case q"$mods val $name: Option[Int]               = $rhs" => (true,  "int")\n                case q"$mods val $name: Option[String]            = $rhs" => (true,  "string")\n                case q"$mods val $name: Option[Boolean]           = $rhs" => (true,  "boolean")\n                case q"$mods val $name: Option[java.time.Instant] = $rhs" => (true,  "instant")\n                case q"$mods val $name: Option[$tpe]              = $rhs" => (true,  "item")\n                case q"$mods val $name: Int                       = $rhs" => (false, "int")\n                case q"$mods val $name: String                    = $rhs" => (false, "string")\n                case q"$mods val $name: Boolean                   = $rhs" => (false, "boolean")\n                case q"$mods val $name: java.time.Instant         = $rhs" => (false, "instant")\n                case q"$mods val $name: $tpe                      = $rhs" => (false, "item")\n              }\n\n            val (decodeCall, defaultOpt) = {\n            val (decodeCall, defaultOpt) = {\n              (q"""decodeSaxonArg[$tpt](args($argPosInList))""", rhs != EmptyTree option rhs)\n            }\n\n            (argPosInList, isOption, typeString, decodeCall, defaultOpt)\n\n          case _ => throw new IllegalArgumentException\n      }\n\n      val minArity = argumentsDetails.count(_._5.isEmpty)\n      val maxArity = argumentsDetails.size\n\n      if (argumentsDetails.takeWhile(_._5.isEmpty).size != minArity)\n        throw new IllegalArgumentException(s"arguments with default values must be last")\n\n      def getSaxonType(typeString: String) =\n      def getSaxonType(typeString: String) =\n        typeString match {\n          case "int"     => q"""org.orbeon.saxon.model.BuiltInAtomicType.INTEGER"""\n          case "string"  => q"""org.orbeon.saxon.model.BuiltInAtomicType.STRING"""\n          case "boolean" => q"""org.orbeon.saxon.model.BuiltInAtomicType.BOOLEAN"""\n          case "instant" => q"""org.orbeon.saxon.model.BuiltInAtomicType.DATE_TIME"""\n          case "item"    => q"""org.orbeon.saxon.model.AnyItemType"""\n        }\n\n      // Register one entry per distinct arity\n      for (arity <- minArity to maxArity) yield {\n\n        val classNameWithArity          = methodName.toString + "_xpathFunction" + arity\n        val flattenedArgumentsUpToArity = argumentsDetails.take(arity)\n\n        val register =\n          q"""\n            register(\n              $resolvedFunctionName,\n              $arity,\n              () => new ${TypeName(classNameWithArity)},\n              ${getSaxonType(returnTypeString)},\n              $returnTypeCard,\n              org.orbeon.saxon.functions.registry.BuiltInFunctionSet.CITEM | org.orbeon.saxon.functions.registry.BuiltInFunctionSet.LATE\n           """\n\n        val registerWithArgs =\n          flattenedArgumentsUpToArity.foldLeft(register) { case (result, (pos, isOption, typeString, _, _)) =>\n\n            val card =\n              if (isOption)\n                q"""org.orbeon.saxon.functions.registry.BuiltInFunctionSet.OPT"""\n              else\n                q"""org.orbeon.saxon.functions.registry.BuiltInFunctionSet.ONE"""\n\n                q"""org.orbeon.saxon.functions.registry.BuiltInFunctionSet.ONE"""\n\n            q"""\n               $result.arg(\n                 $pos,\n                 ${getSaxonType(typeString)},\n                 $card,\n             """\n          }\n\n        val flattenedTrees   = flattenedArgumentsUpToArity.map(_._4)\n        val defaultArgsTrees = argumentsDetails.drop(arity).flatMap(_._5)\n\n        val encodedCall =\n          if (regularArgs.isEmpty) { // no parameter list!\n        val encodedCall =\n          if (regularArgs.isEmpty) { // no parameter list!\n            q"""\n              encodeSaxonArg[$returnType](\n                fn[..$tpes]\n              )\n             """\n          } else {\n            q"""\n              encodeSaxonArg[$returnType](\n                fn[..$tpes](\n                  ..$flattenedTrees,..$defaultArgsTrees\n                )\n              )\n            """\n          }\n\n        // We use `...$args` which means `Iterable[Iterable[Tree]]`\n        implicitArgs match {\n          case None =>\n            q"""\n              $registerWithArgs\n\n              class ${TypeName(classNameWithArity)} extends org.orbeon.oxf.xml.FunctionSupport2 {\n                def call(\n                  context: org.orbeon.saxon.expr.XPathContext,\n                  args   : Array[org.orbeon.saxon.om.Sequence]\n                ): org.orbeon.saxon.om.Sequence = {\n\n                  def fn[..$tpes](..$regularArgs): $returnType = {..$body}\n\n                  import org.orbeon.oxf.xml.FunctionSupport2._\n\n                  $encodedCall\n                }\n              }\n            """\n          case Some(implicitArgs) =>\n            q"""\n              $registerWithArgs\n\n              class ${TypeName(classNameWithArity)} extends org.orbeon.oxf.xml.FunctionSupport2 {\n                def call(\n                  context: org.orbeon.saxon.expr.XPathContext,\n                  args   : Array[org.orbeon.saxon.om.Sequence]\n                ): org.orbeon.saxon.om.Sequence = {\n\n                  def fn[..$tpes](..$regularArgs)(..$implicitArgs): $returnType = {..$body}\n\n                  locally {\n                    implicit val xpathContext : org.orbeon.saxon.expr.XPathContext = context\n                    implicit val xformsContext: org.orbeon.oxf.xforms.function.XFormsFunction.Context =\n                      org.orbeon.oxf.xforms.function.XFormsFunction.context\n\n                    import org.orbeon.oxf.xml.FunctionSupport2._\n\n                    $encodedCall\n                  }\n                }\n              }\n            """\n        }\n      }\n    }\n\n    // TODO: Would like to keep `..$annottees` but that gets scope into a block?\n    val result = q"""..$results"""\n\n//    println("`XPathFunctionAnnotationMacro` result:")\n//    println(result)\n\n    c.Expr[Any](result)\n  }\n\n  // Inspired from function found online\n  private def camelToKebab(name: String): String = {\n    @tailrec\n    def recurse(accDone: List[Char], acc: List[Char]): List[Char] = acc match {\n      case Nil => accDone\n      case a :: b :: c :: tail if a.isUpper && b.isUpper && c.isLower => recurse(accDone ++ List(a, \'-\', b, c), tail)\n      case      a :: b :: tail if a.isLower && b.isUpper              => recurse(accDone ++ List(a, \'-\', b),    tail)\n      case           a :: tail                                        => recurse(accDone :+ a,                  tail)\n    }\n    recurse(Nil, name.toList).mkString.toLowerCase\n  }\n}\n', 'message': 'Macro: register proper types for `List` and `Iterable`', 'author_name': 'Erik Bruchez', 'lang': 'scala'}