Repository: 7mind/izumi
Pull request #1828 (2022-11-21 23:54:02)

Scala 3: implement HasConstructor


Patch:
diff --git a/distage/distage-core-api/src/main/scala-2/izumi/distage/constructors/macros/HasConstructorMacro.scala b/distage/distage-core-api/src/main/scala-2/izumi/distage/constructors/macros/HasConstructorMacro.scala
index cf923c131..7f5abc31a 100644
--- a/distage/distage-core-api/src/main/scala-2/izumi/distage/constructors/macros/HasConstructorMacro.scala
+++ b/distage/distage-core-api/src/main/scala-2/izumi/distage/constructors/macros/HasConstructorMacro.scala
@@ -19,15 +19,15 @@ object HasConstructorMacro {
 
     val targetType = ReflectionUtil.norm(c.universe: c.universe.type)(weakTypeOf[T].dealias)
     requireConcreteTypeConstructor(c)("HasConstructor", targetType)
-    val deepIntersection = ReflectionUtil
-      .deepIntersectionTypeMembers[c.universe.type](targetType)
-      .filter(_ ne definitions.AnyTpe)
 
     targetType match {
       case definitions.AnyTpe =>
         c.Expr[HasConstructor[T]](q"_root_.izumi.distage.constructors.HasConstructor.empty")
 
       case _ =>
+        val deepIntersection = ReflectionUtil
+          .deepIntersectionTypeMembers[c.universe.type](targetType)
+          .filter(_ ne definitions.AnyTpe)
         ziohasConstructorAssertion(targetType, deepIntersection)
 
         val reflectionProvider = ReflectionProviderDefaultImpl(macroUniverse)
diff --git a/distage/distage-core-api/src/main/scala-3/izumi/distage/constructors/AnyConstructorMacro.scala b/distage/distage-core-api/src/main/scala-3/izumi/distage/constructors/AnyConstructorMacro.scala
index 8c02c2528..fec955c92 100644
--- a/distage/distage-core-api/src/main/scala-3/izumi/distage/constructors/AnyConstructorMacro.scala
+++ b/distage/distage-core-api/src/main/scala-3/izumi/distage/constructors/AnyConstructorMacro.scala
@@ -28,7 +28,7 @@ object AnyConstructorMacro {
       // ignore intersections for now
       '{ (throw new RuntimeException("unsupported intersection")): AnyConstructor[T] }
     } else if ((tpe0.classSymbol.isDefined && !typeSymbol.flags.is(Flags.Trait) && !typeSymbol.flags.is(Flags.Abstract)) || {
-        util.dropTypeRef(tpe0) match { case _: ConstantType | _: TermRef => true; case _ => false }
+        util.dereferenceTypeRef(tpe0) match { case _: ConstantType | _: TermRef => true; case _ => false }
       }) {
       ClassConstructorMacro.make[T]
     } else if ({
@@ -44,7 +44,7 @@ object AnyConstructorMacro {
            |It's neither a concrete class, nor a wireable trait or abstract class!""".stripMargin
       )
     }
-  } catch { case t: Throwable => qctx.reflect.report.errorAndAbort(t.stackTrace) }
+  } catch { case t: scala.quoted.runtime.StopMacroExpansion => throw t; case t: Throwable => qctx.reflect.report.errorAndAbort(t.stackTrace) }
 
   @experimental
   def makeMethod[T: Type, BT: Type](using qctx: Quotes): Expr[BT] = try {
@@ -65,7 +65,7 @@ object AnyConstructorMacro {
       case None =>
         makeMethodImpl[T, BT](outerClass)
     }
-  } catch { case t: Throwable => qctx.reflect.report.errorAndAbort(t.stackTrace) }
+  } catch { case t: scala.quoted.runtime.StopMacroExpansion => throw t; case t: Throwable => qctx.reflect.report.errorAndAbort(t.stackTrace) }
 
   @experimental
   private def applyMake[T: Type, BT: Type](using qctx: Quotes)(outerClass: qctx.reflect.Symbol)(functoid: Expr[Functoid[T]]): Expr[BT] = {
diff --git a/distage/distage-core-api/src/main/scala-3/izumi/distage/constructors/ClassConstructorMacro.scala b/distage/distage-core-api/src/main/scala-3/izumi/distage/constructors/ClassConstructorMacro.scala
index b33d8493c..c030ae865 100644
--- a/distage/distage-core-api/src/main/scala-3/izumi/distage/constructors/ClassConstructorMacro.scala
+++ b/distage/distage-core-api/src/main/scala-3/izumi/distage/constructors/ClassConstructorMacro.scala
@@ -17,7 +17,7 @@ object ClassConstructorMacro {
 
     val typeRepr = TypeRepr.of[R].dealias.simplified
 
-    util.dropTypeRef(typeRepr) match {
+    util.dereferenceTypeRef(typeRepr) match {
       case c: ConstantType =>
         singletonClassConstructor[R](Literal(c.constant))
 
@@ -46,7 +46,7 @@ object ClassConstructorMacro {
             report.errorAndAbort(s"No class symbol defined for $typeRepr")
         }
     }
-  } catch { case t: Throwable => qctx.reflect.report.errorAndAbort(t.stackTrace) }
+  } catch { case t: scala.quoted.runtime.StopMacroExpansion => throw t; case t: Throwable => qctx.reflect.report.errorAndAbort(t.stackTrace) }
 
   private def singletonClassConstructor[R0](using qctx: Quotes, rtpe0: Type[R0])(tree: qctx.reflect.Tree): Expr[ClassConstructor[R0]] = {
     type R <: R0 & Singleton
diff --git a/distage/distage-core-api/src/main/scala-3/izumi/distage/constructors/ConstructorUtil.scala b/distage/distage-core-api/src/main/scala-3/izumi/distage/constructors/ConstructorUtil.scala
index 45bc4e693..07802417f 100644
--- a/distage/distage-core-api/src/main/scala-3/izumi/distage/constructors/ConstructorUtil.scala
+++ b/distage/distage-core-api/src/main/scala-3/izumi/distage/constructors/ConstructorUtil.scala
@@ -50,10 +50,9 @@ class ConstructorUtil[Q <: Quotes](using val qctx: Q) {
 
   final case class ParamRepr(name: String, mbSymbol: Option[Symbol], tpe: TypeRepr)
 
-  type ParamReprList = List[ParamRepr]
-  type ParamReprLists = List[ParamReprList]
+  type ParamReprLists = List[List[ParamRepr]]
 
-  def assertSignatureIsAcceptableForFactory(signatureParams: ParamReprList, resultTpe: TypeRepr, clue: String): Unit = {
+  def assertSignatureIsAcceptableForFactory(signatureParams: List[ParamRepr], resultTpe: TypeRepr, clue: String): Unit = {
     assert(signatureParams.groupMap(_.name)(_.tpe).forall(_._2.size == 1), "BUG: duplicated arg names!")
 
     val sigRevIndex = signatureParams.groupMap(_.tpe)(_.name)
@@ -85,7 +84,7 @@ class ConstructorUtil[Q <: Quotes](using val qctx: Q) {
     }
   }
 
-  def makeFunctoid[R: Type](params: ParamReprList, argsLambda: Expr[Seq[Any] => R], providerType: Expr[ProviderType]): Expr[Functoid[R]] = {
+  def makeFunctoid[R: Type](params: List[ParamRepr], argsLambda: Expr[Seq[Any] => R], providerType: Expr[ProviderType]): Expr[Functoid[R]] = {
     val paramsMacro = new FunctoidParametersMacro[qctx.type]
 
     val paramDefs = params.map {
@@ -120,7 +119,7 @@ class ConstructorUtil[Q <: Quotes](using val qctx: Q) {
   }
 
   def wrapIntoFunctoidRawLambda[R: Type](
-    params: ParamReprList
+    params: List[ParamRepr]
   )(body: (Symbol, List[Term]) => Tree
   ): Expr[Seq[Any] => R] = {
     val mtpe = MethodType(List("args"))(
@@ -135,9 +134,9 @@ class ConstructorUtil[Q <: Quotes](using val qctx: Q) {
           val argRefs = params.iterator.zipWithIndex.map {
             case (ParamRepr(_, _, paramTpe), idx) =>
               paramTpe match {
-                case ByNameUnwrappedAsType('[t]) =>
+                case ByNameUnwrappedTypeReprAsType('[t]) =>
                   '{ ${ args.asExprOf[Seq[Any]] }.apply(${ Expr(idx) }).asInstanceOf[() => t].apply() }.asTerm
-                case AsType('[t]) =>
+                case TypeReprAsType('[t]) =>
                   '{ ${ args.asExprOf[Seq[Any]] }.apply(${ Expr(idx) }).asInstanceOf[t] }.asTerm
                 case _ =>
                   report.errorAndAbort(s"Invalid higher-kinded type $paramTpe ${paramTpe.show}")
@@ -164,7 +163,7 @@ class ConstructorUtil[Q <: Quotes](using val qctx: Q) {
     }
   }
 
-  private object ByNameUnwrappedAsType {
+  private object ByNameUnwrappedTypeReprAsType {
     def unapply(t: TypeRepr): Option[Type[?]] = {
       t match {
         case ByNameType(u) => Some(u.asType)
@@ -173,7 +172,7 @@ class ConstructorUtil[Q <: Quotes](using val qctx: Q) {
     }
   }
 
-  private object AsType {
+  object TypeReprAsType {
     def unapply(t: TypeRepr): Some[Type[?]] = {
       Some(t.asType)
     }
@@ -243,7 +242,7 @@ class ConstructorUtil[Q <: Quotes](using val qctx: Q) {
     }
   }
 
-  def dropTypeRef(tpe: TypeRepr): TypeRepr = {
+  def dereferenceTypeRef(tpe: TypeRepr): TypeRepr = {
     tpe match {
       case t: TypeRef =>
         t.typeSymbol.owner.typeRef.memberType(t.typeSymbol)
diff --git a/distage/distage-core-api/src/main/scala-3/izumi/distage/constructors/FactoryConstructorMacro.scala b/distage/distage-core-api/src/main/scala-3/izumi/distage/constructors/FactoryConstructorMacro.scala
index 59b9be9de..ee562022e 100644
--- a/distage/distage-core-api/src/main/scala-3/izumi/distage/constructors/FactoryConstructorMacro.scala
+++ b/distage/distage-core-api/src/main/scala-3/izumi/distage/constructors/FactoryConstructorMacro.scala
@@ -17,7 +17,7 @@ object FactoryConstructorMacro {
     import qctx.reflect.*
 
     val util = new ConstructorUtil[qctx.type]()
-    import util.{ParamRepr, ParamReprList, ParamReprLists}
+    import util.{ParamRepr, ParamReprLists}
 
     val context = new ConstructorContext[R, qctx.type, util.type](util)
     import context.*
@@ -112,7 +112,7 @@ object FactoryConstructorMacro {
 
     val ctorArgs = flatCtorParams.map { case ParamRepr(n, s, t) => ParamRepr(n, s, util.returnTypeOfMethodOrByName(t)) }
     val byNameMethodArgs = factoryMethodData.flatMap(_.params).flatten.collect { case p: DependencyParameter => ParamRepr(p.argName, p.mbParamSymbol, p.paramTpe) }
-    val lamParams: ParamReprList = ctorArgs ++ byNameMethodArgs
+    val lamParams: List[ParamRepr] = ctorArgs ++ byNameMethodArgs
     val indexShift = ctorArgs.length
 
     val lamExpr = util.wrapIntoFunctoidRawLambda[R](lamParams) {
diff --git a/distage/distage-core-api/src/main/scala-3/izumi/distage/constructors/HasConstructorMacro.scala b/distage/distage-core-api/src/main/scala-3/izumi/distage/constructors/HasConstructorMacro.scala
new file mode 100644
index 000000000..6fb89e928
--- /dev/null
+++ b/distage/distage-core-api/src/main/scala-3/izumi/distage/constructors/HasConstructorMacro.scala
@@ -0,0 +1,75 @@
+package izumi.distage.constructors
+
+import izumi.distage.model.providers.{Functoid, FunctoidMacro}
+import izumi.distage.model.reflection.Provider.ProviderType
+import izumi.fundamentals.platform.exceptions.IzThrowable.toRichThrowable
+import izumi.fundamentals.platform.reflection.ReflectionUtil
+import izumi.reflect.Tag
+
+import scala.collection.immutable.ArraySeq
+import scala.quoted.{Expr, Quotes, Type}
+import scala.compiletime.summonInline
+
+object HasConstructorMacro {
+
+  def make[R: Type](using qctx: Quotes): Expr[HasConstructor[R]] = try {
+    import qctx.reflect.*
+
+    val util = new ConstructorUtil[qctx.type]()
+    import util.{ParamRepr, TypeReprAsType}
+
+    val typeRepr = TypeRepr.of[R].dealias.simplified
+
+    if (typeRepr.typeSymbol == defn.AnyClass) {
+      '{ HasConstructor.empty }.asExprOf[HasConstructor[R]]
+    } else {
+      val deepIntersection = ReflectionUtil
+        .intersectionMembers(typeRepr)
+        .map(_.dealias.simplified)
+        .filter(_.typeSymbol != defn.AnyClass)
+      zioHasConstructorAssertion(typeRepr, deepIntersection)
+
+      val lamParams = deepIntersection.zipWithIndex.map {
+        case (AppliedType(_, tpe :: _), idx) =>
+          val name = s"${tpe.typeSymbol.fullName}_$idx"
+          ParamRepr(name, None, tpe)
+      }
+
+      val lamExpr = util.wrapIntoFunctoidRawLambda[R](lamParams) {
+        case (_, allParams) =>
+          allParams.zip(lamParams) match {
+            case (headParam, ParamRepr(_, _, TypeReprAsType('[t]))) :: params =>
+              params
+                .foldLeft('{ zio.Has.apply[t](${ headParam.asExprOf[t] })(summonInline[Tag[t]]) }) {
+                  case (expr, (arg, ParamRepr(_, _, tpe))) =>
+                    tpe.asType match {
+                      case '[g] =>
+                        '{ $expr.add(${ arg.asExprOf[g] })(summonInline[Tag[g]]) }
+                    }
+                }.asTerm
+            case _ =>
+              report.errorAndAbort(s"Impossible happened, empty Has intersection or malformed type ${typeRepr.show} in HasConstructorMacro")
+          }
+      }
+
+      val f = util.makeFunctoid[R](lamParams, lamExpr, '{ ProviderType.ZIOHas })
+      '{ new HasConstructor[R](${ f }) }
+    }
+  } catch { case t: scala.quoted.runtime.StopMacroExpansion => throw t; case t: Throwable => qctx.reflect.report.errorAndAbort(t.stackTrace) }
+
+  def zioHasConstructorAssertion(using qctx: Quotes)(typeRepr: qctx.reflect.TypeRepr, deepIntersection: List[qctx.reflect.TypeRepr]): Unit = {
+    val (good, bad) = deepIntersection.partition(tpe => tpe.typeSymbol.fullName == "zio.Has")
+    if (bad.nonEmpty) {
+      qctx.reflect.report.errorAndAbort(
+        s"Cannot construct an implementation for ZIO Has type `${typeRepr.show}`: intersection contains type constructors that aren't `zio.Has` or `Any`: ${bad
+            .map(_.show)} (${bad.map(_.typeSymbol)})"
+      )
+    }
+    if (good.isEmpty) {
+      qctx.reflect.report.errorAndAbort(
+        s"Cannot construct an implementation for ZIO Has type `${typeRepr.show}`: the intersection type is empty, it contains no `zio.Has` or `Any` type constructors in it, type was ${typeRepr.show} (${typeRepr.typeSymbol}"
+      )
+    }
+  }
+
+}
diff --git a/distage/distage-core-api/src/main/scala-3/izumi/distage/constructors/TraitConstructorMacro.scala b/distage/distage-core-api/src/main/scala-3/izumi/distage/constructors/TraitConstructorMacro.scala
index ce4384101..606b21e54 100644
--- a/distage/distage-core-api/src/main/scala-3/izumi/distage/constructors/TraitConstructorMacro.scala
+++ b/distage/distage-core-api/src/main/scala-3/izumi/distage/constructors/TraitConstructorMacro.scala
@@ -99,6 +99,6 @@ object TraitConstructorMacro {
     val f = util.makeFunctoid[R](lamParams, lamExpr, '{ ProviderType.Trait })
     '{ new TraitConstructor[R](${ f }) }
 
-  } catch { case t: Throwable => qctx.reflect.report.errorAndAbort(t.stackTrace) }
+  } catch { case t: scala.quoted.runtime.StopMacroExpansion => throw t; case t: Throwable => qctx.reflect.report.errorAndAbort(t.stackTrace) }
 
 }
diff --git a/distage/distage-core-api/src/main/scala-3/izumi/distage/constructors/constructors.scala b/distage/distage-core-api/src/main/scala-3/izumi/distage/constructors/constructors.scala
index 4994457e0..33d91aeb4 100644
--- a/distage/distage-core-api/src/main/scala-3/izumi/distage/constructors/constructors.scala
+++ b/distage/distage-core-api/src/main/scala-3/izumi/distage/constructors/constructors.scala
@@ -119,7 +119,7 @@ object HasConstructor {
 
   val empty: HasConstructor[Any] = new HasConstructor(Functoid.unit)
 
-  inline implicit def materialize[T]: HasConstructor[T] = ???
+  inline implicit def materialize[T]: HasConstructor[T] = ${ HasConstructorMacro.make[T] }
 }
 
 private[constructors] sealed trait AnyConstructorOptionalMakeDSL[T] extends Any {
diff --git a/fundamentals/fundamentals-reflection/src/main/scala-3/izumi/fundamentals/platform/reflection/ReflectionUtil.scala b/fundamentals/fundamentals-reflection/src/main/scala-3/izumi/fundamentals/platform/reflection/ReflectionUtil.scala
index ad4ae89d9..8051e59c8 100644
--- a/fundamentals/fundamentals-reflection/src/main/scala-3/izumi/fundamentals/platform/reflection/ReflectionUtil.scala
+++ b/fundamentals/fundamentals-reflection/src/main/scala-3/izumi/fundamentals/platform/reflection/ReflectionUtil.scala
@@ -1,6 +1,7 @@
 package izumi.fundamentals.platform.reflection
 
 import scala.quoted.Quotes
+import scala.collection.mutable
 
 object ReflectionUtil {
 
@@ -28,4 +29,21 @@ object ReflectionUtil {
     }
   }
 
+  def intersectionMembers(using q: Quotes)(typeRepr: q.reflect.TypeRepr): List[q.reflect.TypeRepr] = {
+    import q.reflect.*
+
+    val tpes = mutable.HashSet.empty[TypeRepr]
+
+    def go(t0: TypeRepr): Unit = t0.dealias match {
+      case tpe: AndOrType =>
+        go(tpe.left)
+        go(tpe.right)
+      case t =>
+        tpes += t
+    }
+
+    go(typeRepr)
+    tpes.toList
+  }
+
 }


Test patch:
diff --git a/distage/distage-core/src/test/scala/izumi/distage/injector/ZIOHasInjectionTest.scala b/distage/distage-core/src/test/scala/izumi/distage/injector/ZIOHasInjectionTest.scala
index a65524746..b71af3ecd 100644
--- a/distage/distage-core/src/test/scala/izumi/distage/injector/ZIOHasInjectionTest.scala
+++ b/distage/distage-core/src/test/scala/izumi/distage/injector/ZIOHasInjectionTest.scala
@@ -64,7 +64,7 @@ class ZIOHasInjectionTest extends AnyWordSpec with MkInjector {
       val error = intercept[TestFailedException](
         assertCompiles(
           """new ModuleDef {
-             make[TestClass2[Dep]].fromHas { value: Has[Dep] =>
+             make[TestClass2[Dep]].fromHas { (value: Has[Dep]) =>
                ZIO(TestClass2(value.get)) : ZIO[Int, Throwable, TestClass2[Dep]]
              }
            }"""
@@ -166,6 +166,8 @@ class ZIOHasInjectionTest extends AnyWordSpec with MkInjector {
           d1 <- ZManaged.access[Has[Dependency1]](_.get)
           d2 <- ZManaged.access[Has[Dependency2]](_.get)
         } yield new Trait2 { val dep1 = d1; val dep2 = d2 })
+//        make[Trait1].fromHas[Any, Nothing, Trait1] {
+        // FIXME: report bug - Dotty infers this as `make[Trait1].fromHas[Any, Any, Trait1]` - awful inference here wtf?
         make[Trait1].fromHas {
           (d1: Dependency1) =>
             ZLayer.succeed(new Trait1 { val dep1 = d1 })