Skip to content

Commit

Permalink
Add initial experiments on mirror support.
Browse files Browse the repository at this point in the history
  • Loading branch information
yilinwei committed Feb 10, 2024
1 parent 0d277a1 commit 143d5b3
Show file tree
Hide file tree
Showing 7 changed files with 65 additions and 17 deletions.
3 changes: 3 additions & 0 deletions compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -974,6 +974,9 @@ class Definitions {
@tu lazy val RuntimeTuples_isInstanceOfEmptyTuple: Symbol = RuntimeTuplesModule.requiredMethod("isInstanceOfEmptyTuple")
@tu lazy val RuntimeTuples_isInstanceOfNonEmptyTuple: Symbol = RuntimeTuplesModule.requiredMethod("isInstanceOfNonEmptyTuple")

@tu lazy val JavaRecordReflectMirrorTypeRef: TypeRef = requiredClassRef("scala.runtime.JavaRecordMirror")
@tu lazy val JavaRecordReflectMirrorModule: Symbol = requiredModule("scala.runtime.JavaRecordMirror")

@tu lazy val TupledFunctionTypeRef: TypeRef = requiredClassRef("scala.util.TupledFunction")
def TupledFunctionClass(using Context): ClassSymbol = TupledFunctionTypeRef.symbol.asClass
def RuntimeTupleFunctionsModule(using Context): Symbol = requiredModule("scala.runtime.TupledFunctions")
Expand Down
13 changes: 10 additions & 3 deletions compiler/src/dotty/tools/dotc/core/SymUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,14 @@ class SymUtils:
def canAccessCtor: Boolean =
def isAccessible(sym: Symbol): Boolean = ctx.owner.isContainedIn(sym)
def isSub(sym: Symbol): Boolean = ctx.owner.ownersIterator.exists(_.derivesFrom(sym))
val ctor = self.primaryConstructor
val ctor = if defn.isJavaRecordClass(self) then self.javaCanonicalConstructor else self.primaryConstructor
(!ctor.isOneOf(Private | Protected) || isSub(self)) // we cant access the ctor because we do not extend cls
&& (!ctor.privateWithin.exists || isAccessible(ctor.privateWithin)) // check scope is compatible


def companionMirror = self.useCompanionAsProductMirror
if (!self.is(CaseClass)) "it is not a case class"

if (!(self.is(CaseClass) || defn.isJavaRecordClass(self))) "it is not a case class or record class"
else if (self.is(Abstract)) "it is an abstract class"
else if (self.primaryConstructor.info.paramInfoss.length != 1) "it takes more than one parameter list"
else if self.isDerivedValueClass then "it is a value class"
Expand Down Expand Up @@ -146,7 +147,7 @@ class SymUtils:
&& (!self.is(Method) || self.is(Accessor))

def useCompanionAsProductMirror(using Context): Boolean =
self.linkedClass.exists && !self.is(Scala2x) && !self.linkedClass.is(Case)
self.linkedClass.exists && !self.is(Scala2x) && !self.linkedClass.is(Case) && !defn.isJavaRecordClass(self)

def useCompanionAsSumMirror(using Context): Boolean =
def companionExtendsSum(using Context): Boolean =
Expand Down Expand Up @@ -249,6 +250,12 @@ class SymUtils:
def caseAccessors(using Context): List[Symbol] =
self.info.decls.filter(_.is(CaseAccessor))

// TODO: I'm convinced that we need to introduce a flag to get the canonical constructor.
// we should also check whether the names are erased in the ctor. If not, we should
// be able to infer the components directly from the constructor.
def javaCanonicalConstructor(using Context): Symbol =
self.info.decls.filter(_.isConstructor).tail.head

// TODO: Check if `Synthetic` is stamped properly
def javaRecordComponents(using Context): List[Symbol] =
self.info.decls.filter(sym => sym.is(Synthetic) && sym.is(Method) && !sym.isConstructor)
Expand Down
18 changes: 4 additions & 14 deletions compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala
Original file line number Diff line number Diff line change
Expand Up @@ -345,23 +345,13 @@ object PatternMatcher {

def resultTypeSym = unapp.symbol.info.resultType.typeSymbol

def isSyntheticJavaRecordUnapply(sym: Symbol) =
// Since the `unapply` symbol is marked as inline, the `Typer` wraps the body of the `unapply` in a separate
// anonymous class. The result type alone is not enough to distinguish that we're calling the synthesized unapply —
// we could have defined a separate `unapply` method returning a Java record somewhere, hence we resort to using
// the `coord`.
sym.is(Synthetic) && sym.isAnonymousClass && {
val resultSym = resultTypeSym
// TODO: Can a user define a separate unapply function in Java?
val unapplyFn = resultSym.linkedClass.info.decl(nme.unapply)
// TODO: This is nasty, can we add an attachment on the anonymous function for a prior link?
defn.isJavaRecordClass(resultSym) && unapplyFn.symbol.coord == sym.coord
}

// TODO: Check Scala -> Java, erased?
def isJavaRecordUnapply(sym: Symbol) = defn.isJavaRecordClass(resultTypeSym)
def tupleSel(sym: Symbol) = ref(scrutinee).select(sym)
def recordSel(sym: Symbol) = tupleSel(sym).appliedToTermArgs(Nil)

if (isSyntheticJavaRecordUnapply(unapp.symbol.owner))
// TODO: Move this to the correct location
if (isJavaRecordUnapply(unapp.symbol.owner))
val components = resultTypeSym.javaRecordComponents.map(recordSel)
matchArgsPlan(components, args, onSuccess)
else if (isSyntheticScala2Unapply(unapp.symbol) && caseAccessors.length == args.length)
Expand Down
8 changes: 8 additions & 0 deletions compiler/src/dotty/tools/dotc/typer/Synthesizer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,12 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
def newTupleMirror(arity: Int): Tree =
New(defn.RuntimeTupleMirrorTypeRef, Literal(Constant(arity)) :: Nil)

def newJavaRecordReflectMirror(tpe: Type) =
ref(defn.JavaRecordReflectMirrorModule)
.select(nme.apply)
.appliedToType(tpe)
.appliedTo(clsOf(tpe))

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)))
Expand All @@ -427,6 +433,7 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
}
val mirrorRef =
if cls.useCompanionAsProductMirror then companionPath(mirroredType, span)
else if defn.isJavaRecordClass(cls) then newJavaRecordReflectMirror(cls.typeRef)
else if defn.isTupleClass(cls) then newTupleMirror(typeElems.size) // TODO: cls == defn.PairClass when > 22
else anonymousMirror(monoType, MirrorImpl.OfProduct(pre), span)
withNoErrors(mirrorRef.cast(mirrorType).withSpan(span))
Expand Down Expand Up @@ -458,6 +465,7 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
val reason = s"it reduces to a tuple with arity $arity, expected arity <= $maxArity"
withErrors(i"${defn.PairClass} is not a generic product because $reason")
case MirrorSource.ClassSymbol(pre, cls) =>

if cls.isGenericProduct then
if ctx.runZincPhases then
// The mirror should be resynthesized if the constructor of the
Expand Down
34 changes: 34 additions & 0 deletions library/src/scala/runtime/JavaRecordMirror.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package scala.runtime

import java.lang.Record
import java.lang.reflect.Constructor
import scala.reflect.ClassTag

// TODO: Rename to JavaRecordReflectMirror
object JavaRecordMirror:

def apply[T <: Record](clazz: Class[T]): JavaRecordMirror[T] =
val components = clazz.getRecordComponents.nn
val constructorTypes = components.map(_.nn.getType.nn)
val constr = clazz.getDeclaredConstructor(constructorTypes*).nn
new JavaRecordMirror(components.length, constr)

def of[T <: Record : ClassTag]: JavaRecordMirror[T] =
JavaRecordMirror(summon[ClassTag[T]].runtimeClass.asInstanceOf[Class[T]])

// TODO: Is a constructor serializable?
final class JavaRecordMirror[T] private(arity: Int, constr: Constructor[T]) extends scala.deriving.Mirror.Product with Serializable:

override type MirroredMonoType <: Record

final def fromProduct(product: Product): MirroredMonoType =
if product.productArity != arity then
throw IllegalArgumentException(s"expected Product with $arity elements, got ${product.productArity}")
else
// TODO: Check this byte code, we want to unroll to give a happy medium between JIT'ing and having tons of extra classes
val t = arity match
case 0 => constr.newInstance()
case 1 => constr.newInstance(product.productElement(0))
case 2 => constr.newInstance(product.productElement(0), product.productElement(1))

t.nn.asInstanceOf[MirroredMonoType]
5 changes: 5 additions & 0 deletions tests/pos-java16+/java-records-mirror/FromScala.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import scala.deriving.Mirror

object C:
def useR2: Unit =
summon[Mirror.Of[R2]]
1 change: 1 addition & 0 deletions tests/pos-java16+/java-records-mirror/R2.java
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
public record R2(int i, String s) {}

0 comments on commit 143d5b3

Please sign in to comment.