Skip to content

Commit afc03c9

Browse files
Merge pull request #5944 from dotty-staging/fix-5941
Implement Monocle macros
2 parents 7343e23 + 7538a32 commit afc03c9

File tree

8 files changed

+365
-1
lines changed

8 files changed

+365
-1
lines changed

compiler/src/dotty/tools/dotc/tastyreflect/TreeOpsImpl.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,9 @@ trait TreeOpsImpl extends scala.tasty.reflect.TreeOps with RootPositionImpl with
383383
}
384384

385385
object Ident extends IdentModule {
386+
def apply(tmref: TermRef)(implicit ctx: Context): Term =
387+
withDefaultPos(implicit ctx => tpd.ref(tmref).asInstanceOf[Term])
388+
386389
def copy(original: Tree)(name: String)(implicit ctx: Context): Ident =
387390
tpd.cpy.Ident(original)(name.toTermName)
388391

compiler/src/dotty/tools/dotc/tastyreflect/TypeOrBoundsOpsImpl.scala

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package dotty.tools.dotc.tastyreflect
22

33
import dotty.tools.dotc.core.{Contexts, Names, Types}
4+
import dotty.tools.dotc.core.Decorators._
45

56
trait TypeOrBoundsOpsImpl extends scala.tasty.reflect.TypeOrBoundsOps with CoreImpl {
67

@@ -23,6 +24,11 @@ trait TypeOrBoundsOpsImpl extends scala.tasty.reflect.TypeOrBoundsOps with CoreI
2324
if (tpe.classSymbol.exists) Some(tpe.classSymbol.asClass) else None
2425

2526
def typeSymbol(implicit ctx: Context): Symbol = tpe.typeSymbol
27+
28+
def isSingleton(implicit ctx: Context): Boolean = tpe.isSingleton
29+
30+
def memberType(member: Symbol)(implicit ctx: Context): Type =
31+
member.info.asSeenFrom(tpe, member.owner)
2632
}
2733

2834
def ConstantTypeDeco(x: ConstantType): Type.ConstantTypeAPI = new Type.ConstantTypeAPI {
@@ -181,6 +187,9 @@ trait TypeOrBoundsOpsImpl extends scala.tasty.reflect.TypeOrBoundsOps with CoreI
181187
}
182188

183189
object TermRef extends TermRefModule {
190+
def apply(qual: TypeOrBounds, name: String)(implicit ctx: Context): TermRef =
191+
Types.TermRef(qual, name.toTermName)
192+
184193
def unapply(x: TypeOrBounds)(implicit ctx: Context): Option[(String, TypeOrBounds /* Type | NoPrefix */)] = x match {
185194
case tp: Types.NamedType =>
186195
tp.designator match {

library/src/scala/tasty/reflect/TreeOps.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package scala.tasty
22
package reflect
33

44
trait TreeOps extends Core {
5-
65
// Decorators
76

87
implicit def TreeDeco(tree: Tree): TreeAPI
@@ -249,6 +248,7 @@ trait TreeOps extends Core {
249248
/** Scala term identifier */
250249
val Ident: IdentModule
251250
abstract class IdentModule {
251+
def apply(tmref: TermRef)(implicit ctx: Context): Term
252252

253253
def copy(original: Tree)(name: String)(implicit ctx: Context): Ident
254254

library/src/scala/tasty/reflect/TypeOrBoundsOps.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ trait TypeOrBoundsOps extends Core {
5555
def widen(implicit ctx: Context): Type
5656
def classSymbol(implicit ctx: Context): Option[ClassSymbol]
5757
def typeSymbol(implicit ctx: Context): Symbol
58+
def isSingleton(implicit ctx: Context): Boolean
59+
def memberType(member: Symbol)(implicit ctx: Context): Type
5860
}
5961

6062
val IsType: IsTypeModule
@@ -107,6 +109,7 @@ trait TypeOrBoundsOps extends Core {
107109

108110
val TermRef: TermRefModule
109111
abstract class TermRefModule {
112+
def apply(qual: TypeOrBounds, name: String)(implicit ctx: Context): TermRef
110113
def unapply(typeOrBounds: TypeOrBounds)(implicit ctx: Context): Option[(String, TypeOrBounds /* Type | NoPrefix */)]
111114
}
112115

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
abstract class Lens[S, T] {
2+
def get(s: S): T
3+
def set(t: T, s: S) :S
4+
}
5+
6+
import scala.quoted._
7+
import scala.tasty._
8+
9+
object Lens {
10+
def apply[S, T](_get: S => T)(_set: T => S => S): Lens[S, T] = new Lens {
11+
def get(s: S): T = _get(s)
12+
def set(t: T, s: S): S = _set(t)(s)
13+
}
14+
15+
def impl[S: Type, T: Type](getter: Expr[S => T])(implicit refl: Reflection): Expr[Lens[S, T]] = {
16+
import refl._
17+
import util._
18+
import quoted.Toolbox.Default._
19+
20+
// obj.copy(field = value)
21+
def setterBody(obj: Expr[S], value: Expr[T], field: String): Expr[S] =
22+
Term.Select.overloaded(obj.unseal, "copy", Nil, Term.NamedArg(field, value.unseal) :: Nil).seal[S]
23+
24+
// exception: getter.unseal.underlyingArgument
25+
getter.unseal match {
26+
case Term.Inlined(
27+
None, Nil,
28+
Term.Block(
29+
DefDef(_, Nil, (param :: Nil) :: Nil, _, Some(Term.Select(o, field))) :: Nil,
30+
Term.Lambda(meth, _)
31+
)
32+
) if o.symbol == param.symbol =>
33+
'{
34+
val setter = (t: T) => (s: S) => ${ setterBody('s, 't, field) }
35+
apply($getter)(setter)
36+
}
37+
case _ =>
38+
throw new QuoteError("Unsupported syntax. Example: `GenLens[Address](_.streetNumber)`")
39+
}
40+
}
41+
}
42+
43+
object GenLens {
44+
/** case class Address(streetNumber: Int, streetName: String)
45+
*
46+
* GenLens[Address](_.streetNumber) ~~>
47+
*
48+
* Lens[Address, Int](_.streetNumber)(n => a => a.copy(streetNumber = n))
49+
*/
50+
51+
def apply[S] = new MkGenLens[S]
52+
class MkGenLens[S] {
53+
inline def apply[T](get: => (S => T)): Lens[S, T] = ${ Lens.impl('get) }
54+
}
55+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
case class Address(streetNumber: Int, streetName: String)
2+
3+
object Test {
4+
def main(args: Array[String]): Unit = {
5+
val len = GenLens[Address](_.streetNumber + 3) // error
6+
val address = Address(10, "High Street")
7+
assert(len.get(address) == 10)
8+
val addr2 = len.set(5, address)
9+
assert(len.get(addr2) == 5)
10+
}
11+
}
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
trait Lens[S, T] {
2+
def get(s: S): T
3+
def set(t: T, s: S) :S
4+
}
5+
6+
import scala.quoted._
7+
import scala.tasty._
8+
9+
object Lens {
10+
def apply[S, T](_get: S => T)(_set: T => S => S): Lens[S, T] = new Lens {
11+
def get(s: S): T = _get(s)
12+
def set(t: T, s: S): S = _set(t)(s)
13+
}
14+
15+
def impl[S: Type, T: Type](getter: Expr[S => T])(implicit refl: Reflection): Expr[Lens[S, T]] = {
16+
import refl._
17+
import util._
18+
import quoted.Toolbox.Default._
19+
20+
21+
// obj.copy(a = obj.a.copy(b = a.b.copy(c = v)))
22+
def setterBody(obj: Term, value: Term, parts: List[String]): Term = {
23+
// o.copy(field = value)
24+
def helper(obj: Term, value: Term, field: String): Term =
25+
Term.Select.overloaded(obj, "copy", Nil, Term.NamedArg(field, value) :: Nil)
26+
27+
parts match {
28+
case field :: Nil => helper(obj, value, field)
29+
case field :: parts =>
30+
helper(obj, setterBody(Term.Select.unique(obj, field), value, parts), field)
31+
}
32+
}
33+
34+
object Path {
35+
private def recur(tree: Term, selects: List[String]): Option[(Term, List[String])] = tree match {
36+
case Term.Ident(_) if selects.nonEmpty => Some((tree, selects))
37+
case Term.Select(qual, name) => recur(qual, name :: selects)
38+
case _ => None
39+
}
40+
41+
def unapply(t: Term): Option[(Term, List[String])] = recur(t, Nil)
42+
}
43+
44+
object Function {
45+
def unapply(t: Term): Option[(List[ValDef], Term)] = t match {
46+
case Term.Inlined(
47+
None, Nil,
48+
Term.Block(
49+
(ddef @ DefDef(_, Nil, params :: Nil, _, Some(body))) :: Nil,
50+
Term.Lambda(meth, _)
51+
)
52+
) if meth.symbol == ddef.symbol => Some((params, body))
53+
case _ => None
54+
}
55+
}
56+
57+
// exception: getter.unseal.underlyingArgument
58+
getter.unseal match {
59+
case Function(param :: Nil, Path(o, parts)) if o.symbol == param.symbol =>
60+
'{
61+
val setter = (t: T) => (s: S) => ${ setterBody(('s).unseal, ('t).unseal, parts).seal[S] }
62+
apply($getter)(setter)
63+
}
64+
case _ =>
65+
throw new QuoteError("Unsupported syntax. Example: `GenLens[Address](_.streetNumber)`")
66+
}
67+
}
68+
}
69+
70+
object GenLens {
71+
/** case class Address(streetNumber: Int, streetName: String)
72+
*
73+
* GenLens[Address](_.streetNumber) ~~>
74+
*
75+
* Lens[Address, Int](_.streetNumber)(n => a => a.copy(streetNumber = n))
76+
*/
77+
78+
def apply[S] = new MkGenLens[S]
79+
class MkGenLens[S] {
80+
inline def apply[T](get: => (S => T)): Lens[S, T] = ${ Lens.impl('get) }
81+
}
82+
}
83+
84+
trait Iso[S, A] {
85+
def from(a: A): S
86+
def to(s: S): A
87+
}
88+
89+
object Iso {
90+
def apply[S, A](_from: A => S)(_to: S => A): Iso[S, A] = new Iso {
91+
def from(a: A): S = _from(a)
92+
def to(s: S): A = _to(s)
93+
}
94+
95+
def impl[S: Type, A: Type](implicit refl: Reflection): Expr[Iso[S, A]] = {
96+
import refl._
97+
import util._
98+
import quoted.Toolbox.Default._
99+
100+
val tpS = typeOf[S]
101+
val tpA = typeOf[A]
102+
103+
// 1. S must be a case class
104+
// 2. A must be a tuple
105+
// 3. The parameters of S must match A
106+
if (tpS.classSymbol.flatMap(cls => if (cls.flags.is(Flags.Case)) Some(true) else None).isEmpty)
107+
throw new QuoteError("Only support generation for case classes")
108+
109+
val cls = tpS.classSymbol.get
110+
111+
val companion = tpS match {
112+
case Type.SymRef(sym, prefix) => Type.TermRef(prefix, sym.name)
113+
case Type.TypeRef(name, prefix) => Type.TermRef(prefix, name)
114+
}
115+
116+
if (cls.caseFields.size != 1)
117+
throw new QuoteError("Use GenIso.fields for case classes more than one parameter")
118+
119+
val fieldTp = tpS.memberType(cls.caseFields.head)
120+
if (!(fieldTp =:= tpA))
121+
throw new QuoteError(s"The type of case class field $fieldTp does not match $tpA")
122+
123+
'{
124+
// (p: S) => p._1
125+
val to = (p: S) => ${ Term.Select.unique(('p).unseal, "_1").seal[A] }
126+
// (p: A) => S(p)
127+
val from = (p: A) => ${ Term.Select.overloaded(Term.Ident(companion), "apply", Nil, ('p).unseal :: Nil).seal[S] }
128+
apply(from)(to)
129+
}
130+
}
131+
132+
def implUnit[S: Type](implicit refl: Reflection): Expr[Iso[S, 1]] = {
133+
import refl._
134+
import util._
135+
import quoted.Toolbox.Default._
136+
137+
val tpS = typeOf[S]
138+
139+
if (tpS.isSingleton) {
140+
val ident = Term.Ident(tpS.asInstanceOf[TermRef]).seal[S]
141+
'{
142+
Iso[S, 1](Function.const($ident))(Function.const(1))
143+
}
144+
}
145+
else if (tpS.classSymbol.flatMap(cls => if (cls.flags.is(Flags.Case)) Some(true) else None).nonEmpty) {
146+
val cls = tpS.classSymbol.get
147+
148+
if (cls.caseFields.size != 0)
149+
throw new QuoteError("Use GenIso.fields for case classes more than one parameter")
150+
151+
val companion = tpS match {
152+
case Type.SymRef(sym, prefix) => Type.TermRef(prefix, sym.name)
153+
case Type.TypeRef(name, prefix) => Type.TermRef(prefix, name)
154+
}
155+
156+
val obj = Term.Select.overloaded(Term.Ident(companion), "apply", Nil, Nil).seal[S]
157+
158+
'{
159+
Iso[S, 1](Function.const($obj))(Function.const(1))
160+
}
161+
}
162+
else {
163+
throw new QuoteError("Only support generation for case classes or singleton types")
164+
}
165+
}
166+
167+
// TODO: require whitebox macro
168+
def implFields[S: Type](implicit refl: Reflection): Expr[Iso[S, Any]] = ???
169+
}
170+
171+
object GenIso {
172+
/**
173+
* GenIso[Person, String] ~~>
174+
*
175+
* Iso[Person, String]
176+
* { p => p._1 }
177+
* { p => Person(p) }
178+
*/
179+
inline def apply[S, A]: Iso[S, A] = ${ Iso.impl[S, A] }
180+
181+
// TODO: require whitebox macro
182+
inline def fields[S]: Iso[S, Any] = ${ Iso.implFields[S] }
183+
184+
inline def unit[S]: Iso[S, 1] = ${ Iso.implUnit[S] }
185+
}
186+
187+
trait Prism[S, A] { outer =>
188+
def getOption(s: S): Option[A]
189+
def apply(a: A): S
190+
191+
def composeIso[B](iso: Iso[A, B]): Prism[S, B] = new Prism {
192+
def getOption(s: S): Option[B] = outer.getOption(s).map(a => iso.to(a))
193+
def apply(b: B): S = outer(iso.from(b))
194+
}
195+
}
196+
197+
object Prism {
198+
def apply[S, A](getOpt: S => Option[A])(app: A => S): Prism[S, A] = new Prism {
199+
def getOption(s: S): Option[A] = getOpt(s)
200+
def apply(a: A): S = app(a)
201+
}
202+
203+
def impl[S: Type, A <: S : Type](implicit refl: Reflection): Expr[Prism[S, A]] = {
204+
import refl._
205+
import util._
206+
207+
'{
208+
val get = (p: S) => if (p.isInstanceOf[A]) Some(p.asInstanceOf[A]) else None
209+
val app = (p: A) => p
210+
apply(get)(app)
211+
}
212+
}
213+
}
214+
215+
object GenPrism {
216+
/**
217+
* GenPrism[Json, JStr] ~~>
218+
*
219+
* Prism[Json, JStr]{
220+
* case JStr(v) => Some(v)
221+
* case _ => None
222+
* }(jstr => jstr)
223+
*/
224+
inline def apply[S, A <: S]: Prism[S, A] = ${ Prism.impl[S, A] }
225+
}

0 commit comments

Comments
 (0)