Skip to content

Commit

Permalink
Merge pull request #67 from tarao/append-by-refinement
Browse files Browse the repository at this point in the history
Accept both refinement type and tuple type in  `Append`
  • Loading branch information
tarao authored Feb 6, 2024
2 parents e97cd37 + 85d39bb commit 31a7cf1
Show file tree
Hide file tree
Showing 7 changed files with 241 additions and 171 deletions.
2 changes: 1 addition & 1 deletion docs/advanced/generic.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ object Person {
domain: String,
localPart: String = p.firstName,
)(using
Append.Aux[R & Tag[Person], ("email", String) *: EmptyTuple, RR],
Append.Aux[R & Tag[Person], % { val email: String }, RR],
): RR = p + (email = s"${localPart}@${domain}")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,28 +55,43 @@ object ArrayRecordMacros {

requireApply(record, method) {
val rec = '{ ${ record }.__fields }
val (fields, tpe) = extractFieldsFrom(args)
val (fields, schema) = extractFieldsFrom(args)
val vec = '{ ${ rec }.toVector }

tpe match {
case '[tpe] =>
val concat = evidenceOf[Concat[R, tpe]]
concat match {
case '{ ${ _ }: Concat[R, tpe] { type Out = returnType } } =>
concat match {
// We have to do `match` independently because `NeedDedup` is
// not necessarily supplied
case '{ ${ _ }: Concat[R, tpe] { type NeedDedup = false } } =>
'{ newArrayRecord[returnType](${ vec }.concat(${ fields })) }
case _ =>
'{
newArrayRecord[returnType](
unsafeConcat(${ vec }, ${ fields }),
)
}
}
def tryFieldTypes(tpe: Type[?]): Option[Expr[Any]] =
tpe match {
case '[tpe] =>
Expr.summon[Concat[R, tpe]].map {
case concat @ '{
${ _ }: Concat[R, tpe] { type Out = returnType }
} =>
concat match {
// We have to do `match` independently because `NeedDedup` is
// not necessarily supplied
case '{ ${ _ }: Concat[R, tpe] { type NeedDedup = false } } =>
'{
newArrayRecord[returnType](${ vec }.concat(${ fields }))
}
case _ =>
'{
newArrayRecord[returnType](
unsafeConcat(${ vec }, ${ fields }),
)
}
}
}
}

tryFieldTypes(schema.asTupleType)
.orElse(tryFieldTypes(schema.asType))
.getOrElse {
schema.asTupleType match {
case '[tpe] =>
errorAndAbort(
s"No given instance of ${Type.show[Concat[R, tpe]]}",
)
}
}
}
}
}

Expand Down Expand Up @@ -143,6 +158,9 @@ object ArrayRecordMacros {
): Expr[Concat[R1, R2]] = withTyping {
import internal.*

requireConcreteType[R1]
requireConcreteType[R2]

var deduped = false

val result = catching {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ private[record4s] class InternalMacros(using

case class Schema private[InternalMacros] (
private[InternalMacros] val fieldTypes: Seq[(String, TypeRepr)],
tags: Seq[Type[?]],
tags: Seq[Type[?]] = Seq.empty,
) {
def size: Int = fieldTypes.size

Expand Down Expand Up @@ -336,6 +336,33 @@ private[record4s] class InternalMacros(using
traverse1(List(safeDealias(fixupOpaqueAlias(TypeRepr.of[R]))), acc)
}

def freeTypeVariables[T: Type]: List[Type[?]] =
traverse[T, List[Type[?]]](
List.empty,
(acc: List[Type[?]], tpe: Type[?]) => {
tpe match {
case '[t] if TypeRepr.of[t].typeSymbol.isTypeParam =>
tpe :: acc
case _ =>
acc
}
},
)

def requireConcreteType[T: Type]: Unit = {
val vs = freeTypeVariables[T]
if (vs.nonEmpty)
vs.head match {
case '[tpe] =>
errorAndAbort(
Seq(
s"A concrete type expected but type variable ${Type.show[tpe]} is given.",
"Did you forget to make the method inline?",
).mkString("\n"),
)
}
}

def schemaOfRecord[R: Type]: Schema = {
def unapplyTuple2(tpr: TypeRepr): Option[(TypeRepr, TypeRepr)] =
// We can't do
Expand Down Expand Up @@ -462,7 +489,7 @@ private[record4s] class InternalMacros(using

def extractFieldsFrom(
varargs: Expr[Seq[Any]],
): (Expr[Seq[(String, Any)]], Type[?]) = {
): (Expr[Seq[(String, Any)]], Schema) = {
// We have no way to write this without transparent inline macro. Literal string
// types are subject to widening and they become `String`s at the type level. A
// `transparent inline given` also doesn't work since it can only depend on type-level
Expand All @@ -476,24 +503,12 @@ private[record4s] class InternalMacros(using
errorAndAbort("Expected explicit varargs sequence", Some(varargs))
}
val fieldTypes = fieldTypesOf(fields)

val tupledFieldTypes =
fieldTypes.foldRight(Type.of[EmptyTuple]: Type[?]) {
case ((label, _, '[tpe]), base) =>
val pair = ConstantType(StringConstant(label)).asType match {
case '[label] => Type.of[(label, tpe)]
}
(pair, base) match {
case ('[tpe], '[head *: tail]) =>
Type.of[tpe *: head *: tail]
case ('[tpe], '[EmptyTuple]) =>
Type.of[tpe *: EmptyTuple]
}
}

val namedFields =
fieldTypes.map((label, value, _) => Expr.ofTuple((Expr(label), value)))
(Expr.ofSeq(namedFields), tupledFieldTypes)
val schema = Schema(fieldTypes.map { case (label, _, '[tpe]) =>
(label, TypeRepr.of[tpe])
})
(Expr.ofSeq(namedFields), schema)
}

def requireApply[C, T](context: Expr[C], method: Expr[String])(
Expand Down
76 changes: 34 additions & 42 deletions modules/core/src/main/scala/com/github/tarao/record4s/Macros.scala
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,32 @@ object Macros {

requireApply(record, method) {
val rec = '{ ${ record }.__iterable }
val (fields, tpe) = extractFieldsFrom(args)

tpe match {
case '[tpe] =>
evidenceOf[Concat[R, tpe]] match {
case '{
type returnType <: %
${ _ }: Concat[R, tpe] { type Out = `returnType` }
} =>
'{ newMapRecord[returnType](${ rec }.toMap.concat(${ fields })) }
val (fields, schema) = extractFieldsFrom(args)

def tryFieldTypes(tpe: Type[?]): Option[Expr[Any]] =
tpe match {
case '[tpe] =>
Expr.summon[Concat[R, tpe]].map {
case '{
type returnType <: %
${ _ }: Concat[R, tpe] { type Out = `returnType` }
} =>
'{
newMapRecord[returnType](${ rec }.toMap.concat(${ fields }))
}
}
}

tryFieldTypes(schema.asType)
.orElse(tryFieldTypes(schema.asTupleType))
.getOrElse {
schema.asType match {
case '[tpe] =>
errorAndAbort(
s"No given instance of ${Type.show[Concat[R, tpe]]}",
)
}
}
}
}
}

Expand Down Expand Up @@ -82,6 +96,9 @@ object Macros {
): Expr[Concat[R1, R2]] = withTyping {
import internal.*

requireConcreteType[R1]
requireConcreteType[R2]

val result = catching {
val schema1 = schemaOf[R1]
val schema2 = schemaOf[R2]
Expand Down Expand Up @@ -195,40 +212,15 @@ object Macros {
def derivedTypingConcreteImple[T: Type](using
Quotes,
): Expr[Concrete[T]] = withInternal {
import quotes.reflect.*
import internal.*

type Acc = List[Type[?]]
def freeTypeVariables[T: Type]: Acc =
traverse[T, Acc](
List.empty,
(acc: Acc, tpe: Type[?]) => {
tpe match {
case '[t] if TypeRepr.of[t].typeSymbol.isTypeParam =>
tpe :: acc
case _ =>
acc
}
},
)
requireConcreteType[T]

val vs = freeTypeVariables[T]
if (vs.nonEmpty)
vs.head match {
case '[tpe] =>
errorAndAbort(
Seq(
s"A concrete type expected but type variable ${Type.show[tpe]} is given.",
"Did you forget to make the method inline?",
).mkString("\n"),
)
}
else
'{
Concrete
.instance
.asInstanceOf[Concrete[T]]
}
'{
Concrete
.instance
.asInstanceOf[Concrete[T]]
}
}

private def typeNameOfImpl[T: Type](using Quotes): Expr[String] = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ object ArrayRecord {
${ ArrayRecordMacros.derivedTypingConcatImpl }
}

type Append[R1, R2 <: Tuple] = Concat[R1, R2]
type Append[R1, R2] = Concat[R1, R2]

object Append {
type Aux[R1, R2 <: Tuple, Out0 <: ProductRecord] = Concat[R1, R2] {
type Aux[R1, R2, Out0 <: ProductRecord] = Concat[R1, R2] {
type Out = Out0
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ object Record {
${ Macros.derivedTypingConcatImpl }
}

type Append[R1, R2 <: Tuple] = Concat[R1, R2]
type Append[R1, R2] = Concat[R1, R2]

object Append {
type Aux[R1, R2 <: Tuple, Out0 <: %] = Concat[R1, R2] { type Out = Out0 }
type Aux[R1, R2, Out0 <: %] = Concat[R1, R2] { type Out = Out0 }
}

@implicitNotFound("Value '${Label}' is not a member of ${R}")
Expand Down
Loading

0 comments on commit 31a7cf1

Please sign in to comment.