Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
4 changes: 3 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ ThisBuild / semanticdbEnabled := true
ThisBuild / semanticdbVersion := scalafixSemanticdb.revision

ThisBuild / mimaBinaryIssueFilters ++= Seq(
ProblemFilters.exclude[Problem]("io.github.arainko.ducktape.internal.*")
ProblemFilters.exclude[Problem]("io.github.arainko.ducktape.internal.*"),
// Selector only exists at compiletime
ProblemFilters.exclude[ReversedMissingMethodProblem]("io.github.arainko.ducktape.Selector.element")
)

ThisBuild / tlCiReleaseBranches := Seq("series/0.1.x", "series/0.2.x")
Expand Down
3 changes: 3 additions & 0 deletions ducktape/src/main/scala/io/github/arainko/ducktape/Mode.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ private given Transformer.Mode.Either[String, List] with {}
"""
)
sealed trait Mode[F[+x]] {
final type Self[+x] = F[x]

def pure[A](value: A): F[A]
def map[A, B](fa: F[A], f: A => B): F[B]
def traverseCollection[A, B, AColl <: Iterable[A], BColl <: Iterable[B]](
Expand All @@ -19,6 +21,7 @@ sealed trait Mode[F[+x]] {
}

object Mode {
inline def current(using mode: Mode[?]): mode.type = mode
extension [F[+x], M <: Mode[F]](self: M) {
inline def locally[A](inline f: M ?=> A): A = f(using self)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ sealed trait Selector {
extension [A](self: A) def at[B <: A]: B

extension [Elem](self: Iterable[Elem] | Option[Elem]) def element: Elem

extension [Elem, F[+x]](using Mode[F])(self: F[Elem]) def element: Elem
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import scala.quoted.runtime.StopMacroExpansion

private[ducktape] object Backend {

def refineOrReportErrorsAndAbort[F <: Fallible](
plan: Plan[Plan.Error, F],
def refineOrReportErrorsAndAbort[F <: Fallible](using Context.Of[F])(
plan: Plan[Erroneous, F],
configs: List[Configuration.Instruction[F]]
)(using Quotes) = {
import quotes.reflect.*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.github.arainko.ducktape.internal

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

private[ducktape] object ConfigInstructionRefiner {

def run[F <: Fallible](instruction: Configuration.Instruction[F]): Configuration.Instruction[Nothing] | None.type =
instruction match
case inst @ Instruction.Static(_, _, config, _) =>
config match
case cfg: (Const | CaseComputed | FieldComputed | FieldReplacement) => inst.copy(config = cfg)
case fallible: (FallibleConst | FallibleFieldComputed | FallibleCaseComputed) => None
case inst: (Instruction.Dynamic | Instruction.Bulk | Instruction.Regional | Instruction.Failed) => inst

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,22 @@ import scala.quoted.*
import Configuration.*

private[ducktape] sealed trait ConfigParser[+F <: Fallible] {
def apply(using Quotes): PartialFunction[quotes.reflect.Term, Instruction[F]]
def apply(using Quotes, Context): PartialFunction[quotes.reflect.Term, Instruction[F]]
}

private[ducktape] object ConfigParser {
val total = NonEmptyList(Total)

def fallible[F[+x]: Type] = NonEmptyList(Total, PossiblyFallible[F])

def combine[F <: Fallible](parsers: NonEmptyList[ConfigParser[F]])(using
Quotes
Quotes,
Context
): PartialFunction[quotes.reflect.Term, Instruction[F]] =
parsers.map(_.apply).reduceLeft(_ orElse _)

object Total extends ConfigParser[Nothing] {
def apply(using Quotes): PartialFunction[quotes.reflect.Term, Instruction[Nothing]] = {
def apply(using Quotes, Context): PartialFunction[quotes.reflect.Term, Instruction[Nothing]] = {
import quotes.reflect.*
{
case cfg @ Apply(
Expand All @@ -35,7 +40,7 @@ private[ducktape] object ConfigParser {
TypeApply(Select(IdentOfType('[Field.type]), "default"), a :: b :: destFieldTpe :: Nil),
PathSelector(path) :: Nil
) =>
def default(parent: Plan[Plan.Error, Fallible] | None.type) =
def default(parent: Plan[Erroneous, Fallible] | None.type) =
for {
selectedField <-
path.segments.lastOption
Expand All @@ -44,8 +49,8 @@ private[ducktape] object ConfigParser {
defaults <-
PartialFunction
.condOpt(parent) {
case parent: Plan.BetweenProducts[Plan.Error, Fallible] => parent.dest.defaults
case parent: Plan.BetweenTupleProduct[Plan.Error, Fallible] => parent.dest.defaults
case parent: Plan.BetweenProducts[Erroneous, Fallible] => parent.dest.defaults
case parent: Plan.BetweenTupleProduct[Erroneous, Fallible] => parent.dest.defaults
}
.toRight("Selected field's parent is not a product")
defaultValue <-
Expand Down Expand Up @@ -141,7 +146,7 @@ private[ducktape] object ConfigParser {
}

class PossiblyFallible[F[+x]: Type] extends ConfigParser[Fallible] {
def apply(using Quotes): PartialFunction[quotes.reflect.Term, Instruction[Fallible]] = {
def apply(using Quotes, Context): PartialFunction[quotes.reflect.Term, Instruction[Fallible]] = {
import quotes.reflect.*
{
case cfg @ Apply(
Expand Down Expand Up @@ -195,7 +200,7 @@ private[ducktape] object ConfigParser {
}
}

private def parseAllMatching(using Quotes)(
private def parseAllMatching(using Quotes, Context)(
sourceExpr: Expr[Any],
path: Path,
fieldSourceTpe: quotes.reflect.TypeRepr,
Expand All @@ -208,10 +213,10 @@ private[ducktape] object ConfigParser {
.map { sourceStruct =>
val modifier = new FieldModifier:
def apply(
parent: Plan.BetweenProductFunction[Plan.Error, Fallible] | Plan.BetweenProducts[Plan.Error, Fallible] |
Plan.BetweenTupleProduct[Plan.Error, Fallible],
parent: Plan.BetweenProductFunction[Erroneous, Fallible] | Plan.BetweenProducts[Erroneous, Fallible] |
Plan.BetweenTupleProduct[Erroneous, Fallible],
field: String,
plan: Plan[Plan.Error, Fallible]
plan: Plan[Erroneous, Fallible]
)(using Quotes): Configuration[Nothing] | plan.type =
sourceStruct.fields.get(field).match {
case Some(struct) if struct.tpe.repr <:< plan.dest.tpe.repr =>
Expand All @@ -237,7 +242,7 @@ private[ducktape] object ConfigParser {
}

private object DeprecatedConfig {
def unapply(using Quotes)(term: quotes.reflect.Term) = {
def unapply(using Quotes, Context)(term: quotes.reflect.Term) = {
import quotes.reflect.*

PartialFunction.condOpt(term.asExpr):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ private[ducktape] object Configuration {
given debug: Debug[Configuration[Fallible]] = Debug.derived

trait ErrorModifier {
def apply(parent: Plan[Plan.Error, Fallible] | None.type, plan: Plan.Error)(using Quotes): Configuration[Nothing] | plan.type
def apply(parent: Plan[Erroneous, Fallible] | None.type, plan: Plan.Error)(using Quotes): Configuration[Nothing] | plan.type
}

object ErrorModifier {
given Debug[ErrorModifier] = Debug.nonShowable

val substituteOptionsWithNone = new ErrorModifier:
def apply(parent: Plan[Plan.Error, Fallible] | None.type, plan: Plan.Error)(using
def apply(parent: Plan[Erroneous, Fallible] | None.type, plan: Plan.Error)(using
Quotes
): Configuration[Nothing] | plan.type =
plan.dest.tpe match {
Expand All @@ -37,7 +37,7 @@ private[ducktape] object Configuration {
}

val substituteWithDefaults = new ErrorModifier:
def apply(parent: Plan[Plan.Error, Fallible] | None.type, plan: Plan.Error)(using
def apply(parent: Plan[Erroneous, Fallible] | None.type, plan: Plan.Error)(using
Quotes
): Configuration[Nothing] | plan.type =
PartialFunction
Expand All @@ -61,10 +61,10 @@ private[ducktape] object Configuration {

trait FieldModifier {
def apply(
parent: Plan.BetweenProductFunction[Plan.Error, Fallible] | Plan.BetweenProducts[Plan.Error, Fallible] |
Plan.BetweenTupleProduct[Plan.Error, Fallible],
parent: Plan.BetweenProductFunction[Erroneous, Fallible] | Plan.BetweenProducts[Erroneous, Fallible] |
Plan.BetweenTupleProduct[Erroneous, Fallible],
field: String,
plan: Plan[Plan.Error, Fallible]
plan: Plan[Erroneous, Fallible]
)(using Quotes): Configuration[Nothing] | plan.type
}

Expand All @@ -82,9 +82,9 @@ private[ducktape] object Configuration {
case Dynamic(
path: Path,
side: Side,
config: Plan[Plan.Error, Fallible] | None.type => Either[String, Configuration[F]],
config: Plan[Erroneous, Fallible] | None.type => Either[String, Configuration[Nothing]],
span: Span
) extends Instruction[F]
) extends Instruction[Nothing]

case Bulk(
path: Path,
Expand All @@ -110,7 +110,7 @@ private[ducktape] object Configuration {
def parse[G[+x], A: Type, B: Type, F <: Fallible](
configs: Expr[Seq[Field.Fallible[G, A, B] | Case.Fallible[G, A, B]]],
parsers: NonEmptyList[ConfigParser[F]]
)(using Quotes): List[Instruction[F]] = {
)(using Quotes, Context): List[Instruction[F]] = {
import quotes.reflect.*
def fallback(term: quotes.reflect.Term) =
Configuration.Instruction.Failed(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package io.github.arainko.ducktape.internal

sealed trait Context {
type F <: Fallible

def summoner: Summoner[F]
def transformationSite: TransformationSite

final def reify[FF <: Fallible, G[x]](value: G[FF])(using ev: G[FF] =:= G[F]): G[F] = ev(value)

final def toTotal: Context.Total = Context.Total(transformationSite)
}

object Context {
type Of[F0 <: Fallible] = Context { type F = F0 }

inline def current(using ctx: Context): ctx.type = ctx

case class PossiblyFallible[G[+x]](
wrapperType: WrapperType.Wrapped[G],
transformationSite: TransformationSite,
summoner: Summoner.PossiblyFallible[G],
mode: TransformationMode[G]
) extends Context {
final type F = Fallible
}

case class Total(
transformationSite: TransformationSite
) extends Context {
final type F = Nothing

val summoner: Summoner[Nothing] = Summoner.Total
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
package io.github.arainko.ducktape.internal

private[ducktape] object PlanRefiner {
private[ducktape] object ErroneousnessRefiner {
private object ErrorCollector extends PlanTraverser[List[Plan.Error]] {
protected def foldOver(plan: Plan[Plan.Error, Fallible], accumulator: List[Plan.Error]): List[Plan.Error] =
protected def foldOver(plan: Plan[Erroneous, Fallible], accumulator: List[Plan.Error]): List[Plan.Error] =
plan match {
case error: Plan.Error => error :: accumulator
case other => accumulator
}
}

def run[F <: Fallible](plan: Plan[Plan.Error, F]): Either[NonEmptyList[Plan.Error], Plan[Nothing, F]] = {
def run[F <: Fallible](plan: Plan[Erroneous, F]): Either[NonEmptyList[Plan.Error], Plan[Nothing, F]] = {
// if no errors were accumulated that means there are no Plan.Error nodes which means we operate on a Plan[Nothing]
NonEmptyList
.fromList(ErrorCollector.run(plan, Nil))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,6 @@ private[ducktape] object ErrorMessage {
val side: Side = Side.Dest
}

final case class UndeterminedTransformationMode(span: Span) extends ErrorMessage {
def render(using Quotes): String =
"Couldn't determine the transformation mode, make sure an instance of either Mode.FailFast[F] or Mode.Accumulating[F] is in implicit scope"
val side: Side = Side.Dest
}

final case class SourceConfigDoesntEndWithCaseSegment(span: Span) extends ErrorMessage {
def render(using Quotes): String =
"Case config's path should always end with an `.at` segment"
Expand All @@ -117,4 +111,10 @@ private[ducktape] object ErrorMessage {
val side = Side.Dest
}

final case class FallibleConfigNotPermitted(span: Span, side: Side) extends ErrorMessage {
def render(using Quotes): String =
"""Fallible configuration is not supported for F-unwrapped transformations with Mode.Accumulating.
|You can make this work if you supply a deprioritized instance of Mode.FailFast for the same wrapper type.""".stripMargin
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import scala.util.boundary
import scala.util.boundary.Label

private[ducktape] object FallibilityRefiner {
def run[E <: Plan.Error](plan: Plan[E, Fallible]): Plan[E, Nothing] | None.type =
def run[E <: Erroneous](plan: Plan[E, Fallible]): Plan[E, Nothing] | None.type =
recurse(plan) match
case None => None
case b: Unit => plan.asInstanceOf[Plan[E, Nothing]]

private def recurse[E <: Plan.Error](plan: Plan[E, Fallible]): None.type | Unit =
private def recurse[E <: Erroneous](plan: Plan[E, Fallible]): None.type | Unit =
boundary[None.type | Unit]:
plan match
case Upcast(source, dest) => ()
Expand Down Expand Up @@ -74,9 +74,15 @@ private[ducktape] object FallibilityRefiner {
case BetweenCollections(source, dest, plan) =>
recurse(plan)

case BetweenFallibleNonFallible(source, dest, plan) =>
boundary.break(None)

case BetweenFallibles(_, _, _, _) =>
boundary.break(None)

case Plan.Error(source, dest, message, suppressed) => ()

private inline def evaluate(plans: Iterable[Plan[Plan.Error, Fallible]])(using inline label: boundary.Label[None.type | Unit]) =
private inline def evaluate(plans: Iterable[Plan[Erroneous, Fallible]])(using inline label: boundary.Label[None.type | Unit]) =
val iterator = plans.iterator
while iterator.hasNext do
recurse(iterator.next()) match {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package io.github.arainko.ducktape.internal

import io.github.arainko.ducktape.Transformer
import io.github.arainko.ducktape.internal.Summoner.UserDefined.{ FallibleTransformer, TotalTransformer }
import io.github.arainko.ducktape.{ Mode, Transformer }

import scala.collection.Factory
import scala.collection.immutable.VectorMap
Expand Down Expand Up @@ -69,6 +69,28 @@ private[ducktape] object FalliblePlanInterpreter {
case plan @ Plan.BetweenTuples(source, dest, plans) =>
fromTupleTransformation(source, plan, plans, value, F)(ProductConstructor.Tuple)

case plan @ Plan.BetweenFallibleNonFallible(source, dest, elemPlan) =>
(source.underlying.tpe, dest.tpe) match {
case '[src] -> '[dest] =>
val src = value.asExprOf[F[src]]
Value.Wrapped('{ ${ F.value }.map($src, a => ${ PlanInterpreter.recurse(elemPlan, 'a).asExprOf[dest] }) })
}

case plan @ Plan.BetweenFallibles(source, dest, mode, elemPlan) =>
FallibilityRefiner.run(elemPlan) match {
case plan: Plan[Nothing, Nothing] =>
val simplified = Plan.BetweenFallibleNonFallible(source, dest, plan)
recurse(simplified, value, F)
case None =>
(source.underlying.tpe, dest.tpe) match {
case '[src] -> '[dest] =>
val localMode = mode.value.asExprOf[Mode.FailFast[F]]
val src = value.asExprOf[F[src]]

Value.Wrapped('{ $localMode.flatMap($src, a => ${ recurse(elemPlan, 'a, F).wrapped(F, Type.of[dest]) }) })
}
}

case Plan.BetweenCoproducts(source, dest, casePlans) =>
dest.tpe match {
case '[destSupertype] =>
Expand Down
Loading