Skip to content

Commit 15c5e58

Browse files
Only override defaultArgument in mirrors of new case classes
And test backwards compat
1 parent 3e1f445 commit 15c5e58

File tree

11 files changed

+107
-2
lines changed

11 files changed

+107
-2
lines changed

compiler/src/dotty/tools/dotc/core/SymUtils.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import Annotations.Annotation
1717
import Phases.*
1818
import ast.tpd.Literal
1919
import transform.Mixin
20+
import dotty.tools.tasty.TastyVersion
2021

2122
import dotty.tools.dotc.transform.sjs.JSSymUtils.sjsNeedsField
2223

@@ -115,6 +116,13 @@ class SymUtils:
115116

116117
def isGenericProduct(using Context): Boolean = whyNotGenericProduct.isEmpty
117118

119+
/** Is a case class for which mirrors support access to default arguments.
120+
* see sbt-test/scala3-compat/defaultArgument-mirrors-3.3 for why this is needed
121+
*/
122+
def mirrorSupportsDefaultArguments(using Context): Boolean =
123+
!(self.is(JavaDefined) || self.is(Scala2x)) && self.isClass && self.tastyInfo.forall:
124+
case TastyInfo(TastyVersion(major, minor, exp), _) => major == 28 && minor >= 4
125+
118126
/** Is this an old style implicit conversion?
119127
* @param directOnly only consider explicitly written methods
120128
* @param forImplicitClassOnly only consider methods generated from implicit classes

compiler/src/dotty/tools/dotc/transform/SyntheticMembers.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -663,7 +663,7 @@ class SyntheticMembers(thisPhase: DenotTransformer) {
663663
addParent(defn.Mirror_ProductClass.typeRef)
664664
addMethod(nme.fromProduct, MethodType(defn.ProductClass.typeRef :: Nil, monoType.typeRef), cls,
665665
fromProductBody(_, _, optInfo).ensureConforms(monoType.typeRef)) // t4758.scala or i3381.scala are examples where a cast is needed
666-
if cls.primaryConstructor.hasDefaultParams then
666+
if cls.primaryConstructor.hasDefaultParams && cls.mirrorSupportsDefaultArguments then
667667
overrideMethod(nme.defaultArgument, MethodType(defn.IntType :: Nil, defn.AnyType), cls,
668668
defaultArgumentBody(_, _, optInfo), isExperimental = true)
669669
}

compiler/src/dotty/tools/dotc/typer/Synthesizer.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,9 +410,10 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
410410
def makeProductMirror(pre: Type, cls: Symbol, tps: Option[List[Type]]): TreeWithErrors =
411411
val accessors = cls.caseAccessors
412412
val Seq(elemLabels, elemHasDefaults, elemTypes1) =
413+
val supportsDefaults = cls.mirrorSupportsDefaultArguments
413414
Seq(
414415
accessors.map(acc => ConstantType(Constant(acc.name.toString))),
415-
accessors.map(acc => ConstantType(Constant(acc.is(HasDefault)))),
416+
accessors.map(acc => ConstantType(Constant(supportsDefaults && acc.is(HasDefault)))),
416417
tps.getOrElse(accessors.map(mirroredType.resultType.memberInfo(_).widenExpr))
417418
).map(TypeOps.nestedPairs)
418419
val (monoType, elemTypes) = mirroredType match
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import scala.deriving.Mirror
2+
3+
package lib {
4+
5+
case class NewFoo(x: Int = 1, y: Int)
6+
7+
object NewMirrors {
8+
val mNewFoo = summon[Mirror.Of[NewFoo]]
9+
10+
val mOldFoo = summon[Mirror.Of[OldFoo]]
11+
val mOldBar = summon[Mirror.Of[OldBar]]
12+
}
13+
}
14+
15+
package app {
16+
import lib.*
17+
18+
object Main {
19+
20+
// defaultArgument implementation did not throw NoSuchElementException
21+
def foundDefaultArgument(m: Mirror.Product): Boolean = try {
22+
m.defaultArgument(0)
23+
true
24+
} catch {
25+
case _: NoSuchElementException => false
26+
}
27+
28+
def main(args: Array[String]): Unit = {
29+
30+
// NewFoo: normal case with support for default arguments
31+
32+
assert(NewMirrors.mNewFoo.defaultArgument(0) == 1)
33+
summon[NewMirrors.mNewFoo.MirroredElemHasDefaults =:= (true, false)]
34+
35+
// OldFoo: does not override the defaultArgument implementation
36+
37+
assert(!foundDefaultArgument(NewMirrors.mOldFoo)) // Expected: since mirror of old case class
38+
summon[NewMirrors.mOldFoo.MirroredElemHasDefaults =:= (false, false)] // Necessary: to be consistent with defaultArgument implementation
39+
40+
assert(!foundDefaultArgument(OldMirrors.mOldFoo)) // Expected: since mirror of old case class
41+
summon[scala.util.NotGiven[OldMirrors.mOldFoo.MirroredElemHasDefaults <:< (Boolean, Boolean)]] // reference to old mirror doesn't have any refinement
42+
summon[OldMirrors.mOldFoo.MirroredElemHasDefaults <:< Tuple] // but does inherit type member from Mirror trait
43+
44+
// OldBar: is anon mirror so could implement defaultArgument
45+
// but we manually keep behaviour consistent with other mirrors of old case classes
46+
47+
assert(NewMirrors.mOldBar ne lib.OldBar)
48+
assert(!foundDefaultArgument(NewMirrors.mOldBar))
49+
summon[NewMirrors.mOldBar.MirroredElemHasDefaults =:= (false, false)] // Ok: should be consistent with above
50+
51+
assert(OldMirrors.mOldBar ne lib.OldBar)
52+
assert(!foundDefaultArgument(OldMirrors.mOldBar))
53+
summon[scala.util.NotGiven[OldMirrors.mOldBar.MirroredElemHasDefaults <:< (Boolean, Boolean)]]
54+
summon[OldMirrors.mOldBar.MirroredElemHasDefaults <:< Tuple]
55+
56+
}
57+
}
58+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
lazy val lib = project.in(file("lib"))
2+
.settings(
3+
scalaVersion := "3.3.0"
4+
)
5+
6+
lazy val app = project.in(file("app"))
7+
.dependsOn(lib)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package lib
2+
3+
import deriving.Mirror
4+
5+
case class OldFoo(x: Int = 1, y: Int)
6+
7+
case class OldBar(x: Int = 1, y: Int)
8+
case object OldBar
9+
10+
object OldMirrors {
11+
val mOldFoo = summon[Mirror.ProductOf[OldFoo]]
12+
val mOldBar = summon[Mirror.ProductOf[OldBar]]
13+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import sbt._
2+
import Keys._
3+
4+
object DottyInjectedPlugin extends AutoPlugin {
5+
override def requires = plugins.JvmPlugin
6+
override def trigger = allRequirements
7+
8+
override val projectSettings = Seq(
9+
scalaVersion := sys.props("plugin.scalaVersion")
10+
)
11+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
> app/run
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
case class MyProduct(x: Int)
2+
case class WillGetDefault(x: Int)
3+
case class WillChangeDefault(x: Int = 1)

sbt-test/source-dependencies/mirror-product/Test.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,5 @@ transparent inline def foo[T](using m: Mirror.Of[T]): Int =
88

99
@main def Test =
1010
assert(foo[MyProduct] == 2)
11+
assert(summon[Mirror.Of[WillGetDefault]].defaultArgument(0) == 1)
12+
assert(summon[Mirror.Of[WillChangeDefault]].defaultArgument(0) == 2)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
case class MyProduct(x: Int, y: String)
2+
case class WillGetDefault(x: Int = 1)
3+
case class WillChangeDefault(x: Int = 2)

0 commit comments

Comments
 (0)