Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package io.github.arainko.ducktape
import io.github.arainko.ducktape.builder.*
import io.github.arainko.ducktape.internal.macros.*

import scala.annotation.implicitNotFound
import scala.collection.Factory
import scala.deriving.Mirror

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ private[ducktape] object CoproductTransformations {
given Cases.Source = Cases.Source.fromMirror(Source)
given Cases.Dest = Cases.Dest.fromMirror(Dest)

val ifBranches = singletonIfBranches[Source, Dest](sourceValue, Cases.source.value)
val ifBranches = coproductBranches[Source, Dest](sourceValue, Cases.source.value)
ifStatement(ifBranches).asExprOf[Dest]
}

Expand All @@ -42,41 +42,34 @@ private[ducktape] object CoproductTransformations {
val (nonConfiguredCases, configuredCases) =
Cases.source.value.partition(c => !materializedConfig.contains(c.tpe.fullName))

val nonConfiguredIfBranches = singletonIfBranches[Source, Dest](sourceValue, nonConfiguredCases)
val nonConfiguredIfBranches = coproductBranches[Source, Dest](sourceValue, nonConfiguredCases)

val configuredIfBranches =
configuredCases
.map(c => c.tpe.fullName -> c)
.toMap
.map {
case (fullName, source) =>
ConfiguredCase(materializedConfig(fullName), source)
}
.map {
case ConfiguredCase(config, source) =>
config match {
materializedConfig(fullName) match {
case Coproduct.Computed(tpe, function) =>
val cond = source.tpe match {
case '[tpe] => '{ $sourceValue.isInstanceOf[tpe] }
}
val castedSource = tpe match {
case '[tpe] => '{ $sourceValue.asInstanceOf[tpe] }
val value = tpe match {
case '[tpe] =>
'{
val casted = $sourceValue.asInstanceOf[tpe]
$function(casted)
}
}
val value = '{ $function($castedSource) }
cond.asTerm -> value.asTerm
IfBranch(IsInstanceOf(sourceValue, source.tpe), value)

case Coproduct.Const(tpe, value) =>
val cond = source.tpe match {
case '[tpe] => '{ $sourceValue.isInstanceOf[tpe] }
}
cond.asTerm -> value.asTerm
IfBranch(IsInstanceOf(sourceValue, source.tpe), value)
}
}

ifStatement(nonConfiguredIfBranches ++ configuredIfBranches).asExprOf[Dest]
}

private def singletonIfBranches[Source: Type, Dest: Type](
private def coproductBranches[Source: Type, Dest: Type](
sourceValue: Expr[Source],
sourceCases: List[Case]
)(using Quotes, Cases.Dest) = {
Expand All @@ -87,26 +80,44 @@ private[ducktape] object CoproductTransformations {
.get(source.name)
.getOrElse(Failure.emit(Failure.NoChildMapping(source.name, summon[Type[Dest]])))
}.map { (source, dest) =>
val cond = source.tpe match {
case '[tpe] => '{ $sourceValue.isInstanceOf[tpe] }
}
val cond = IsInstanceOf(sourceValue, source.tpe)

(source.tpe -> dest.tpe) match {
case '[src] -> '[dest] =>
val value =
source.transformerTo(dest).map {
case '{ $t: Transformer[src, dest] } =>
'{
val castedSource = $sourceValue.asInstanceOf[src]
${ LiftTransformation.liftTransformation(t, 'castedSource) }
}
} match {
case Right(value) => value
case Left(explanation) =>
dest.materializeSingleton
.getOrElse(Failure.emit(Failure.CannotTransformCoproductCase(source.tpe, dest.tpe, explanation)))
}

cond.asTerm ->
dest.materializeSingleton
.getOrElse(Failure.emit(Failure.CannotMaterializeSingleton(dest.tpe)))
IfBranch(cond, value)
}
}
}

private def ifStatement(using Quotes)(branches: List[(quotes.reflect.Term, quotes.reflect.Term)]): quotes.reflect.Term = {
private def ifStatement(using Quotes)(branches: List[IfBranch]): quotes.reflect.Term = {
import quotes.reflect.*

branches match {
case (p1, a1) :: xs =>
If(p1, a1, ifStatement(xs))
case IfBranch(cond, value) :: xs =>
If(cond.asTerm, value.asTerm, ifStatement(xs))
case Nil =>
'{ throw RuntimeException("Unhandled condition encountered during Coproduct Transformer derivation") }.asTerm
}
}

private case class ConfiguredCase(config: Coproduct, subcase: Case)
private def IsInstanceOf(value: Expr[Any], tpe: Type[?])(using Quotes) =
tpe match {
case '[tpe] => '{ $value.isInstanceOf[tpe] }
}

private case class IfBranch(cond: Expr[Boolean], value: Expr[Any])
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,33 @@
package io.github.arainko.ducktape.internal.modules

import io.github.arainko.ducktape.Transformer

import scala.quoted.*

private[ducktape] final case class Case(
val name: String,
val tpe: Type[?],
val ordinal: Int
val tpe: Type[?]
) {
def materializeSingleton(using Quotes): Option[quotes.reflect.Term] = {

def transformerTo(that: Case)(using Quotes): Either[String, Expr[Transformer[?, ?]]] = {
import quotes.reflect.*

(tpe -> that.tpe) match {
case '[src] -> '[dest] =>
Implicits.search(TypeRepr.of[Transformer[src, dest]]) match {
case success: ImplicitSearchSuccess => Right(success.tree.asExprOf[Transformer[src, dest]])
case err: ImplicitSearchFailure => Left(err.explanation)
}
}
}

def materializeSingleton(using Quotes): Option[Expr[Any]] = {
import quotes.reflect.*

val typeRepr = TypeRepr.of(using tpe)

Option.when(typeRepr.isSingleton) {
typeRepr match { case TermRef(a, b) => Ident(TermRef(a, b)) }
typeRepr match { case ref: TermRef => Ident(ref).asExpr }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ private[ducktape] sealed trait Cases {
val byName: Map[String, Case] = value.map(c => c.name -> c).toMap
}

object Cases {
private[ducktape] object Cases {
def source(using sourceCases: Cases.Source): Cases.Source = sourceCases
def dest(using destCases: Cases.Dest): Cases.Dest = destCases

Expand All @@ -25,12 +25,11 @@ object Cases {
def apply(cases: List[Case]): CasesSubtype

final def fromMirror[A: Type](mirror: Expr[Mirror.SumOf[A]])(using Quotes): CasesSubtype = {
val materializedMirror = MaterializedMirror.createOrAbort(mirror)
val materializedMirror = MaterializedMirror.create(mirror)

val cases = materializedMirror.mirroredElemLabels
.zip(materializedMirror.mirroredElemTypes)
.zipWithIndex
.map { case name -> tpe -> ordinal => Case(name, tpe.asType, ordinal) }
.map(Case.apply)

apply(cases)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ private[ducktape] object Constructor {
case notApplied => (tpe, tpe.typeSymbol.primaryConstructor, Nil)
}

// workaround for invoking constructors of singleton which in turn actually create new instances of singletons!
if (tpe.typeSymbol.flags.is(Flags.Module)) report.errorAndAbort("Cannot invoke constructor of a singleton")

New(Inferred(repr))
.select(constructor)
.appliedToTypes(tpeArgs)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,13 +142,15 @@ private[ducktape] object Failure {
override final def render(using Quotes): String = s"No child named '$childName' found in ${destinationType.show}"
}

final case class CannotMaterializeSingleton(tpe: Type[?]) extends Failure {
private def suggestions(using Quotes) = Suggestion.all(s"${tpe.show} is not a singleton type")

final case class CannotTransformCoproductCase(source: Type[?], dest: Type[?], implicitSearchExplanation: String)
extends Failure {
override final def render(using Quotes): String =
s"""
|Cannot materialize singleton for ${tpe.show}.
|Possible causes: ${Suggestion.renderAll(suggestions)}
|Neither an instance of Transformer[${source.fullName}, ${dest.fullName}] was found nor are '${source.show}' '${dest.show}'
|singletons with the same name.
|
|Compiler supplied explanation for the failed Transformer derivation (may or may not be helpful):
|$implicitSearchExplanation
""".stripMargin
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import io.github.arainko.ducktape.fallible.FallibleTransformer

import scala.quoted.*

private[ducktape] final class Field(val name: String, val tpe: Type[?], val default: Option[Expr[Any]]) {
private[ducktape] final class Field(val name: String, val tpe: Type[?], defaultValue: => Option[Expr[Any]]) {

lazy val default: Option[Expr[Any]] = defaultValue

def transformerTo(that: Field)(using Quotes): Expr[Transformer[?, ?]] = {
import quotes.reflect.*

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,14 @@ private[ducktape] object Fields {
def apply(fields: List[Field]): FieldsSubtype

final def fromMirror[A: Type](mirror: Expr[Mirror.ProductOf[A]])(using Quotes): FieldsSubtype = {
val materializedMirror = MaterializedMirror.createOrAbort(mirror)
val materializedMirror = MaterializedMirror.create(mirror)

lazy val defaults = defaultParams[A]

val defaults = defaultParams[A]
val fields = materializedMirror.mirroredElemLabels
.zip(materializedMirror.mirroredElemTypes)
.map((name, tpe) => Field(name, tpe.asType, defaults.get(name)))
.map((name, tpe) => Field(name, tpe, defaults.get(name)))

apply(fields)
}

Expand All @@ -53,21 +55,21 @@ private[ducktape] object Fields {
apply(fields)
}

private def defaultParams[T: Type](using Quotes): Map[String, Expr[Any]] = {
private def defaultParams[A: Type](using Quotes): Map[String, Expr[Any]] = {
import quotes.reflect.*

val typ = TypeRepr.of[T]
val sym = typ.typeSymbol
val typeArgs = typ.typeArgs
val mod = Ref(sym.companionModule)
val names = sym.caseFields.filter(_.flags.is(Flags.HasDefault)).map(_.name)
val body = sym.companionClass.tree.asInstanceOf[ClassDef].body
val idents: List[Term] = body.collect {
val tpe = TypeRepr.of[A]
val sym = tpe.typeSymbol
val typeArgs = tpe.typeArgs
val companion = Ref(sym.companionModule)
val fieldNamesWithDefaults = sym.caseFields.filter(_.flags.is(Flags.HasDefault)).map(_.name)
val companionBody = sym.companionClass.tree.asInstanceOf[ClassDef].body
val defaultValues = companionBody.collect {
case deff @ DefDef(name, _, _, _) if name.startsWith("$lessinit$greater$default") =>
mod.select(deff.symbol).appliedToTypes(typeArgs)
companion.select(deff.symbol).appliedToTypes(typeArgs).asExpr
}

names.zip(idents.map(_.asExpr)).toMap
fieldNamesWithDefaults.zip(defaultValues).toMap
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,67 +4,40 @@ import scala.annotation.tailrec
import scala.deriving.Mirror
import scala.quoted.*

private[ducktape] final class MaterializedMirror[Q <: Quotes & Singleton] private (using val quotes: Q)(
val mirroredType: quotes.reflect.TypeRepr,
val mirroredMonoType: quotes.reflect.TypeRepr,
val mirroredElemTypes: List[quotes.reflect.TypeRepr],
val mirroredLabel: String,
private[ducktape] final class MaterializedMirror(
val mirroredElemTypes: List[Type[?]],
val mirroredElemLabels: List[String]
)

// Lifted from shapeless 3:
// https://github.com/typelevel/shapeless-3/blob/main/modules/deriving/src/main/scala/shapeless3/deriving/internals/reflectionutils.scala
private[ducktape] object MaterializedMirror {

def createOrAbort[A: Type](mirror: Expr[Mirror.Of[A]])(using Quotes): MaterializedMirror[quotes.type] =
create(mirror).fold(memberName => Failure.emit(Failure.MirrorMaterialization(summon, memberName)), identity)

private def create(mirror: Expr[Mirror])(using Quotes): Either[String, MaterializedMirror[quotes.type]] = {
import quotes.reflect.*

val mirrorTpe = mirror.asTerm.tpe.widen
for {
mirroredType <- findMemberType(mirrorTpe, "MirroredType")
mirroredMonoType <- findMemberType(mirrorTpe, "MirroredMonoType")
mirroredElemTypes <- findMemberType(mirrorTpe, "MirroredElemTypes")
mirroredLabel <- findMemberType(mirrorTpe, "MirroredLabel")
mirroredElemLabels <- findMemberType(mirrorTpe, "MirroredElemLabels")
} yield {
val elemTypes = tupleTypeElements(mirroredElemTypes)
val ConstantType(StringConstant(label)) = mirroredLabel: @unchecked
val elemLabels = tupleTypeElements(mirroredElemLabels).map { case ConstantType(StringConstant(l)) => l }
MaterializedMirror(mirroredType, mirroredMonoType, elemTypes, label, elemLabels)
def create[A: Type](mirror: Expr[Mirror.Of[A]])(using Quotes): MaterializedMirror =
mirror match {
case '{
$m: Mirror.Of[A] {
type MirroredElemTypes = types
type MirroredElemLabels = labels
}
} =>
import quotes.reflect.*
val elemTypes = tupleTypeElements(TypeRepr.of[types])
val labels = tupleTypeElements(TypeRepr.of[labels]).map {
case '[IsString[tpe]] => Type.valueOfConstant[tpe].getOrElse(report.errorAndAbort("Couldn't extract constact value"))
}
MaterializedMirror(elemTypes, labels)
}
}

private def tupleTypeElements(using Quotes)(tp: quotes.reflect.TypeRepr): List[quotes.reflect.TypeRepr] = {
private def tupleTypeElements(using Quotes)(tp: quotes.reflect.TypeRepr): List[Type[?]] = {
import quotes.reflect.*

@tailrec def loop(tp: TypeRepr, acc: List[TypeRepr]): List[TypeRepr] = tp match {
case AppliedType(pairTpe, List(hd: TypeRepr, tl: TypeRepr)) => loop(tl, hd :: acc)
case _ => acc
}
@tailrec def loop(curr: TypeRepr, acc: List[Type[?]]): List[Type[?]] =
curr match {
case AppliedType(pairTpe, head :: tail :: Nil) => loop(tail, head.asType :: acc)
case _ => acc
}
loop(tp, Nil).reverse
}

private def low(using Quotes)(tp: quotes.reflect.TypeRepr): quotes.reflect.TypeRepr = {
import quotes.reflect.*

tp match {
case tp: TypeBounds => tp.low
case tp => tp
}
}

private def findMemberType(using Quotes)(tp: quotes.reflect.TypeRepr, name: String): Either[String, quotes.reflect.TypeRepr] = {
import quotes.reflect.*

tp match {
case Refinement(_, `name`, tp) => Right(low(tp))
case Refinement(parent, _, _) => findMemberType(parent, name)
case AndType(left, right) => findMemberType(left, name).orElse(findMemberType(right, name))
case _ => Left(name)
}
}
private type IsString[A <: String] = A

}
Loading