Skip to content

Commit

Permalink
[Issues #187 and #190] Make F-unwrapping not interfere with special-c…
Browse files Browse the repository at this point in the history
…ased Option transformations and make transformations more bulletproof in relation to match types (#191)

Fixes #187
Fixes #190
  • Loading branch information
arainko authored Jul 28, 2024
2 parents c89672c + ff6b08c commit ba40550
Show file tree
Hide file tree
Showing 9 changed files with 213 additions and 40 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.github.arainko.ducktape.internal

import io.github.arainko.ducktape.*
import io.github.arainko.ducktape.internal.Configuration.*

private[ducktape] object ConfigInstructionRefiner {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ private[ducktape] object Context {
transparent inline def current(using ctx: Context): ctx.type = ctx

case class PossiblyFallible[G[+x]](
wrapperType: WrapperType.Wrapped[G],
wrapperType: WrapperType[G],
transformationSite: TransformationSite,
summoner: Summoner.PossiblyFallible[G],
mode: TransformationMode[G]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ private[ducktape] object FallibleTransformations {
configs: Expr[Seq[Field.Fallible[F, A, B] | Case.Fallible[F, A, B]]]
)(using Quotes): Expr[F[B]] = {
given Context.PossiblyFallible[F](
WrapperType.Wrapped(Type.of[F]),
WrapperType.create[F],
TransformationSite.fromStringExpr(transformationSite),
Summoner.PossiblyFallible[F],
TransformationMode.create(F)
Expand Down Expand Up @@ -50,7 +50,7 @@ private[ducktape] object FallibleTransformations {
configs: Expr[Seq[Field.Fallible[F, A, Args] | Case.Fallible[F, A, Args]]]
)(using Quotes): Expr[F[B]] = {
given Context.PossiblyFallible[F](
WrapperType.Wrapped(Type.of[F]),
WrapperType.create[F],
TransformationSite.fromStringExpr(transformationSite),
Summoner.PossiblyFallible[F],
TransformationMode.create(F)
Expand Down Expand Up @@ -92,7 +92,7 @@ private[ducktape] object FallibleTransformations {
import quotes.reflect.*

given Context.PossiblyFallible[F](
WrapperType.Wrapped(Type.of[F]),
WrapperType.create[F],
TransformationSite.Transformation,
Summoner.PossiblyFallible[F],
TransformationMode.create(F)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,13 @@ private[ducktape] object Plan {
) extends Plan[Nothing, Nothing]

case class BetweenFallibleNonFallible[+E <: Erroneous](
source: Structure.Wrappped[?],
source: Structure.Wrapped[?],
dest: Structure,
plan: Plan[E, Nothing]
) extends Plan[E, Fallible]

case class BetweenFallibles[+E <: Erroneous](
source: Structure.Wrappped[?],
source: Structure.Wrapped[?],
dest: Structure,
mode: TransformationMode.FailFast[?],
plan: Plan[E, Fallible]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,22 @@ private[ducktape] object Planner {
recurse(source, paramStruct)
)

// Wrapped(WrapperType.Optional) is isomorphic to Optional
// scalafmt: { maxColumn = 150 }
case (source @ Wrapped(_, WrapperType.Optional, _, srcUnderlying)) -> (dest @ Wrapped(_, WrapperType.Optional, _, destUnderlying)) =>
Plan.BetweenOptions(
Structure.Optional.fromWrapped(source),
Structure.Optional.fromWrapped(dest),
recurse(srcUnderlying, destUnderlying)
)

case source -> (dest @ Wrapped(_, WrapperType.Optional, _, underlying)) =>
Plan.BetweenNonOptionOption(
source,
Structure.Optional.fromWrapped(dest),
recurse(source, underlying)
)

case (source @ Collection(_, _, srcParamStruct)) -> (dest @ Collection(_, _, destParamStruct)) =>
Plan.BetweenCollections(
source,
Expand Down Expand Up @@ -246,8 +262,7 @@ private[ducktape] object Planner {
boundary[Plan.Error | plan.type]:
var owner = Symbol.spliceOwner
while (!owner.isNoSymbol) {
if owner == transformerSymbol then
boundary.break(Plan.Error.from(plan, ErrorMessage.LoopingTransformerDetected, None))
if owner == transformerSymbol then boundary.break(Plan.Error.from(plan, ErrorMessage.LoopingTransformerDetected, None))
owner = owner.maybeOwner
}
plan
Expand All @@ -260,7 +275,7 @@ private[ducktape] object Planner {
structs: (Structure, Structure)
)(using Quotes, Depth, Context.Of[F]): Option[Plan[Erroneous, F]] =
PartialFunction.condOpt(Context.current *: structs) {
case (ctx: Context.PossiblyFallible[f], source @ Wrappped(tpe, path, underlying), dest) =>
case (ctx: Context.PossiblyFallible[f], source @ Wrapped(tpe, _, path, underlying), dest) =>
// needed for the recurse call to return Plan[Erroneous, Nothing]
given Context.Total = ctx.toTotal
val plan = Plan.BetweenFallibleNonFallible(
Expand All @@ -283,7 +298,7 @@ private[ducktape] object Planner {
PartialFunction.condOpt(Context.current *: structs) {
case (
ctx @ Context.PossiblyFallible(_, _, _, mode: TransformationMode.FailFast[f]),
source @ Wrappped(tpe, path, underlying),
source @ Wrapped(tpe, _, path, underlying),
dest
) =>
ctx.reifyPlan[F] {
Expand All @@ -296,13 +311,8 @@ private[ducktape] object Planner {
}

case (
ctx @ Context.PossiblyFallible(
WrapperType.Wrapped(given Type[f]),
_,
_,
TransformationMode.Accumulating(mode, Some(localMode))
),
source @ Wrappped(tpe, path, underlying),
ctx @ Context.PossiblyFallible(_, _, _, TransformationMode.Accumulating(mode, Some(localMode))),
source @ Wrapped(tpe, _, path, underlying),
dest
) =>
ctx.reifyPlan[F] {
Expand All @@ -313,7 +323,6 @@ private[ducktape] object Planner {
recurse(underlying, dest)
)
}

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ private[ducktape] object Structure {

case class Optional(tpe: Type[? <: Option[?]], path: Path, paramStruct: Structure) extends Structure

object Optional {
def fromWrapped(wrapped: Wrapped[Option]): Optional =
Optional(wrapped.tpe, wrapped.path, wrapped.underlying)
}

case class Collection(tpe: Type[? <: Iterable[?]], path: Path, paramStruct: Structure) extends Structure

case class Singleton(tpe: Type[?], path: Path, name: String, value: Expr[Any]) extends Structure
Expand All @@ -60,7 +65,7 @@ private[ducktape] object Structure {

case class ValueClass(tpe: Type[? <: AnyVal], path: Path, paramTpe: Type[?], paramFieldName: String) extends Structure

case class Wrappped[F[+x]](tpe: Type[? <: F[Any]], path: Path, underlying: Structure) extends Structure
case class Wrapped[F[+x]](tpe: Type[? <: F[Any]], wrapper: WrapperType[F], path: Path, underlying: Structure) extends Structure

case class Lazy private (tpe: Type[?], path: Path, private val deferredStruct: () => Structure) extends Structure {
lazy val struct: Structure = deferredStruct()
Expand Down Expand Up @@ -95,10 +100,11 @@ private[ducktape] object Structure {
case tpe @ '[Nothing] =>
Structure.Ordinary(tpe, path)

case WrapperType(wrapper: WrapperType.Wrapped[f], '[underlying]) =>
@unused given Type[f] = wrapper.wrapperTpe
Structure.Wrappped(
case WrapperType(wrapper: WrapperType[f], '[underlying]) =>
@unused given Type[f] = wrapper.wrapper
Structure.Wrapped(
Type.of[f[underlying]],
wrapper,
path,
Structure.of[underlying](path.appended(Path.Segment.Element(Type.of[underlying])))
)
Expand All @@ -117,7 +123,7 @@ private[ducktape] object Structure {

case tpe @ '[Any *: scala.Tuple] if !tpe.repr.isTupleN => // let plain tuples be caught later on
val elements =
tupleTypeElements(tpe.repr.dealias).zipWithIndex.map { (tpe, idx) =>
tupleTypeElements(tpe).zipWithIndex.map { (tpe, idx) =>
tpe.asType match {
case '[tpe] => Lazy.of[tpe](path.appended(Path.Segment.TupleElement(Type.of[tpe], idx)))
}
Expand Down Expand Up @@ -155,7 +161,7 @@ private[ducktape] object Structure {
}
} if tpe.repr.isTupleN =>
val structures =
tupleTypeElements(TypeRepr.of[types]).zipWithIndex
tupleTypeElements(Type.of[types]).zipWithIndex
.map((tpe, idx) =>
tpe.asType match {
case '[tpe] => Lazy.of[tpe](path.appended(Path.Segment.TupleElement(Type.of[tpe], idx)))
Expand All @@ -172,7 +178,7 @@ private[ducktape] object Structure {
}
} =>
val structures =
tupleTypeElements(TypeRepr.of[types])
tupleTypeElements(Type.of[types])
.zip(constStringTuple(TypeRepr.of[labels]))
.map((tpe, name) =>
name -> (tpe.asType match {
Expand All @@ -189,7 +195,7 @@ private[ducktape] object Structure {
}
} =>
val structures =
tupleTypeElements(TypeRepr.of[types])
tupleTypeElements(Type.of[types])
.zip(constStringTuple(TypeRepr.of[labels]))
.map((tpe, name) =>
name -> (tpe.asType match { case '[tpe] => Lazy.of[tpe](path.appended(Path.Segment.Case(Type.of[tpe]))) })
Expand All @@ -209,21 +215,27 @@ private[ducktape] object Structure {

private def constantString[Const <: String: Type](using Quotes) = Type.valueOfConstant[Const].get

private def tupleTypeElements(using Quotes)(tp: quotes.reflect.TypeRepr): List[quotes.reflect.TypeRepr] = {
import quotes.reflect.*
private def tupleTypeElements(tpe: Type[?])(using Quotes): List[quotes.reflect.TypeRepr] = {
@tailrec def loop(using Quotes)(curr: Type[?], acc: List[quotes.reflect.TypeRepr]): List[quotes.reflect.TypeRepr] = {
import quotes.reflect.*

@tailrec def loop(curr: TypeRepr, acc: List[TypeRepr]): List[TypeRepr] =
curr match {
case AppliedType(pairTpe, head :: tail :: Nil) =>
loop(tail, head :: acc)
case _ =>
case '[head *: tail] =>
loop(Type.of[tail], TypeRepr.of[head] :: acc)
case '[EmptyTuple] =>
acc
case other =>
report.errorAndAbort(
s"Unexpected type (${other.repr.show}) encountered when extracting tuple type elems. This is a bug in ducktape."
)
}
loop(tp, Nil).reverse
}

loop(tpe, Nil).reverse
}

private def constStringTuple(using Quotes)(tp: quotes.reflect.TypeRepr): List[String] = {
import quotes.reflect.*
tupleTypeElements(tp).map { case ConstantType(StringConstant(l)) => l }
tupleTypeElements(tp.asType).map { case ConstantType(StringConstant(l)) => l }
}
}
Original file line number Diff line number Diff line change
@@ -1,26 +1,58 @@
package io.github.arainko.ducktape.internal

import io.github.arainko.ducktape.internal.Debug.AST

import scala.annotation.unused
import scala.quoted.*

private[ducktape] sealed trait WrapperType {
def unapply(tpe: Type[?])(using Quotes): Option[(WrapperType, Type[?])]
private[ducktape] sealed trait WrapperType[F[+x]] {
def wrapper(using Quotes): Type[F]

def unapply(tpe: Type[?])(using Quotes): Option[(WrapperType[F], Type[?])]
}

private[ducktape] object WrapperType {
def create[F[+x]: Type](using Quotes): WrapperType[F] = {
import quotes.reflect.*

Type.of[F[Any]] match {
case '[Option[a]] =>
Optional.asInstanceOf[WrapperType[F]]
case other =>
Wrapped(Type.of[F])
}
}

given Debug[WrapperType[?]] with {
def astify(self: WrapperType[?])(using Quotes): AST =
import quotes.reflect.*
self match
case Optional => Debug.AST.Text(s"WrapperType[Option]")
case Wrapped(wrapperTpe) => Debug.AST.Text(s"WrapperType[${wrapperTpe.repr.show(using Printer.TypeReprShortCode)}]")
}

def unapply(using Quotes, Context)(tpe: Type[?]) =
Context.current match {
case ctx: Context.PossiblyFallible[?] => ctx.wrapperType.unapply(tpe)
case Context.Total(_) => None
}

case object Absent extends WrapperType {
override def unapply(tpe: Type[? <: AnyKind])(using Quotes): Option[(WrapperType, Type[?])] = None
case object Optional extends WrapperType[Option] {

def wrapper(using Quotes): Type[Option] = Type.of[Option]

override def unapply(tpe: Type[? <: AnyKind])(using Quotes): Option[(WrapperType[Option], Type[?])] = {
tpe match {
case '[Option[underlying]] => Some(this -> Type.of[underlying])
case _ => None
}
}
}

final case class Wrapped[F[+x]](wrapperTpe: Type[F]) extends WrapperType {
override def unapply(tpe: Type[? <: AnyKind])(using Quotes): Option[(WrapperType, Type[?])] = {
final case class Wrapped[F[+x]] private[WrapperType] (wrapperTpe: Type[F]) extends WrapperType[F] {
def wrapper(using Quotes): Type[F] = wrapperTpe

override def unapply(tpe: Type[? <: AnyKind])(using Quotes): Option[(WrapperType[F], Type[?])] = {
@unused given Type[F] = wrapperTpe
tpe match
case '[F[underlying]] => Some(this -> Type.of[underlying])
Expand Down
Loading

0 comments on commit ba40550

Please sign in to comment.