Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
37a8d31
Add Mirror-based derivation for ToExpr and FromExpr
halotukozak Jul 13, 2026
5616c97
Add nested sum derivation for Vehicle and Arith with corresponding tests
halotukozak Jul 14, 2026
398a533
Auto-derive a sum's own case instances
halotukozak Jul 14, 2026
bb1f44f
cleanup comments
halotukozak Jul 14, 2026
9f3356a
use lazy val for elemInstances in FromExpr and ToExpr derivations to …
halotukozak Jul 14, 2026
9331e0f
Add FromExprFactory and ToExprFactory for type constructors
halotukozak Jul 14, 2026
8a8136c
revert copy-pasted derivation mechanisms
halotukozak Jul 14, 2026
a01e97c
Fix ToExprFactory.derivedSum crash on case-object sum cases
halotukozak Jul 14, 2026
04d8fea
Remove ToExpr.derived and FromExpr.derived in favor of ToExprFactory/…
halotukozak Jul 14, 2026
c18b902
Fix enum-case dispatch and singleton priority in ToExprFactory/FromEx…
halotukozak Jul 14, 2026
8264cd4
Add deferred-Type container/tuple instances to ToExprFactory/FromExpr…
halotukozak Jul 15, 2026
05a51dc
Simplify ToExprFactory/FromExprFactory: drop redundant ToExpr/FromExp…
halotukozak Jul 15, 2026
a22884e
Name the FromExprFactory/ToExprFactory proxy params; proxy Tuple1..22…
halotukozak Jul 15, 2026
b9dd49f
Restore pre-existing ToExpr/FromExpr given signatures for binary comp…
halotukozak Jul 16, 2026
68ced9e
fix derivedSum in ToExprFactory
halotukozak Aug 12, 2026
8caec3d
fix derivedSum in FromExprFactory
halotukozak Aug 13, 2026
03c9654
Demote tupleConsToExprFactory to a low-priority fallback
halotukozak Aug 13, 2026
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
388 changes: 161 additions & 227 deletions library/src/scala/quoted/FromExpr.scala

Large diffs are not rendered by default.

704 changes: 704 additions & 0 deletions library/src/scala/quoted/FromExprFactory.scala

Large diffs are not rendered by default.

353 changes: 178 additions & 175 deletions library/src/scala/quoted/ToExpr.scala

Large diffs are not rendered by default.

580 changes: 580 additions & 0 deletions library/src/scala/quoted/ToExprFactory.scala

Large diffs are not rendered by default.

304 changes: 304 additions & 0 deletions tests/run-macros/quoted-FromExpr-mirror-derivation/Macro_1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,304 @@
import scala.quoted.*

// Plain product, primitives
case class Point(x: Int, y: Int) derives ToExprFactory, FromExprFactory

// Product, mixed field kinds
case class Mixed(name: String, flag: Boolean, ratio: Double) derives ToExprFactory, FromExprFactory

// Same shape, different types (wrong-mirror guard)
case class Meters(value: Int) derives ToExprFactory, FromExprFactory
case class Seconds(value: Int) derives ToExprFactory, FromExprFactory

// `derived` on a tuple type directly
given tuple2FromExpr(using Quotes): FromExpr[(Int, String)] = FromExprFactory.derived[(Int, String)].apply()
given tuple2ToExpr(using Quotes): ToExpr[(Int, String)] = ToExprFactory.derived[(Int, String)].apply()
given tuple5FromExpr(using Quotes): FromExpr[(Int, String, Boolean, Double, Char)] = FromExprFactory.derived[(Int, String, Boolean, Double, Char)].apply()
given tuple5ToExpr(using Quotes): ToExpr[(Int, String, Boolean, Double, Char)] = ToExprFactory.derived[(Int, String, Boolean, Double, Char)].apply()

// Nested products, cross-object
case class Address(city: String, zip: Int) derives FromExprFactory
object Namespace:
case class Person(name: String, address: Address) derives FromExprFactory

// Three levels of nesting
case class Company(hq: Address, ceo: Namespace.Person) derives FromExprFactory

// Singleton (case object)
case object Singleton derives ToExprFactory, FromExprFactory

// Generic product (needs an explicit Type/instance bound)
case class Box[A](value: A) derives FromExprFactory, ToExprFactory

// Container-typed fields, now handled by dedicated container Factory instances
case class Tagged(tags: List[Int]) derives ToExprFactory, FromExprFactory
case class WithOpt(o: Option[String]) derives ToExprFactory, FromExprFactory
case class WithEither(e: Either[Int, String]) derives ToExprFactory, FromExprFactory
case class WithMap(m: Map[String, Int]) derives ToExprFactory, FromExprFactory
case class WithSet(s: Set[Long]) derives ToExprFactory, FromExprFactory
case class WithArray(a: Array[Int]) derives ToExprFactory, FromExprFactory
case class WithTuple(a: (Int, List[String])) derives ToExprFactory, FromExprFactory

// Sum: sealed trait + cases, auto-derived
sealed trait Shape derives ToExprFactory, FromExprFactory
object Shape:
case class Circle(r: Double) extends Shape
case class Rect(w: Double, h: Double) extends Shape
case object Origin extends Shape

// Sum: enum, singleton + parameterized cases
enum Color derives ToExprFactory, FromExprFactory:
case Red, Green
case Custom(hex: String)

// Product with a sum-typed field
case class Container(name: String, shape: Shape) derives ToExprFactory, FromExprFactory

// Zero-arg product (not a singleton)
case class Blank() derives ToExprFactory, FromExprFactory

// Generic sum (explicit bound on the trait; cases auto-derived)
sealed trait Result[+A] derives FromExprFactory, ToExprFactory
object Result:
case class Ok[A](value: A) extends Result[A]
case object Fail extends Result[Nothing]

// Nested sum hierarchy, two levels of dispatch
sealed trait Vehicle derives ToExprFactory, FromExprFactory
object Vehicle:
sealed trait Motorized extends Vehicle
object Motorized:
case class Car(wheels: Int) extends Motorized
case class Motorcycle(cc: Int) extends Motorized
case class Bicycle(gears: Int) extends Vehicle

// Recursive sum: a case's own field is the enclosing type
enum Arith derives ToExprFactory, FromExprFactory:
case Lit(n: Int)
case Add(l: Arith, r: Arith)
case Neg(e: Arith)

// Arity > 22 (known limitation, not a crash)
case class Big25(
f1: Int, f2: Int, f3: Int, f4: Int, f5: Int, f6: Int, f7: Int, f8: Int, f9: Int, f10: Int,
f11: Int, f12: Int, f13: Int, f14: Int, f15: Int, f16: Int, f17: Int, f18: Int, f19: Int, f20: Int,
f21: Int, f22: Int, f23: Int, f24: Int, f25: Int
) derives ToExprFactory, FromExprFactory

object Macro:
// Constructor-call recognition: `apply` and `new`
inline def matchApplyPoint: Boolean = ${ Macro.matchApplyPointImpl }
inline def matchNewPoint: Boolean = ${ Macro.matchNewPointImpl }
inline def matchNegativePoint: Boolean = ${ Macro.matchNegativePointImpl }
inline def matchMixed: Boolean = ${ Macro.matchMixedImpl }
inline def matchNestedPerson: Boolean = ${ Macro.matchNestedPersonImpl }
inline def matchDeepCompany: Boolean = ${ Macro.matchDeepCompanyImpl }
inline def matchSingleton: Boolean = ${ Macro.matchSingletonImpl }
inline def matchBoxInt: Boolean = ${ Macro.matchBoxIntImpl }
inline def matchBoxBox: Boolean = ${ Macro.matchBoxBoxImpl }
inline def matchShapeCircle: Boolean = ${ Macro.matchShapeCircleImpl }
inline def matchShapeRect: Boolean = ${ Macro.matchShapeRectImpl }
inline def matchShapeOrigin: Boolean = ${ Macro.matchShapeOriginImpl }
inline def matchColorRed: Boolean = ${ Macro.matchColorRedImpl }
inline def matchColorCustom: Boolean = ${ Macro.matchColorCustomImpl }
inline def matchContainer: Boolean = ${ Macro.matchContainerImpl }
inline def matchBlankApply: Boolean = ${ Macro.matchBlankApplyImpl }
inline def matchBlankNew: Boolean = ${ Macro.matchBlankNewImpl }
inline def matchResultOk: Boolean = ${ Macro.matchResultOkImpl }
inline def matchResultFail: Boolean = ${ Macro.matchResultFailImpl }

inline def matchVehicleCar: Boolean = ${ Macro.matchVehicleCarImpl }
inline def matchVehicleMotorcycle: Boolean = ${ Macro.matchVehicleMotorcycleImpl }
inline def matchVehicleBicycle: Boolean = ${ Macro.matchVehicleBicycleImpl }
// Nested-sum wrong-variant guard
inline def wrongNestedVariantIsNone: Boolean = ${ Macro.wrongNestedVariantIsNoneImpl }

// Recursive ADT, several levels deep
inline def matchArithNested: Boolean = ${ Macro.matchArithNestedImpl }

// `Typed` node unwrapping
inline def matchAscribedPoint: Boolean = ${ Macro.matchAscribedPointImpl }

// `Block(Nil, ...)` node unwrapping
inline def matchBracedPoint: Boolean = ${ Macro.matchBracedPointImpl }

// Not a constructor call
inline def nonMatchingIsNone: Boolean = ${ Macro.nonMatchingIsNoneImpl }
// Same arity, wrong type
inline def wrongTypeIsNone: Boolean = ${ Macro.wrongTypeIsNoneImpl }
// Wrong sum variant
inline def wrongSumVariantIsNone: Boolean = ${ Macro.wrongSumVariantIsNoneImpl }
// Wrong arity
inline def wrongArityIsNone: Boolean = ${ Macro.wrongArityIsNoneImpl }
// Arity > 22, documented limitation
inline def roundTripBig25IsNone: Boolean = ${ Macro.roundTripBig25IsNoneImpl }
// Wrong mirror, same arity/field type
inline def wrongMirrorIsNone: Boolean = ${ Macro.wrongMirrorIsNoneImpl }

// `derived` on a tuple type directly
inline def matchTuple2: Boolean = ${ Macro.matchTuple2Impl }
inline def roundTripTuple5: Boolean = ${ Macro.roundTripTuple5Impl }

// ToExpr -> FromExpr round trip, all cases above
inline def roundTrips: Boolean = ${ Macro.roundTripsImpl }

// Container-typed fields, round trip without any ambient Quotes given
inline def roundTripsContainers: Boolean = ${ Macro.roundTripsContainersImpl }

// Direct coverage of tuple1ToExprFactory..tuple22ToExprFactory / tuple1FromExprFactory..tuple22FromExprFactory
inline def roundTripsTupleFactories: Boolean = ${ Macro.roundTripsTupleFactoriesImpl }

private def check[T: {Type, FromExpr as fe}](expr: Expr[T], expected: T)(using Quotes): Expr[Boolean] =
Expr(fe.unapply(expr) == Some(expected))

private def checkNone[T: {Type, FromExpr as fe}](expr: Expr[T])(using Quotes): Expr[Boolean] =
Expr(fe.unapply(expr).isEmpty)

def matchApplyPointImpl(using Quotes): Expr[Boolean] = check('{ Point(1, 2) }, Point(1, 2))
def matchNewPointImpl(using Quotes): Expr[Boolean] = check('{ new Point(1, 2) }, Point(1, 2))
def matchNegativePointImpl(using Quotes): Expr[Boolean] = check('{ Point(-1, -2) }, Point(-1, -2))
def matchMixedImpl(using Quotes): Expr[Boolean] = check('{ Mixed("m", true, 1.5) }, Mixed("m", true, 1.5))

def matchNestedPersonImpl(using Quotes): Expr[Boolean] =
check('{ Namespace.Person("nyc", Address("nyc", 10001)) }, Namespace.Person("nyc", Address("nyc", 10001)))

def matchDeepCompanyImpl(using Quotes): Expr[Boolean] =
check(
'{ Company(Address("sf", 94107), Namespace.Person("ana", Address("nyc", 10001))) },
Company(Address("sf", 94107), Namespace.Person("ana", Address("nyc", 10001)))
)

def matchSingletonImpl(using Quotes): Expr[Boolean] = check('{ Singleton }, Singleton)
def matchBoxIntImpl(using Quotes): Expr[Boolean] = check('{ Box(1) }, Box(1))
def matchBoxBoxImpl(using Quotes): Expr[Boolean] = check('{ Box(Box("x")) }, Box(Box("x")))
def matchShapeCircleImpl(using Quotes): Expr[Boolean] = check[Shape]('{ Shape.Circle(1.0) }, Shape.Circle(1.0))
def matchShapeRectImpl(using Quotes): Expr[Boolean] = check[Shape]('{ Shape.Rect(1.0, 2.0) }, Shape.Rect(1.0, 2.0))
def matchShapeOriginImpl(using Quotes): Expr[Boolean] = check[Shape]('{ Shape.Origin }, Shape.Origin)
def matchColorRedImpl(using Quotes): Expr[Boolean] = check[Color]('{ Color.Red }, Color.Red)
def matchColorCustomImpl(using Quotes): Expr[Boolean] = check[Color]('{ Color.Custom("#fff") }, Color.Custom("#fff"))

def matchContainerImpl(using Quotes): Expr[Boolean] =
check('{ Container("c", Shape.Circle(3.0)) }, Container("c", Shape.Circle(3.0)))

def matchBlankApplyImpl(using Quotes): Expr[Boolean] = check('{ Blank() }, Blank())
def matchBlankNewImpl(using Quotes): Expr[Boolean] = check('{ new Blank() }, Blank())
def matchResultOkImpl(using Quotes): Expr[Boolean] = check[Result[Int]]('{ Result.Ok(5) }, Result.Ok(5))
def matchResultFailImpl(using Quotes): Expr[Boolean] = check[Result[Int]]('{ Result.Fail }, Result.Fail)

def matchVehicleCarImpl(using Quotes): Expr[Boolean] =
check[Vehicle]('{ Vehicle.Motorized.Car(4) }, Vehicle.Motorized.Car(4))
def matchVehicleMotorcycleImpl(using Quotes): Expr[Boolean] =
check[Vehicle]('{ Vehicle.Motorized.Motorcycle(650) }, Vehicle.Motorized.Motorcycle(650))
def matchVehicleBicycleImpl(using Quotes): Expr[Boolean] =
check[Vehicle]('{ Vehicle.Bicycle(21) }, Vehicle.Bicycle(21))

def wrongNestedVariantIsNoneImpl(using Quotes): Expr[Boolean] =
given FromExpr[Vehicle.Motorized.Motorcycle] = FromExprFactory.derived[Vehicle.Motorized.Motorcycle].apply()
val e: Expr[Vehicle.Motorized.Car] = '{ Vehicle.Motorized.Car(4) }
checkNone(e.asInstanceOf[Expr[Vehicle.Motorized.Motorcycle]])

def matchArithNestedImpl(using Quotes): Expr[Boolean] =
check[Arith](
'{ Arith.Add(Arith.Lit(1), Arith.Neg(Arith.Add(Arith.Lit(2), Arith.Lit(3)))) },
Arith.Add(Arith.Lit(1), Arith.Neg(Arith.Add(Arith.Lit(2), Arith.Lit(3))))
)

def matchAscribedPointImpl(using Quotes): Expr[Boolean] = check('{ (Point(1, 2): Point) }, Point(1, 2))
def matchBracedPointImpl(using Quotes): Expr[Boolean] = check('{ { Point(1, 2) } }, Point(1, 2))

def nonMatchingIsNoneImpl(using Quotes): Expr[Boolean] =
checkNone('{ if true then Point(1, 2) else Point(3, 4) })

def wrongTypeIsNoneImpl(using Quotes): Expr[Boolean] =
val e: Expr[Point] = '{ Point(1, 2) }
checkNone(e.asInstanceOf[Expr[Address]])

def wrongSumVariantIsNoneImpl(using Quotes): Expr[Boolean] =
given FromExpr[Shape.Rect] = FromExprFactory.derived[Shape.Rect].apply()
val e: Expr[Shape.Circle] = '{ Shape.Circle(1.0) }
checkNone(e.asInstanceOf[Expr[Shape.Rect]])

def wrongArityIsNoneImpl(using Quotes): Expr[Boolean] =
checkNone(Expr(Point(1, 2)).asInstanceOf[Expr[Mixed]])

def roundTripBig25IsNoneImpl(using Quotes): Expr[Boolean] =
val original = Big25(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25)
checkNone(Expr(original))

def wrongMirrorIsNoneImpl(using Quotes): Expr[Boolean] =
val e: Expr[Meters] = Expr(Meters(5))
checkNone(e.asInstanceOf[Expr[Seconds]])

def matchTuple2Impl(using Quotes): Expr[Boolean] =
Expr(tuple2FromExpr.unapply('{ (1, "x") }) == Some((1, "x")))

def roundTripTuple5Impl(using Quotes): Expr[Boolean] =
val original = (1, "x", true, 2.5, 'c')
Expr(tuple5FromExpr.unapply(tuple5ToExpr(original)) == Some(original))

def roundTripsImpl(using Quotes): Expr[Boolean] =
def roundTrip[T: {Type, ToExpr, FromExpr as fe}](original: T): Boolean =
fe.unapply(Expr(original)) == Some(original)
Expr(
roundTrip(Point(1, 2))
&& roundTrip(Mixed("m", true, 1.5))
&& roundTrip(Singleton)
&& roundTrip(Blank())
&& roundTrip[Shape](Shape.Circle(2.0))
&& roundTrip[Shape](Shape.Origin)
&& roundTrip(Container("c", Shape.Rect(1.0, 2.0)))
&& roundTrip(Box(7))
&& roundTrip[Result[Int]](Result.Ok(5))
&& roundTrip[Result[Int]](Result.Fail)
&& roundTrip[Color](Color.Red)
&& roundTrip[Color](Color.Custom("#fff"))
&& roundTrip[Vehicle](Vehicle.Motorized.Car(4))
&& roundTrip[Vehicle](Vehicle.Bicycle(21))
&& roundTrip(Arith.Add(Arith.Lit(1), Arith.Neg(Arith.Add(Arith.Lit(2), Arith.Lit(3)))))
)

def roundTripsContainersImpl(using Quotes): Expr[Boolean] =
def roundTrip[T: {Type, ToExpr, FromExpr as fe}](original: T): Boolean =
fe.unapply(Expr(original)) == Some(original)
val arrayBack = summon[FromExpr[WithArray]].unapply(Expr(WithArray(Array(1, 2, 3))))
Expr(
roundTrip(Tagged(List(1, 2, 3)))
&& roundTrip(WithOpt(Some("hi")))
&& roundTrip(WithOpt(None))
&& roundTrip(WithEither(Left(5): Either[Int, String]))
&& roundTrip(WithEither(Right("r"): Either[Int, String]))
&& roundTrip(WithMap(Map("a" -> 1, "b" -> 2)))
&& roundTrip(WithSet(Set(1L, 2L)))
&& roundTrip(WithTuple((1, List("a", "b"))))
&& arrayBack.exists(_.a.sameElements(Array(1, 2, 3)))
)

def roundTripsTupleFactoriesImpl(using Quotes): Expr[Boolean] =
def roundTripFactory[T: {Type, ToExprFactory as tef, FromExprFactory as fef}](original: T): Boolean =
val te = tef.apply()
val fe = fef.apply()
fe.unapply(te.apply(original)) == Some(original)
Expr(
roundTripFactory(Tuple1(1))
&& roundTripFactory((1, 2))
&& roundTripFactory((1, 2, 3))
&& roundTripFactory((1, 2, 3, 4))
&& roundTripFactory((1, 2, 3, 4, 5))
&& roundTripFactory((1, 2, 3, 4, 5, 6))
&& roundTripFactory((1, 2, 3, 4, 5, 6, 7))
&& roundTripFactory((1, 2, 3, 4, 5, 6, 7, 8))
&& roundTripFactory((1, 2, 3, 4, 5, 6, 7, 8, 9))
&& roundTripFactory((1, 2, 3, 4, 5, 6, 7, 8, 9, 10))
&& roundTripFactory((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11))
&& roundTripFactory((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12))
&& roundTripFactory((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13))
&& roundTripFactory((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14))
&& roundTripFactory((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15))
&& roundTripFactory((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16))
&& roundTripFactory((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17))
&& roundTripFactory((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18))
&& roundTripFactory((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19))
&& roundTripFactory((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20))
&& roundTripFactory((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21))
&& roundTripFactory((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22))
)
39 changes: 39 additions & 0 deletions tests/run-macros/quoted-FromExpr-mirror-derivation/Test_2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
object Test:
def main(args: Array[String]): Unit =
assert(Macro.matchApplyPoint, "matchApplyPoint")
assert(Macro.matchNewPoint, "matchNewPoint")
assert(Macro.matchNegativePoint, "matchNegativePoint")
assert(Macro.matchMixed, "matchMixed")
assert(Macro.matchNestedPerson, "matchNestedPerson")
assert(Macro.matchDeepCompany, "matchDeepCompany")
assert(Macro.matchSingleton, "matchSingleton")
assert(Macro.matchBoxInt, "matchBoxInt")
assert(Macro.matchBoxBox, "matchBoxBox")
assert(Macro.matchShapeCircle, "matchShapeCircle")
assert(Macro.matchShapeRect, "matchShapeRect")
assert(Macro.matchShapeOrigin, "matchShapeOrigin")
assert(Macro.matchColorRed, "matchColorRed")
assert(Macro.matchColorCustom, "matchColorCustom")
assert(Macro.matchContainer, "matchContainer")
assert(Macro.matchBlankApply, "matchBlankApply")
assert(Macro.matchBlankNew, "matchBlankNew")
assert(Macro.matchResultOk, "matchResultOk")
assert(Macro.matchResultFail, "matchResultFail")
assert(Macro.matchVehicleCar, "matchVehicleCar")
assert(Macro.matchVehicleMotorcycle, "matchVehicleMotorcycle")
assert(Macro.matchVehicleBicycle, "matchVehicleBicycle")
assert(Macro.wrongNestedVariantIsNone, "wrongNestedVariantIsNone")
assert(Macro.matchArithNested, "matchArithNested")
assert(Macro.matchAscribedPoint, "matchAscribedPoint")
assert(Macro.matchBracedPoint, "matchBracedPoint")
assert(Macro.nonMatchingIsNone, "nonMatchingIsNone")
assert(Macro.wrongTypeIsNone, "wrongTypeIsNone")
assert(Macro.wrongSumVariantIsNone, "wrongSumVariantIsNone")
assert(Macro.wrongArityIsNone, "wrongArityIsNone")
assert(Macro.roundTripBig25IsNone, "roundTripBig25IsNone")
assert(Macro.wrongMirrorIsNone, "wrongMirrorIsNone")
assert(Macro.matchTuple2, "matchTuple2")
assert(Macro.roundTripTuple5, "roundTripTuple5")
assert(Macro.roundTrips, "roundTrips")
assert(Macro.roundTripsContainers, "roundTripsContainers")
assert(Macro.roundTripsTupleFactories, "roundTripsTupleFactories")
Loading
Loading