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

Let mirrors support default parameters #17979

Open
wants to merge 2 commits into
base: main
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
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,7 @@ class Definitions {
@tu lazy val MirrorClass: ClassSymbol = requiredClass("scala.deriving.Mirror")
@tu lazy val Mirror_ProductClass: ClassSymbol = requiredClass("scala.deriving.Mirror.Product")
@tu lazy val Mirror_Product_fromProduct: Symbol = Mirror_ProductClass.requiredMethod(nme.fromProduct)
@tu lazy val Mirror_Product_defaultArgument: Symbol = Mirror_ProductClass.requiredMethod(nme.defaultArgument)
@tu lazy val Mirror_SumClass: ClassSymbol = requiredClass("scala.deriving.Mirror.Sum")
@tu lazy val Mirror_SingletonClass: ClassSymbol = requiredClass("scala.deriving.Mirror.Singleton")
@tu lazy val Mirror_SingletonProxyClass: ClassSymbol = requiredClass("scala.deriving.Mirror.SingletonProxy")
Expand Down
2 changes: 2 additions & 0 deletions compiler/src/dotty/tools/dotc/core/StdNames.scala
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@ object StdNames {
val LiteralAnnotArg: N = "LiteralAnnotArg"
val Matchable: N = "Matchable"
val MatchCase: N = "MatchCase"
val MirroredElemHasDefaults: N = "MirroredElemHasDefaults"
val MirroredElemTypes: N = "MirroredElemTypes"
val MirroredElemLabels: N = "MirroredElemLabels"
val MirroredLabel: N = "MirroredLabel"
Expand Down Expand Up @@ -452,6 +453,7 @@ object StdNames {
val create: N = "create"
val currentMirror: N = "currentMirror"
val curried: N = "curried"
val defaultArgument: N = "defaultArgument"
val definitions: N = "definitions"
val delayedInit: N = "delayedInit"
val delayedInitArg: N = "delayedInit$body"
Expand Down
8 changes: 8 additions & 0 deletions compiler/src/dotty/tools/dotc/core/SymUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import Annotations.Annotation
import Phases.*
import ast.tpd.Literal
import transform.Mixin
import dotty.tools.tasty.TastyVersion

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

Expand Down Expand Up @@ -115,6 +116,13 @@ class SymUtils:

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

/** Is a case class for which mirrors support access to default arguments.
* see sbt-test/scala3-compat/defaultArgument-mirrors-3.3 for why this is needed
*/
def mirrorSupportsDefaultArguments(using Context): Boolean =
!(self.is(JavaDefined) || self.is(Scala2x)) && self.isClass && self.tastyInfo.forall:
case TastyInfo(TastyVersion(major, minor, exp), _) => major == 28 && minor >= 4

/** Is this an old style implicit conversion?
* @param directOnly only consider explicitly written methods
* @param forImplicitClassOnly only consider methods generated from implicit classes
Expand Down
35 changes: 34 additions & 1 deletion compiler/src/dotty/tools/dotc/transform/SyntheticMembers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Decorators.*
import NameOps.*
import Annotations.Annotation
import typer.ProtoTypes.constrained
import ast.untpd
import ast.{tpd, untpd}

import util.Property
import util.Spans.Span
Expand Down Expand Up @@ -547,6 +547,30 @@ class SyntheticMembers(thisPhase: DenotTransformer) {
New(classRefApplied, elems)
end fromProductBody

def defaultArgumentBody(caseClass: Symbol, index: Tree, optInfo: Option[MirrorImpl.OfProduct])(using Context): Tree =
val companionTree: Tree =
val companion: Symbol = caseClass.companionModule
val prefix: Type = optInfo.fold(NoPrefix)(_.pre)
ref(TermRef(prefix, companion.asTerm))

def defaultArgumentGetter(idx: Int): Tree =
val getterName = NameKinds.DefaultGetterName(nme.CONSTRUCTOR, idx)
val getterDenot = companionTree.tpe.member(getterName)
companionTree.select(TermRef(companionTree.tpe, getterName, getterDenot))

val withDefaultCases = for
(acc, idx) <- caseClass.caseAccessors.zipWithIndex if acc.is(HasDefault)
body = Typed(defaultArgumentGetter(idx), TypeTree(defn.AnyType)) // so match tree does try to find union of case types
yield CaseDef(Literal(Constant(idx)), EmptyTree, body)

val withoutDefaultCase =
val stringIndex = Apply(Select(index, nme.toString_), Nil)
val nsee = tpd.resolveConstructor(defn.NoSuchElementExceptionType, List(stringIndex))
CaseDef(Underscore(defn.IntType), EmptyTree, Throw(nsee))

Match(index, withDefaultCases :+ withoutDefaultCase)
end defaultArgumentBody

/** For an enum T:
*
* def ordinal(x: MirroredMonoType) = x.ordinal
Expand Down Expand Up @@ -616,6 +640,12 @@ class SyntheticMembers(thisPhase: DenotTransformer) {
synthesizeDef(meth, vrefss => body(cls, vrefss.head.head))
}
}
def overrideMethod(name: TermName, info: Type, cls: Symbol, body: (Symbol, Tree) => Context ?=> Tree, isExperimental: Boolean = false): Unit = {
val meth = newSymbol(clazz, name, Synthetic | Method | Override, info, coord = clazz.coord)
if isExperimental then meth.addAnnotation(defn.ExperimentalAnnot)
meth.enteredAfter(thisPhase)
newBody = newBody :+ synthesizeDef(meth, vrefss => body(cls, vrefss.head.head))
}
val linked = clazz.linkedClass
lazy val monoType = {
val existing = clazz.info.member(tpnme.MirroredMonoType).symbol
Expand All @@ -633,6 +663,9 @@ class SyntheticMembers(thisPhase: DenotTransformer) {
addParent(defn.Mirror_ProductClass.typeRef)
addMethod(nme.fromProduct, MethodType(defn.ProductClass.typeRef :: Nil, monoType.typeRef), cls,
fromProductBody(_, _, optInfo).ensureConforms(monoType.typeRef)) // t4758.scala or i3381.scala are examples where a cast is needed
if cls.primaryConstructor.hasDefaultParams && cls.mirrorSupportsDefaultArguments then
overrideMethod(nme.defaultArgument, MethodType(defn.IntType :: Nil, defn.AnyType), cls,
defaultArgumentBody(_, _, optInfo), isExperimental = true)
}
def makeSumMirror(cls: Symbol, optInfo: Option[MirrorImpl.OfSum]) = {
addParent(defn.Mirror_SumClass.typeRef)
Expand Down
30 changes: 18 additions & 12 deletions compiler/src/dotty/tools/dotc/typer/Synthesizer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -409,25 +409,31 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):

def makeProductMirror(pre: Type, cls: Symbol, tps: Option[List[Type]]): TreeWithErrors =
val accessors = cls.caseAccessors
val elemLabels = accessors.map(acc => ConstantType(Constant(acc.name.toString)))
val typeElems = tps.getOrElse(accessors.map(mirroredType.resultType.memberInfo(_).widenExpr))
val nestedPairs = TypeOps.nestedPairs(typeElems)
val (monoType, elemsType) = mirroredType match
val Seq(elemLabels, elemHasDefaults, elemTypes1) =
val supportsDefaults = cls.mirrorSupportsDefaultArguments
Seq(
accessors.map(acc => ConstantType(Constant(acc.name.toString))),
accessors.map(acc => ConstantType(Constant(supportsDefaults && acc.is(HasDefault)))),
tps.getOrElse(accessors.map(mirroredType.resultType.memberInfo(_).widenExpr))
).map(TypeOps.nestedPairs)
val (monoType, elemTypes) = mirroredType match
case mirroredType: HKTypeLambda =>
(mkMirroredMonoType(mirroredType), mirroredType.derivedLambdaType(resType = nestedPairs))
(mkMirroredMonoType(mirroredType), mirroredType.derivedLambdaType(resType = elemTypes1))
case _ =>
(mirroredType, nestedPairs)
val elemsLabels = TypeOps.nestedPairs(elemLabels)
checkRefinement(formal, tpnme.MirroredElemTypes, elemsType, span)
checkRefinement(formal, tpnme.MirroredElemLabels, elemsLabels, span)
(mirroredType, elemTypes1)

checkRefinement(formal, tpnme.MirroredElemTypes, elemTypes, span)
checkRefinement(formal, tpnme.MirroredElemLabels, elemLabels, span)
checkRefinement(formal, tpnme.MirroredElemHasDefaults, elemHasDefaults, span)
val mirrorType = formal.constrained_& {
mirrorCore(defn.Mirror_ProductClass, monoType, mirroredType, cls.name)
.refinedWith(tpnme.MirroredElemTypes, TypeAlias(elemsType))
.refinedWith(tpnme.MirroredElemLabels, TypeAlias(elemsLabels))
.refinedWith(tpnme.MirroredElemTypes, TypeAlias(elemTypes))
.refinedWith(tpnme.MirroredElemLabels, TypeAlias(elemLabels))
.refinedWith(tpnme.MirroredElemHasDefaults, TypeAlias(elemHasDefaults))
}
val mirrorRef =
if cls.useCompanionAsProductMirror then companionPath(mirroredType, span)
else if defn.isTupleClass(cls) then newTupleMirror(typeElems.size) // TODO: cls == defn.PairClass when > 22
else if defn.isTupleClass(cls) then newTupleMirror(accessors.size) // TODO: cls == defn.PairClass when > 22
else anonymousMirror(monoType, MirrorImpl.OfProduct(pre), span)
withNoErrors(mirrorRef.cast(mirrorType).withSpan(span))
end makeProductMirror
Expand Down
11 changes: 11 additions & 0 deletions library/src/scala/deriving/Mirror.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package scala.deriving

import java.util.NoSuchElementException
import scala.annotation.experimental

/** Mirrors allows typelevel access to enums, case classes and objects, and their sealed parents.
*/
sealed trait Mirror {
Expand Down Expand Up @@ -27,6 +30,14 @@ object Mirror {

/** Create a new instance of type `T` with elements taken from product `p`. */
def fromProduct(p: scala.Product): MirroredMonoType

/** Whether each product element has a default value */
@experimental type MirroredElemHasDefaults <: Tuple

/** The default argument of the product argument at given `index` */
@experimental def defaultArgument(index: Int): Any =
throw NoSuchElementException(String.valueOf(index))

}

trait Singleton extends Product {
Expand Down
2 changes: 2 additions & 0 deletions project/MiMaFilters.scala
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ object MiMaFilters {
val LibraryForward: Map[String, Seq[ProblemFilter]] = Map(
// Additions that require a new minor version of the library
Build.previousDottyVersion -> Seq(
ProblemFilters.exclude[DirectMissingMethodProblem]("scala.compiletime.testing.Error.defaultArgument"),
),

// Additions since last LTS
Expand Down Expand Up @@ -62,6 +63,7 @@ object MiMaFilters {
),
)
val TastyCore: Seq[ProblemFilter] = Seq(
ProblemFilters.exclude[DirectMissingMethodProblem]("dotty.tools.tasty.TastyVersion.defaultArgument"),
)
val Interfaces: Seq[ProblemFilter] = Seq(
)
Expand Down
58 changes: 58 additions & 0 deletions sbt-test/scala3-compat/defaultArgument-mirrors-3.3/app/Main.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import scala.deriving.Mirror

package lib {

case class NewFoo(x: Int = 1, y: Int)

object NewMirrors {
val mNewFoo = summon[Mirror.Of[NewFoo]]

val mOldFoo = summon[Mirror.Of[OldFoo]]
val mOldBar = summon[Mirror.Of[OldBar]]
}
}

package app {
import lib.*

object Main {

// defaultArgument implementation did not throw NoSuchElementException
def foundDefaultArgument(m: Mirror.Product): Boolean = try {
m.defaultArgument(0)
true
} catch {
case _: NoSuchElementException => false
}

def main(args: Array[String]): Unit = {

// NewFoo: normal case with support for default arguments

assert(NewMirrors.mNewFoo.defaultArgument(0) == 1)
summon[NewMirrors.mNewFoo.MirroredElemHasDefaults =:= (true, false)]

// OldFoo: does not override the defaultArgument implementation

assert(!foundDefaultArgument(NewMirrors.mOldFoo)) // Expected: since mirror of old case class
summon[NewMirrors.mOldFoo.MirroredElemHasDefaults =:= (false, false)] // Necessary: to be consistent with defaultArgument implementation

assert(!foundDefaultArgument(OldMirrors.mOldFoo)) // Expected: since mirror of old case class
summon[scala.util.NotGiven[OldMirrors.mOldFoo.MirroredElemHasDefaults <:< (Boolean, Boolean)]] // reference to old mirror doesn't have any refinement
summon[OldMirrors.mOldFoo.MirroredElemHasDefaults <:< Tuple] // but does inherit type member from Mirror trait

// OldBar: is anon mirror so could implement defaultArgument
// but we manually keep behaviour consistent with other mirrors of old case classes

assert(NewMirrors.mOldBar ne lib.OldBar)
assert(!foundDefaultArgument(NewMirrors.mOldBar))
summon[NewMirrors.mOldBar.MirroredElemHasDefaults =:= (false, false)] // Ok: should be consistent with above

assert(OldMirrors.mOldBar ne lib.OldBar)
assert(!foundDefaultArgument(OldMirrors.mOldBar))
summon[scala.util.NotGiven[OldMirrors.mOldBar.MirroredElemHasDefaults <:< (Boolean, Boolean)]]
summon[OldMirrors.mOldBar.MirroredElemHasDefaults <:< Tuple]

}
}
}
7 changes: 7 additions & 0 deletions sbt-test/scala3-compat/defaultArgument-mirrors-3.3/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
lazy val lib = project.in(file("lib"))
.settings(
scalaVersion := "3.3.0"
)

lazy val app = project.in(file("app"))
.dependsOn(lib)
13 changes: 13 additions & 0 deletions sbt-test/scala3-compat/defaultArgument-mirrors-3.3/lib/Foo.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package lib

import deriving.Mirror

case class OldFoo(x: Int = 1, y: Int)

case class OldBar(x: Int = 1, y: Int)
case object OldBar

object OldMirrors {
val mOldFoo = summon[Mirror.ProductOf[OldFoo]]
val mOldBar = summon[Mirror.ProductOf[OldBar]]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import sbt._
import Keys._

object DottyInjectedPlugin extends AutoPlugin {
override def requires = plugins.JvmPlugin
override def trigger = allRequirements

override val projectSettings = Seq(
scalaVersion := sys.props("plugin.scalaVersion")
)
}
1 change: 1 addition & 0 deletions sbt-test/scala3-compat/defaultArgument-mirrors-3.3/test
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
> app/run
2 changes: 2 additions & 0 deletions sbt-test/source-dependencies/mirror-product/MyProduct.scala
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
case class MyProduct(x: Int)
case class WillGetDefault(x: Int)
case class WillChangeDefault(x: Int = 1)
2 changes: 2 additions & 0 deletions sbt-test/source-dependencies/mirror-product/Test.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ transparent inline def foo[T](using m: Mirror.Of[T]): Int =

@main def Test =
assert(foo[MyProduct] == 2)
assert(summon[Mirror.Of[WillGetDefault]].defaultArgument(0) == 1)
assert(summon[Mirror.Of[WillChangeDefault]].defaultArgument(0) == 2)
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
case class MyProduct(x: Int, y: String)
case class WillGetDefault(x: Int = 1)
case class WillChangeDefault(x: Int = 2)
1 change: 1 addition & 0 deletions tests/run-macros/i7987.check
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ scala.deriving.Mirror.Product {
type MirroredLabel >: "Some" <: "Some"
type MirroredElemTypes >: scala.*:[scala.Int, scala.Tuple$package.EmptyTuple] <: scala.*:[scala.Int, scala.Tuple$package.EmptyTuple]
type MirroredElemLabels >: scala.*:["value", scala.Tuple$package.EmptyTuple] <: scala.*:["value", scala.Tuple$package.EmptyTuple]
type MirroredElemHasDefaults >: scala.*:[false, scala.Tuple$package.EmptyTuple] <: scala.*:[false, scala.Tuple$package.EmptyTuple]
}
25 changes: 25 additions & 0 deletions tests/run-macros/mirror-defaultArgument/MirrorOps.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import scala.deriving._
import scala.annotation.experimental
import scala.quoted._

object MirrorOps:

inline def overridesDefaultArgument[T]: Boolean = ${ overridesDefaultArgumentImpl[T] }

def overridesDefaultArgumentImpl[T](using Quotes, Type[T]): Expr[Boolean] =
import quotes.reflect.*
val cls = TypeRepr.of[T].classSymbol.get
val companion = cls.companionModule.moduleClass
val methods = companion.declaredMethods

val experAnnotType = Symbol.requiredClass("scala.annotation.experimental").typeRef

Expr {
methods.exists { m =>
m.name == "defaultArgument" &&
m.flags.is(Flags.Synthetic) &&
m.annotations.exists(_.tpe <:< experAnnotType)
}
}

end MirrorOps
13 changes: 13 additions & 0 deletions tests/run-macros/mirror-defaultArgument/test.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import scala.deriving._
import scala.annotation.experimental
import scala.quoted._

import MirrorOps.*

object Test extends App:

case class WithDefault(x: Int, y: Int = 1)
assert(overridesDefaultArgument[WithDefault])

case class WithoutDefault(x: Int)
assert(!overridesDefaultArgument[WithoutDefault])
4 changes: 4 additions & 0 deletions tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ val experimentalDefinitionInLibrary = Set(
"scala.Tuple$.Reverse", // can be stabilized in 3.5
"scala.Tuple$.ReverseOnto", // can be stabilized in 3.5
"scala.runtime.Tuples$.reverse", // can be stabilized in 3.5

// New APIs: Mirror support for default arguments
"scala.deriving.Mirror$.Product.MirroredElemHasDefaults",
"scala.deriving.Mirror$.Product.defaultArgument",
)


Expand Down
Loading
Loading