Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix compilation of lenses with context bounds in Scala 3 #1267

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
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 @@ -6,6 +6,10 @@ private[focus] trait ErrorHandling {
def errorMessage(error: FocusError): String = error match {
case FocusError.NotACaseClass(fromClass, fieldName) =>
s"Cannot generate Lens for field '$fieldName', because '$fromClass' is not a case class"
case FocusError.NotACaseField(caseClass, fieldName) =>
s"Can only create lenses for case fields, but '$fieldName' is not a case field of '$caseClass'"
case FocusError.NonImplicitNonCaseParameter(caseClass, parameters) =>
s"Case class '$caseClass' has non-implicit non-case parameters, which is not supported: ${parameters.map("'" + _ + "'").mkString(", ")}"
case FocusError.NotAConcreteClass(fromClass) =>
s"Expecting a concrete case class in the 'From' position; cannot reify type $fromClass"
case FocusError.NotASimpleLambdaFunction =>
Expand All @@ -17,5 +21,9 @@ private[focus] trait ErrorHandling {
case FocusError.UnexpectedCodeStructure(code) => s"Unexpected code structure: $code"
case FocusError.CouldntFindFieldType(fromType, fieldName) => s"Couldn't find type for $fromType.$fieldName"
case FocusError.InvalidDowncast(fromType, toType) => s"Type '$fromType' could not be cast to '$toType'"
case FocusError.ImplicitNotFound(implicitType, explanation) =>
s"Could not find (unique) implicit value for '$implicitType' due to $explanation. Note: implicits with default values are not supported."
case FocusError.ExpansionFailed(reason) =>
s"Case class with multiple parameter sets could not be expanded because of: $reason"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,23 @@ private[focus] trait FocusBase {

given Quotes = macroContext

type Symbol = macroContext.reflect.Symbol
type Term = macroContext.reflect.Term
type TypeRepr = macroContext.reflect.TypeRepr

case class LambdaConfig(argName: String, lambdaBody: Term)

enum FocusAction {
case SelectField(fieldName: String, fromType: TypeRepr, fromTypeArgs: List[TypeRepr], toType: TypeRepr)
case SelectField(caseFieldSymbol: Symbol, fromType: TypeRepr, fromTypeArgs: List[TypeRepr], toType: TypeRepr)
case SelectFieldWithImplicits(caseFieldSymbol: Symbol, fromType: TypeRepr, toType: TypeRepr, setter: Term)
case SelectOnlyField(
fieldName: String,
caseFieldSymbol: Symbol,
fromType: TypeRepr,
fromTypeArgs: List[TypeRepr],
fromCompanion: Term,
toType: TypeRepr
)
case SelectOnlyFieldWithImplicits(caseFieldSymbol: Symbol, fromType: TypeRepr, toType: TypeRepr, reverseGet: Term)
case KeywordSome(toType: TypeRepr)
case KeywordAs(fromType: TypeRepr, toType: TypeRepr)
case KeywordEach(fromType: TypeRepr, toType: TypeRepr, eachInstance: Term)
Expand All @@ -29,10 +32,14 @@ private[focus] trait FocusBase {
case KeywordWithDefault(toType: TypeRepr, defaultValue: Term)

override def toString(): String = this match {
case SelectField(fieldName, fromType, fromTypeArgs, toType) =>
s"SelectField($fieldName, ${fromType.show}, ${fromTypeArgs.map(_.show)}, ${toType.show})"
case SelectOnlyField(fieldName, fromType, fromTypeArgs, _, toType) =>
s"SelectOnlyField($fieldName, ${fromType.show}, ${fromTypeArgs.map(_.show)}, ..., ${toType.show})"
case SelectField(caseFieldSymbol, fromType, fromTypeArgs, toType) =>
s"SelectField(${caseFieldSymbol.name}, ${fromType.show}, ${fromTypeArgs.map(_.show)}, ${toType.show})"
case SelectFieldWithImplicits(caseFieldSymbol, fromType, toType, setter) =>
s"SelectFieldWithImplicits(${caseFieldSymbol.name}, ${fromType.show}, ${toType.show}, ...)"
case SelectOnlyField(caseFieldSymbol, fromType, fromTypeArgs, _, toType) =>
s"SelectOnlyField(${caseFieldSymbol.name}, ${fromType.show}, ${fromTypeArgs.map(_.show)}, ..., ${toType.show})"
case SelectOnlyFieldWithImplicits(caseFieldSymbol, fromType, toType, reverseGet) =>
s"SelectOnlyFieldWithImplicits(${caseFieldSymbol.name}, ${fromType.show}, ${toType.show}, ...)"
case KeywordSome(toType) => s"KeywordSome(${toType.show})"
case KeywordAs(fromType, toType) => s"KeywordAs(${fromType.show}, ${toType.show})"
case KeywordEach(fromType, toType, _) => s"KeywordEach(${fromType.show}, ${toType.show}, ...)"
Expand All @@ -44,6 +51,8 @@ private[focus] trait FocusBase {

enum FocusError {
case NotACaseClass(className: String, fieldName: String)
case NotACaseField(className: String, fieldName: String)
case NonImplicitNonCaseParameter(className: String, parameters: List[String])
case NotAConcreteClass(className: String)
case DidNotDirectlyAccessArgument(argName: String)
case NotASimpleLambdaFunction
Expand All @@ -52,6 +61,8 @@ private[focus] trait FocusBase {
case CouldntFindFieldType(fromType: String, fieldName: String)
case ComposeMismatch(type1: String, type2: String)
case InvalidDowncast(fromType: String, toType: String)
case ImplicitNotFound(implicitType: String, explanation: String)
case ExpansionFailed(reason: String)

def asResult: FocusResult[Nothing] = Left(this)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package monocle.internal.focus.features

import monocle.internal.focus.FocusBase
import monocle.internal.focus.features.selectfield.SelectFieldGenerator
import monocle.internal.focus.features.selectonlyfield.SelectOnlyFieldGenerator
import monocle.internal.focus.features.some.SomeGenerator
import monocle.internal.focus.features.as.AsGenerator
import monocle.internal.focus.features.each.EachGenerator
Expand All @@ -15,7 +14,6 @@ import scala.quoted.Type
private[focus] trait AllFeatureGenerators
extends FocusBase
with SelectFieldGenerator
with SelectOnlyFieldGenerator
with SomeGenerator
with AsGenerator
with EachGenerator
Expand All @@ -28,24 +26,27 @@ private[focus] trait GeneratorLoop {

import macroContext.reflect._

def generateCode[From: Type](actions: List[FocusAction]): FocusResult[Term] = {
val idOptic: FocusResult[Term] = Right('{ Iso.id[From] }.asTerm)

actions.foldLeft(idOptic) { (resultSoFar, action) =>
resultSoFar.flatMap(term => composeOptics(term, generateActionCode(action)))
def generateCode[From: Type](actions: List[FocusAction]): FocusResult[Term] =
actions match {
case Nil => Right('{ Iso.id[From] }.asTerm)
case head :: tail =>
tail.foldLeft[FocusResult[Term]](Right(generateActionCode(head))) { (resultSoFar, action) =>
resultSoFar.flatMap(term => composeOptics(term, generateActionCode(action)))
}
}
}

private def generateActionCode(action: FocusAction): Term =
action match {
case a: FocusAction.SelectField => generateSelectField(a)
case a: FocusAction.SelectOnlyField => generateSelectOnlyField(a)
case a: FocusAction.KeywordSome => generateSome(a)
case a: FocusAction.KeywordAs => generateAs(a)
case a: FocusAction.KeywordEach => generateEach(a)
case a: FocusAction.KeywordAt => generateAt(a)
case a: FocusAction.KeywordIndex => generateIndex(a)
case a: FocusAction.KeywordWithDefault => generateWithDefault(a)
case a: FocusAction.SelectField => generateSelectField(a)
case a: FocusAction.SelectFieldWithImplicits => generateSelectFieldWithImplicits(a)
case a: FocusAction.SelectOnlyField => generateSelectOnlyField(a)
case a: FocusAction.SelectOnlyFieldWithImplicits => generateSelectOnlyFieldWithImplicits(a)
case a: FocusAction.KeywordSome => generateSome(a)
case a: FocusAction.KeywordAs => generateAs(a)
case a: FocusAction.KeywordEach => generateEach(a)
case a: FocusAction.KeywordAt => generateAt(a)
case a: FocusAction.KeywordIndex => generateIndex(a)
case a: FocusAction.KeywordWithDefault => generateWithDefault(a)
}

private def composeOptics(lens1: Term, lens2: Term): FocusResult[Term] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package monocle.internal.focus.features
import scala.quoted.Type
import monocle.internal.focus.FocusBase
import monocle.internal.focus.features.selectfield.SelectFieldParser
import monocle.internal.focus.features.selectonlyfield.SelectOnlyFieldParser
import monocle.internal.focus.features.some.SomeParser
import monocle.internal.focus.features.as.AsParser
import monocle.internal.focus.features.each.EachParser
Expand All @@ -16,7 +15,6 @@ private[focus] trait AllFeatureParsers
with SelectParserBase
with KeywordParserBase
with SelectFieldParser
with SelectOnlyFieldParser
with SomeParser
with AsParser
with EachParser
Expand Down Expand Up @@ -53,9 +51,6 @@ private[focus] trait ParserLoop {
case KeywordWithDefault(Right(remainingCode, action)) => loop(remainingCode, action :: listSoFar)
case KeywordWithDefault(Left(error)) => Left(error)

case SelectOnlyField(Right(remainingCode, action)) => loop(remainingCode, action :: listSoFar)
case SelectOnlyField(Left(error)) => Left(error)

case SelectField(Right(remainingCode, action)) => loop(remainingCode, action :: listSoFar)
case SelectField(Left(error)) => Left(error)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,52 +1,65 @@
package monocle.internal.focus.features

import monocle.internal.focus.FocusBase
import scala.annotation.tailrec
import scala.util.Failure
import scala.util.Success
import scala.util.Try

private[focus] trait SelectParserBase extends ParserBase {
this: FocusBase =>

import this.macroContext.reflect._

// Match on a term that is an instance of a case class
object CaseClass {
def unapply(term: Term): Option[Term] =
case class CaseClass(typeRepr: TypeRepr, classSymbol: Symbol) {
val typeArgs: List[TypeRepr] = getSuppliedTypeArgs(typeRepr)
val companionObject: Term = Ref(classSymbol.companionModule)

private val (typeParams, caseFieldParams :: otherParams) =
classSymbol.primaryConstructor.paramSymss.span(_.headOption.fold(false)(_.isTypeParam))
val hasOnlyOneCaseField: Boolean = caseFieldParams.length == 1
val hasOnlyOneParameterList: Boolean = otherParams.isEmpty
private val nonCaseNonImplicitParameters: List[Symbol] =
otherParams.flatten.filterNot(symbol => symbol.flags.is(Flags.Implicit) || symbol.flags.is(Flags.Given))
val allOtherParametersAreImplicitResult: FocusResult[Unit] = nonCaseNonImplicitParameters match {
case Nil => Right(())
case list => FocusError.NonImplicitNonCaseParameter(typeRepr.show, list.map(_.name)).asResult
}

def getCaseFieldSymbol(fieldName: String): FocusResult[Symbol] =
classSymbol.caseFields.find(_.name == fieldName) match {
case Some(symbol) => Right(symbol)
case None => FocusError.NotACaseField(typeRepr.show, fieldName).asResult
}
def getCaseFieldType(caseFieldSymbol: Symbol): FocusResult[TypeRepr] =
caseFieldSymbol match {
case FieldType(possiblyTypeArg) => Right(swapWithSuppliedType(typeRepr, possiblyTypeArg))
case _ => FocusError.CouldntFindFieldType(typeRepr.show, caseFieldSymbol.name).asResult
}
}

object CaseClassExtractor {
def unapply(term: Term): Option[CaseClass] =
term.tpe.classSymbol.flatMap { sym =>
Option.when(sym.flags.is(Flags.Case))(term)
Option.when(sym.flags.is(Flags.Case))(CaseClass(getType(term), sym))
}
}

def getSuppliedTypeArgs(fromType: TypeRepr): List[TypeRepr] =
private def getSuppliedTypeArgs(fromType: TypeRepr): List[TypeRepr] =
fromType match {
case AppliedType(_, argTypeReprs) => argTypeReprs
case _ => Nil
}

def getClassSymbol(tpe: TypeRepr): FocusResult[Symbol] = tpe.classSymbol match {
case Some(sym) => Right(sym)
case None => FocusError.NotAConcreteClass(tpe.show).asResult
}

def getFieldType(fromType: TypeRepr, fieldName: String): FocusResult[TypeRepr] = {
// We need to do this to support tuples, because even though they conform as case classes in other respects,
// for some reason their field names (_1, _2, etc) have a space at the end, ie `_1 `.
def getTrimmedFieldSymbol(fromTypeSymbol: Symbol): Symbol =
fromTypeSymbol.memberFields.find(_.name.trim == fieldName).getOrElse(Symbol.noSymbol)

getClassSymbol(fromType).flatMap { fromTypeSymbol =>
getTrimmedFieldSymbol(fromTypeSymbol) match {
case FieldType(possiblyTypeArg) => Right(swapWithSuppliedType(fromType, possiblyTypeArg))
case _ => FocusError.CouldntFindFieldType(fromType.show, fieldName).asResult
}
}
}

private object FieldType {
def unapply(fieldSymbol: Symbol): Option[TypeRepr] = fieldSymbol match {
case sym if sym.isNoSymbol => None
case sym =>
sym.tree match {
case ValDef(_, typeTree, _) => Some(typeTree.tpe)
case _ => None
// Only needed for Tuples because `_1` is a DefDef while `_1 ` is the corresponding ValDef.
case DefDef(_, _, typeTree, _) => Some(typeTree.tpe)
case _ => None
}
}
}
Expand All @@ -69,4 +82,42 @@ private[focus] trait SelectParserBase extends ParserBase {
case Some(typeParamList :: _) if typeParamList.exists(_.isTypeParam) => typeParamList
case _ => Nil
}

@tailrec
final def etaExpandIfNecessary(term: Term): FocusResult[Term] =
if (term.isExpr) {
Right(term)
} else {
val expanded: Term = term.etaExpand(Symbol.spliceOwner)

val implicitsResult: FocusResult[List[Term]] =
expanded match {
case Block(List(DefDef(_, List(params), _, _)), _) =>
params.params.foldLeft[FocusResult[List[Term]]](Right(List.empty[Term])) {
case (Right(acc), ValDef(_, t, _)) =>
def searchForImplicit(typeRepr: TypeRepr): FocusResult[Term] =
Implicits.search(typeRepr) match {
case success: ImplicitSearchSuccess =>
Right(success.tree)
case failure: ImplicitSearchFailure =>
FocusError.ImplicitNotFound(typeRepr.show, failure.explanation).asResult
}

searchForImplicit(t.tpe)
.map(acc :+ _)

case (Right(acc), other) =>
FocusError.ExpansionFailed(s"Expected value definition but found unexpected ${other.show}").asResult
case (left @ Left(_), _) =>
left
}
case other =>
FocusError.ExpansionFailed(s"Expected block of expanded term but found unexpected ${other.show}").asResult
}

implicitsResult match {
case Left(error) => Left(error)
case Right(implicits) => etaExpandIfNecessary(Apply(term, implicits))
}
}
}
Original file line number Diff line number Diff line change
@@ -1,30 +1,72 @@
package monocle.internal.focus.features.selectfield

import monocle.internal.focus.FocusBase
import monocle.Iso
import monocle.Lens
import scala.quoted.Quotes

private[focus] trait SelectFieldGenerator {
this: FocusBase =>

import macroContext.reflect._

def generateSelectField(action: FocusAction.SelectField): Term = {
import action.{fieldName, fromType, fromTypeArgs, toType}
private def generateGetter(from: Term, caseFieldSymbol: Symbol): Term =
Select(from, caseFieldSymbol) // o.field

def generateGetter(from: Term): Term =
Select.unique(from, fieldName) // o.field
def generateSelectField(action: FocusAction.SelectField): Term = {
import action.{caseFieldSymbol, fromType, fromTypeArgs, toType}

def generateSetter(from: Term, to: Term): Term =
Select.overloaded(from, "copy", fromTypeArgs, NamedArg(fieldName, to) :: Nil) // o.copy(field = value)
Select.overloaded(from, "copy", fromTypeArgs, NamedArg(caseFieldSymbol.name, to) :: Nil) // o.copy(field = value)

(fromType.asType, toType.asType) match {
case ('[f], '[t]) =>
'{
Lens.apply[f, t]((from: f) => ${ generateGetter('{ from }.asTerm).asExprOf[t] })((to: t) =>
Lens.apply[f, t]((from: f) => ${ generateGetter('{ from }.asTerm, caseFieldSymbol).asExprOf[t] })((to: t) =>
(from: f) => ${ generateSetter('{ from }.asTerm, '{ to }.asTerm).asExprOf[f] }
)
}.asTerm
}
}

def generateSelectFieldWithImplicits(action: FocusAction.SelectFieldWithImplicits): Term = {
import action.{caseFieldSymbol, fromType, toType, setter}

(fromType.asType, toType.asType) match {
case ('[f], '[t]) =>
'{
Lens.apply[f, t]((from: f) => ${ generateGetter('{ from }.asTerm, caseFieldSymbol).asExprOf[t] })(
${ setter.asExprOf[t => f => f] }
)
}.asTerm
}
}

def generateSelectOnlyField(action: FocusAction.SelectOnlyField): Term = {
import action.{caseFieldSymbol, fromType, fromTypeArgs, fromCompanion, toType}

def generateReverseGet(to: Term): Term =
Select.overloaded(fromCompanion, "apply", fromTypeArgs, List(to)) // Companion.apply(value)

(fromType.asType, toType.asType) match {
case ('[f], '[t]) =>
'{
Iso.apply[f, t]((from: f) => ${ generateGetter('{ from }.asTerm, caseFieldSymbol).asExprOf[t] })((to: t) =>
${ generateReverseGet('{ to }.asTerm).asExprOf[f] }
)
}.asTerm
}
}

def generateSelectOnlyFieldWithImplicits(action: FocusAction.SelectOnlyFieldWithImplicits): Term = {
import action.{caseFieldSymbol, fromType, toType, reverseGet}

(fromType.asType, toType.asType) match {
case ('[f], '[t]) =>
'{
Iso.apply[f, t]((from: f) => ${ generateGetter('{ from }.asTerm, caseFieldSymbol).asExprOf[t] })(
${ reverseGet.asExprOf[t => f] }
)
}.asTerm
}
}
}
Loading