From acb76831a4ef787e15b6389b7c8202b5f1f335e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20=C5=A0pan=C4=9Bl?= Date: Thu, 1 Sep 2022 16:02:49 +0200 Subject: [PATCH 1/7] Add test: Demonstrate issue with this in presence of self alias --- .../quicklens/ModifySelfThisTest.scala | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 quicklens/src/test/scala/com/softwaremill/quicklens/ModifySelfThisTest.scala diff --git a/quicklens/src/test/scala/com/softwaremill/quicklens/ModifySelfThisTest.scala b/quicklens/src/test/scala/com/softwaremill/quicklens/ModifySelfThisTest.scala new file mode 100644 index 0000000..ca011ed --- /dev/null +++ b/quicklens/src/test/scala/com/softwaremill/quicklens/ModifySelfThisTest.scala @@ -0,0 +1,24 @@ +package com.softwaremill.quicklens + +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +import ModifySelfThisTest._ + +object ModifySelfThisTest { + + case class State(x: Int) {self => + + def mod: State = this.modify(_.x).setTo(1) + } + +} + +class ModifySelfThisTest extends AnyFlatSpec with Matchers { + it should "modify an object even in presence of self alias" in { + val s = State(0) + val modified = s.mod + + modified.x shouldBe 1 + } +} From 7ce707a0a22e2d2d0ab9ff9ee28cfd207924cae0 Mon Sep 17 00:00:00 2001 From: Kacper Korban Date: Thu, 1 Sep 2022 18:55:07 +0200 Subject: [PATCH 2/7] Add another self type test --- .../quicklens/ModifySelfThisTest.scala | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/quicklens/src/test/scala/com/softwaremill/quicklens/ModifySelfThisTest.scala b/quicklens/src/test/scala/com/softwaremill/quicklens/ModifySelfThisTest.scala index ca011ed..d175946 100644 --- a/quicklens/src/test/scala/com/softwaremill/quicklens/ModifySelfThisTest.scala +++ b/quicklens/src/test/scala/com/softwaremill/quicklens/ModifySelfThisTest.scala @@ -7,11 +7,21 @@ import ModifySelfThisTest._ object ModifySelfThisTest { - case class State(x: Int) {self => + case class State(x: Int) { self => def mod: State = this.modify(_.x).setTo(1) } + trait A { + def a: Unit + } + + case class State1(x: Int) extends A { self: A => + + def mod: State1 = this.modify(_.x).setTo(1) + + def a: Unit = () + } } class ModifySelfThisTest extends AnyFlatSpec with Matchers { @@ -21,4 +31,11 @@ class ModifySelfThisTest extends AnyFlatSpec with Matchers { modified.x shouldBe 1 } + + it should "modify an object even in presence of self type" in { + val s = State(0) + val modified = s.mod + + modified.x shouldBe 1 + } } From f6969abf918b2d823a8b1b52d2dde66e8e606343 Mon Sep 17 00:00:00 2001 From: Kacper Korban Date: Thu, 1 Sep 2022 19:43:49 +0200 Subject: [PATCH 3/7] Fix self type modification --- .../quicklens/QuicklensMacros.scala | 39 +++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/quicklens/src/main/scala-3/com/softwaremill/quicklens/QuicklensMacros.scala b/quicklens/src/main/scala-3/com/softwaremill/quicklens/QuicklensMacros.scala index ec6c4d4..3afed1c 100644 --- a/quicklens/src/main/scala-3/com/softwaremill/quicklens/QuicklensMacros.scala +++ b/quicklens/src/main/scala-3/com/softwaremill/quicklens/QuicklensMacros.scala @@ -55,7 +55,7 @@ object QuicklensMacros { s"Unsupported path element. Path must have shape: _.field1.field2.each.field3.(...), got: ${tree.show}" def noSuchMember(term: Term, name: String) = - s"${term.tpe} has no member named $name" + s"${term.tpe.show} has no member named $name" def methodSupported(method: String) = Seq("at", "each", "eachWhere", "eachRight", "eachLeft", "atOrElse", "index", "when").contains(method) @@ -140,28 +140,47 @@ object QuicklensMacros { } } + extension (tpe: TypeRepr) + def poorMansLUB: TypeRepr = tpe match { + case AndType(l, r) if l <:< r => l + case AndType(l, r) if r <:< l => r + case _ => tpe + } + def widenAll: TypeRepr = + tpe.widen.dealias.poorMansLUB + def termMethodByNameUnsafe(term: Term, name: String): Symbol = { - term.tpe.widen.dealias.typeSymbol + term.tpe.widenAll.typeSymbol .memberMethod(name) .headOption .getOrElse(report.errorAndAbort(noSuchMember(term, name))) } def termAccessorMethodByNameUnsafe(term: Term, name: String): (Symbol, Int) = { - val caseParamNames = term.tpe.widen.dealias.typeSymbol.primaryConstructor.paramSymss.flatten.filter(_.isTerm).map(_.name) + val typeSymbol = term.tpe.widenAll.typeSymbol + val caseParamNames = typeSymbol.primaryConstructor.paramSymss.flatten.filter(_.isTerm).map(_.name) val idx = caseParamNames.indexOf(name) - term.tpe.widen.dealias.typeSymbol.caseFields.find(_.name == name).getOrElse(report.errorAndAbort(noSuchMember(term, name))) + typeSymbol.caseFields.find(_.name == name).getOrElse(report.errorAndAbort(noSuchMember(term, name))) -> (idx + 1) } + def isProduct(sym: Symbol): Boolean = { + sym.flags.is(Flags.Case) + } + + def isSum(sym: Symbol): Boolean = { + sym.flags.is(Flags.Enum) || + (sym.flags.is(Flags.Sealed) && (sym.flags.is(Flags.Trait) || sym.flags.is(Flags.Abstract))) + } + def caseClassCopy( owner: Symbol, mod: Expr[A => A], obj: Term, fields: Seq[(PathSymbol.Field, Seq[PathTree])] ): Term = { - val objSymbol = obj.tpe.widen.dealias.typeSymbol - if objSymbol.flags.is(Flags.Case) then { + val objSymbol = obj.tpe.widenAll.typeSymbol + if isProduct(objSymbol) then { val copy = termMethodByNameUnsafe(obj, "copy") val argsMap: Map[Int, Term] = fields.map { (field, trees) => val (fieldMethod, idx) = termAccessorMethodByNameUnsafe(obj, field.name) @@ -185,11 +204,9 @@ object QuicklensMacros { case AppliedType(_, typeParams) => Apply(TypeApply(Select(obj, copy), typeParams.map(Inferred(_))), args) case _ => Apply(Select(obj, copy), args) } - } else if objSymbol.flags.is(Flags.Enum) || - (objSymbol.flags.is(Flags.Sealed) && (objSymbol.flags.is(Flags.Trait) || objSymbol.flags.is(Flags.Abstract))) - then { + } else if isSum(objSymbol) then { // if the source is a sealed trait / sealed abstract class / enum, generating a if-then-else with a .copy for each child (implementing case class) - val cases = obj.tpe.widen.dealias.typeSymbol.children.map { child => + val cases = objSymbol.children.map { child => val subtype = TypeIdent(child) val bind = Symbol.newBind(owner, "c", Flags.EmptyFlags, subtype.tpe) CaseDef(Bind(bind, Typed(Ref(bind), subtype)), None, caseClassCopy(owner, mod, Ref(bind), fields)) @@ -201,7 +218,7 @@ object QuicklensMacros { ... else throw new IllegalStateException() */ - val ifThens = obj.tpe.widen.dealias.typeSymbol.children.map { child => + val ifThens = objSymbol.children.map { child => val ifCond = TypeApply(Select.unique(obj, "isInstanceOf"), List(TypeIdent(child))) val ifThen = ValDef.let(owner, TypeApply(Select.unique(obj, "asInstanceOf"), List(TypeIdent(child)))) { From 504a38f00fcb7bd8e9b56f7a9057c14b03023d39 Mon Sep 17 00:00:00 2001 From: Kacper Korban Date: Thu, 1 Sep 2022 19:46:52 +0200 Subject: [PATCH 4/7] Add tests for & types --- .../quicklens/test/ModifyAndTypeTest.scala | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 quicklens/src/test/scala-3/com/softwaremill/quicklens/test/ModifyAndTypeTest.scala diff --git a/quicklens/src/test/scala-3/com/softwaremill/quicklens/test/ModifyAndTypeTest.scala b/quicklens/src/test/scala-3/com/softwaremill/quicklens/test/ModifyAndTypeTest.scala new file mode 100644 index 0000000..f40c08b --- /dev/null +++ b/quicklens/src/test/scala-3/com/softwaremill/quicklens/test/ModifyAndTypeTest.scala @@ -0,0 +1,87 @@ +package com.softwaremill.quicklens + +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +import ModifyAndTypeTest._ + +object ModifyAndTypeTest { + case class A(a: Int) extends B + trait B { + def a: Int + } + + case class A1(a: Int) + + sealed trait T + case class C(a: Int) extends T with B + + sealed trait T1 + case class C1(a: Int) extends T +} + +class ModifyAndTypeTest extends AnyFlatSpec with Matchers { + it should "modify an & type object" in { + val ab: A & B = A(0) + + val modified = ab.modify(_.a).setTo(1) + + modified.a shouldBe 1 + } + + it should "modify an & type object 1" in { + val ab: B & A = A(0) + + val modified = ab.modify(_.a).setTo(1) + + modified.a shouldBe 1 + } + + it should "modify an & type object 2" in { + val ab: B1 & A1 = new A1(0) with B + + val modified = ab.modify(_.a).setTo(1) + + modified.a shouldBe 1 + } + + it should "modify an & type object 3" in { + val ab: A1 & B1 = new A1(0) with B + + val modified = ab.modify(_.a).setTo(1) + + modified.a shouldBe 1 + } + + it should "modify an & type object with a sealed trait" in { + val tb: T & B = C(0) + + val modified = tb.modify(_.a).setTo(1) + + modified.a shouldBe 1 + } + + it should "modify an & type object with a sealed trait 1" in { + val tb: B & T = C(0) + + val modified = tb.modify(_.a).setTo(1) + + modified.a shouldBe 1 + } + + it should "modify an & type object with a sealed trait 2" in { + val tb: B & T1 = new C1(0) with B + + val modified = tb.modify(_.a).setTo(1) + + modified.a shouldBe 1 + } + + it should "modify an & type object with a sealed trait 3" in { + val tb: T1 = new C1(0) with B + + val modified = tb.modify(_.a).setTo(1) + + modified.a shouldBe 1 + } +} From e875d50bf00c5467b420b03bf1e459be91c9c80d Mon Sep 17 00:00:00 2001 From: Kacper Korban Date: Thu, 1 Sep 2022 19:50:37 +0200 Subject: [PATCH 5/7] Mostly working solution for & types --- .../quicklens/QuicklensMacros.scala | 35 +++++++++++++------ .../quicklens/test/ModifyAndTypeTest.scala | 24 ++++++------- 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/quicklens/src/main/scala-3/com/softwaremill/quicklens/QuicklensMacros.scala b/quicklens/src/main/scala-3/com/softwaremill/quicklens/QuicklensMacros.scala index 3afed1c..b0ac6b8 100644 --- a/quicklens/src/main/scala-3/com/softwaremill/quicklens/QuicklensMacros.scala +++ b/quicklens/src/main/scala-3/com/softwaremill/quicklens/QuicklensMacros.scala @@ -54,8 +54,8 @@ object QuicklensMacros { def unsupportedShapeInfo(tree: Tree) = s"Unsupported path element. Path must have shape: _.field1.field2.each.field3.(...), got: ${tree.show}" - def noSuchMember(term: Term, name: String) = - s"${term.tpe.show} has no member named $name" + def noSuchMember(tpeStr: String, name: String) = + s"$tpeStr has no member named $name" def methodSupported(method: String) = Seq("at", "each", "eachWhere", "eachRight", "eachLeft", "atOrElse", "index", "when").contains(method) @@ -148,19 +148,32 @@ object QuicklensMacros { } def widenAll: TypeRepr = tpe.widen.dealias.poorMansLUB + def matchingTypeSymbol: Symbol = tpe.widenAll match { + case AndType(l, r) => + val lSym = l.matchingTypeSymbol + if l.matchingTypeSymbol != Symbol.noSymbol then lSym else r.matchingTypeSymbol + case tpe if isProduct(tpe.typeSymbol) || isSum(tpe.typeSymbol) => + tpe.typeSymbol + case tpe => + Symbol.noSymbol + } - def termMethodByNameUnsafe(term: Term, name: String): Symbol = { - term.tpe.widenAll.typeSymbol + def symbolMethodByNameUnsafe(sym: Symbol, name: String): Symbol = { + sym .memberMethod(name) .headOption - .getOrElse(report.errorAndAbort(noSuchMember(term, name))) + .getOrElse(report.errorAndAbort(noSuchMember(sym.name, name))) + } + + def termMethodByNameUnsafe(term: Term, name: String): Symbol = { + symbolMethodByNameUnsafe(term.tpe.widenAll.typeSymbol, name) } def termAccessorMethodByNameUnsafe(term: Term, name: String): (Symbol, Int) = { - val typeSymbol = term.tpe.widenAll.typeSymbol + val typeSymbol = term.tpe.widenAll.matchingTypeSymbol val caseParamNames = typeSymbol.primaryConstructor.paramSymss.flatten.filter(_.isTerm).map(_.name) val idx = caseParamNames.indexOf(name) - typeSymbol.caseFields.find(_.name == name).getOrElse(report.errorAndAbort(noSuchMember(term, name))) + typeSymbol.caseFields.find(_.name == name).getOrElse(report.errorAndAbort(noSuchMember(term.tpe.show, name))) -> (idx + 1) } @@ -179,9 +192,9 @@ object QuicklensMacros { obj: Term, fields: Seq[(PathSymbol.Field, Seq[PathTree])] ): Term = { - val objSymbol = obj.tpe.widenAll.typeSymbol + val objSymbol = obj.tpe.widenAll.matchingTypeSymbol if isProduct(objSymbol) then { - val copy = termMethodByNameUnsafe(obj, "copy") + val copy = symbolMethodByNameUnsafe(objSymbol, "copy") val argsMap: Map[Int, Term] = fields.map { (field, trees) => val (fieldMethod, idx) = termAccessorMethodByNameUnsafe(obj, field.name) val resTerm: Term = trees.foldLeft[Term](Select(obj, fieldMethod)) { (term, tree) => @@ -195,7 +208,7 @@ object QuicklensMacros { val args = fieldsIdxs.map { i => argsMap.getOrElse( i, - Select(obj, termMethodByNameUnsafe(obj, "copy$default$" + i.toString)) + Select(obj, symbolMethodByNameUnsafe(objSymbol, "copy$default$" + i.toString)) ) }.toList @@ -313,7 +326,7 @@ object QuicklensMacros { paths.foldLeft(PathTree.empty) { (tree, path) => tree <> path } val res: (Expr[A => A] => Expr[S]) = (mod: Expr[A => A]) => - mapToCopy(Symbol.spliceOwner, mod, obj.asTerm, pathTree).asExpr.asInstanceOf[Expr[S]] + Typed(mapToCopy(Symbol.spliceOwner, mod, obj.asTerm, pathTree), TypeTree.of[S]).asExpr.asInstanceOf[Expr[S]] to(res) } } diff --git a/quicklens/src/test/scala-3/com/softwaremill/quicklens/test/ModifyAndTypeTest.scala b/quicklens/src/test/scala-3/com/softwaremill/quicklens/test/ModifyAndTypeTest.scala index f40c08b..b136cd5 100644 --- a/quicklens/src/test/scala-3/com/softwaremill/quicklens/test/ModifyAndTypeTest.scala +++ b/quicklens/src/test/scala-3/com/softwaremill/quicklens/test/ModifyAndTypeTest.scala @@ -38,7 +38,7 @@ class ModifyAndTypeTest extends AnyFlatSpec with Matchers { } it should "modify an & type object 2" in { - val ab: B1 & A1 = new A1(0) with B + val ab: B & A1 = new A1(0) with B val modified = ab.modify(_.a).setTo(1) @@ -46,7 +46,7 @@ class ModifyAndTypeTest extends AnyFlatSpec with Matchers { } it should "modify an & type object 3" in { - val ab: A1 & B1 = new A1(0) with B + val ab: A1 & B = new A1(0) with B val modified = ab.modify(_.a).setTo(1) @@ -69,19 +69,19 @@ class ModifyAndTypeTest extends AnyFlatSpec with Matchers { modified.a shouldBe 1 } - it should "modify an & type object with a sealed trait 2" in { - val tb: B & T1 = new C1(0) with B + // it should "modify an & type object with a sealed trait 2" in { + // val tb: B & T1 = new C1(0) with B - val modified = tb.modify(_.a).setTo(1) + // val modified = tb.modify(_.a).setTo(1) - modified.a shouldBe 1 - } + // modified.a shouldBe 1 + // } - it should "modify an & type object with a sealed trait 3" in { - val tb: T1 = new C1(0) with B + // it should "modify an & type object with a sealed trait 3" in { + // val tb: T1 & B = new C1(0) with B - val modified = tb.modify(_.a).setTo(1) + // val modified = tb.modify(_.a).setTo(1) - modified.a shouldBe 1 - } + // modified.a shouldBe 1 + // } } From ac964b8ee7951548bb449c286cc2d9ddc3aa0e1c Mon Sep 17 00:00:00 2001 From: Kacper Korban Date: Thu, 1 Sep 2022 20:35:51 +0200 Subject: [PATCH 6/7] Add an implementation limitation error for sealed hierarchies mixed with & types --- .../quicklens/QuicklensMacros.scala | 18 +++++++-------- .../quicklens/test/ModifyAndTypeTest.scala | 23 ++++++++++--------- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/quicklens/src/main/scala-3/com/softwaremill/quicklens/QuicklensMacros.scala b/quicklens/src/main/scala-3/com/softwaremill/quicklens/QuicklensMacros.scala index b0ac6b8..0098a73 100644 --- a/quicklens/src/main/scala-3/com/softwaremill/quicklens/QuicklensMacros.scala +++ b/quicklens/src/main/scala-3/com/softwaremill/quicklens/QuicklensMacros.scala @@ -136,7 +136,7 @@ object QuicklensMacros { case i: Ident if i.name.startsWith("_") => Seq.empty case _ => - report.throwError(unsupportedShapeInfo(focus.asTerm)) + report.errorAndAbort(unsupportedShapeInfo(focus.asTerm)) } } @@ -160,7 +160,7 @@ object QuicklensMacros { def symbolMethodByNameUnsafe(sym: Symbol, name: String): Symbol = { sym - .memberMethod(name) + .methodMember(name) .headOption .getOrElse(report.errorAndAbort(noSuchMember(sym.name, name))) } @@ -218,13 +218,11 @@ object QuicklensMacros { case _ => Apply(Select(obj, copy), args) } } else if isSum(objSymbol) then { - // if the source is a sealed trait / sealed abstract class / enum, generating a if-then-else with a .copy for each child (implementing case class) - val cases = objSymbol.children.map { child => - val subtype = TypeIdent(child) - val bind = Symbol.newBind(owner, "c", Flags.EmptyFlags, subtype.tpe) - CaseDef(Bind(bind, Typed(Ref(bind), subtype)), None, caseClassCopy(owner, mod, Ref(bind), fields)) + obj.tpe.widenAll match { + case AndType(_, _) => + report.errorAndAbort(s"Implementation limitation: Cannot modify sealed hierarchies mixed with & types. Try providing a more specific type.") + case _ => } - /* if (obj.isInstanceOf[Child1]) caseClassCopy(obj.asInstanceOf[Child1]) else if (obj.isInstanceOf[Child2]) caseClassCopy(obj.asInstanceOf[Child2]) else @@ -247,7 +245,7 @@ object QuicklensMacros { If(ifCond, ifThen, ifElse) } } else - report.throwError(s"Unsupported source object: must be a case class or sealed trait, but got: $objSymbol") + report.errorAndAbort(s"Unsupported source object: must be a case class or sealed trait, but got: $objSymbol") } def applyFunctionDelegate( @@ -319,7 +317,7 @@ object QuicklensMacros { case Block(List(DefDef(_, _, _, Some(p))), _) => toPath(p, focus) case _ => - report.throwError(unsupportedShapeInfo(tree)) + report.errorAndAbort(unsupportedShapeInfo(tree)) } val pathTree: PathTree = diff --git a/quicklens/src/test/scala-3/com/softwaremill/quicklens/test/ModifyAndTypeTest.scala b/quicklens/src/test/scala-3/com/softwaremill/quicklens/test/ModifyAndTypeTest.scala index b136cd5..ff229a3 100644 --- a/quicklens/src/test/scala-3/com/softwaremill/quicklens/test/ModifyAndTypeTest.scala +++ b/quicklens/src/test/scala-3/com/softwaremill/quicklens/test/ModifyAndTypeTest.scala @@ -17,7 +17,7 @@ object ModifyAndTypeTest { case class C(a: Int) extends T with B sealed trait T1 - case class C1(a: Int) extends T + case class C1(a: Int) extends T1 } class ModifyAndTypeTest extends AnyFlatSpec with Matchers { @@ -53,21 +53,22 @@ class ModifyAndTypeTest extends AnyFlatSpec with Matchers { modified.a shouldBe 1 } - it should "modify an & type object with a sealed trait" in { - val tb: T & B = C(0) + // TODO this is an implemenation limitation for now, since anonymous classes crash on runtime + // it should "modify an & type object with a sealed trait" in { + // val tb: T & B = C(0) - val modified = tb.modify(_.a).setTo(1) + // val modified = tb.modify(_.a).setTo(1) - modified.a shouldBe 1 - } + // modified.a shouldBe 1 + // } - it should "modify an & type object with a sealed trait 1" in { - val tb: B & T = C(0) + // it should "modify an & type object with a sealed trait 1" in { + // val tb: B & T = C(0) - val modified = tb.modify(_.a).setTo(1) + // val modified = tb.modify(_.a).setTo(1) - modified.a shouldBe 1 - } + // modified.a shouldBe 1 + // } // it should "modify an & type object with a sealed trait 2" in { // val tb: B & T1 = new C1(0) with B From fb158250580d0fdead9f83f61d69a9ea749f9c47 Mon Sep 17 00:00:00 2001 From: Kacper Korban Date: Thu, 1 Sep 2022 20:51:10 +0200 Subject: [PATCH 7/7] Fix wrong usage of copy$default$ methods --- .../com/softwaremill/quicklens/QuicklensMacros.scala | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/quicklens/src/main/scala-3/com/softwaremill/quicklens/QuicklensMacros.scala b/quicklens/src/main/scala-3/com/softwaremill/quicklens/QuicklensMacros.scala index 0098a73..feac364 100644 --- a/quicklens/src/main/scala-3/com/softwaremill/quicklens/QuicklensMacros.scala +++ b/quicklens/src/main/scala-3/com/softwaremill/quicklens/QuicklensMacros.scala @@ -204,17 +204,23 @@ object QuicklensMacros { idx -> namedArg }.toMap + val typeParams = obj.tpe.widenAll match { + case AppliedType(_, typeParams) => Some(typeParams) + case _ => None + } + val fieldsIdxs = 1.to(objSymbol.primaryConstructor.paramSymss.flatten.filter(_.isTerm).length) val args = fieldsIdxs.map { i => + val defaultMethod = obj.select(symbolMethodByNameUnsafe(objSymbol, "copy$default$" + i.toString)) argsMap.getOrElse( i, - Select(obj, symbolMethodByNameUnsafe(objSymbol, "copy$default$" + i.toString)) + typeParams.fold(defaultMethod)(defaultMethod.appliedToTypes) ) }.toList - obj.tpe.widen match { + typeParams match { // if the object's type is parametrised, we need to call .copy with the same type parameters - case AppliedType(_, typeParams) => Apply(TypeApply(Select(obj, copy), typeParams.map(Inferred(_))), args) + case Some(typeParams) => Apply(TypeApply(Select(obj, copy), typeParams.map(Inferred(_))), args) case _ => Apply(Select(obj, copy), args) } } else if isSum(objSymbol) then {