Skip to content

Commit 24e861e

Browse files
committed
Add initial experiments on mirror support.
1 parent 0d277a1 commit 24e861e

File tree

7 files changed

+68
-18
lines changed

7 files changed

+68
-18
lines changed

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

+3
Original file line numberDiff line numberDiff line change
@@ -974,6 +974,9 @@ class Definitions {
974974
@tu lazy val RuntimeTuples_isInstanceOfEmptyTuple: Symbol = RuntimeTuplesModule.requiredMethod("isInstanceOfEmptyTuple")
975975
@tu lazy val RuntimeTuples_isInstanceOfNonEmptyTuple: Symbol = RuntimeTuplesModule.requiredMethod("isInstanceOfNonEmptyTuple")
976976

977+
@tu lazy val JavaRecordReflectMirrorTypeRef: TypeRef = requiredClassRef("scala.runtime.JavaRecordMirror")
978+
@tu lazy val JavaRecordReflectMirrorModule: Symbol = requiredModule("scala.runtime.JavaRecordMirror")
979+
977980
@tu lazy val TupledFunctionTypeRef: TypeRef = requiredClassRef("scala.util.TupledFunction")
978981
def TupledFunctionClass(using Context): ClassSymbol = TupledFunctionTypeRef.symbol.asClass
979982
def RuntimeTupleFunctionsModule(using Context): Symbol = requiredModule("scala.runtime.TupledFunctions")

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

+10-3
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,14 @@ class SymUtils:
9999
def canAccessCtor: Boolean =
100100
def isAccessible(sym: Symbol): Boolean = ctx.owner.isContainedIn(sym)
101101
def isSub(sym: Symbol): Boolean = ctx.owner.ownersIterator.exists(_.derivesFrom(sym))
102-
val ctor = self.primaryConstructor
102+
val ctor = if defn.isJavaRecordClass(self) then self.javaCanonicalConstructor else self.primaryConstructor
103103
(!ctor.isOneOf(Private | Protected) || isSub(self)) // we cant access the ctor because we do not extend cls
104104
&& (!ctor.privateWithin.exists || isAccessible(ctor.privateWithin)) // check scope is compatible
105105

106106

107107
def companionMirror = self.useCompanionAsProductMirror
108-
if (!self.is(CaseClass)) "it is not a case class"
108+
109+
if (!(self.is(CaseClass) || defn.isJavaRecordClass(self))) "it is not a case class or record class"
109110
else if (self.is(Abstract)) "it is an abstract class"
110111
else if (self.primaryConstructor.info.paramInfoss.length != 1) "it takes more than one parameter list"
111112
else if self.isDerivedValueClass then "it is a value class"
@@ -146,7 +147,7 @@ class SymUtils:
146147
&& (!self.is(Method) || self.is(Accessor))
147148

148149
def useCompanionAsProductMirror(using Context): Boolean =
149-
self.linkedClass.exists && !self.is(Scala2x) && !self.linkedClass.is(Case)
150+
self.linkedClass.exists && !self.is(Scala2x) && !self.linkedClass.is(Case) && !defn.isJavaRecordClass(self)
150151

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

253+
// TODO: I'm convinced that we need to introduce a flag to get the canonical constructor.
254+
// we should also check whether the names are erased in the ctor. If not, we should
255+
// be able to infer the components directly from the constructor.
256+
def javaCanonicalConstructor(using Context): Symbol =
257+
self.info.decls.filter(_.isConstructor).tail.head
258+
252259
// TODO: Check if `Synthetic` is stamped properly
253260
def javaRecordComponents(using Context): List[Symbol] =
254261
self.info.decls.filter(sym => sym.is(Synthetic) && sym.is(Method) && !sym.isConstructor)

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

+4-14
Original file line numberDiff line numberDiff line change
@@ -345,23 +345,13 @@ object PatternMatcher {
345345

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

348-
def isSyntheticJavaRecordUnapply(sym: Symbol) =
349-
// Since the `unapply` symbol is marked as inline, the `Typer` wraps the body of the `unapply` in a separate
350-
// anonymous class. The result type alone is not enough to distinguish that we're calling the synthesized unapply —
351-
// we could have defined a separate `unapply` method returning a Java record somewhere, hence we resort to using
352-
// the `coord`.
353-
sym.is(Synthetic) && sym.isAnonymousClass && {
354-
val resultSym = resultTypeSym
355-
// TODO: Can a user define a separate unapply function in Java?
356-
val unapplyFn = resultSym.linkedClass.info.decl(nme.unapply)
357-
// TODO: This is nasty, can we add an attachment on the anonymous function for a prior link?
358-
defn.isJavaRecordClass(resultSym) && unapplyFn.symbol.coord == sym.coord
359-
}
360-
348+
// TODO: Check Scala -> Java, erased?
349+
def isJavaRecordUnapply(sym: Symbol) = defn.isJavaRecordClass(resultTypeSym)
361350
def tupleSel(sym: Symbol) = ref(scrutinee).select(sym)
362351
def recordSel(sym: Symbol) = tupleSel(sym).appliedToTermArgs(Nil)
363352

364-
if (isSyntheticJavaRecordUnapply(unapp.symbol.owner))
353+
// TODO: Move this to the correct location
354+
if (isJavaRecordUnapply(unapp.symbol.owner))
365355
val components = resultTypeSym.javaRecordComponents.map(recordSel)
366356
matchArgsPlan(components, args, onSuccess)
367357
else if (isSyntheticScala2Unapply(unapp.symbol) && caseAccessors.length == args.length)

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

+11-1
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,12 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
407407
def newTupleMirror(arity: Int): Tree =
408408
New(defn.RuntimeTupleMirrorTypeRef, Literal(Constant(arity)) :: Nil)
409409

410+
def newJavaRecordReflectMirror(tpe: Type) =
411+
ref(defn.JavaRecordReflectMirrorModule)
412+
.select(nme.apply)
413+
.appliedToType(tpe)
414+
.appliedTo(clsOf(tpe))
415+
410416
def makeProductMirror(pre: Type, cls: Symbol, tps: Option[List[Type]]): TreeWithErrors =
411417
val accessors = cls.caseAccessors
412418
val elemLabels = accessors.map(acc => ConstantType(Constant(acc.name.toString)))
@@ -427,6 +433,7 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
427433
}
428434
val mirrorRef =
429435
if cls.useCompanionAsProductMirror then companionPath(mirroredType, span)
436+
else if defn.isJavaRecordClass(cls) then newJavaRecordReflectMirror(cls.typeRef)
430437
else if defn.isTupleClass(cls) then newTupleMirror(typeElems.size) // TODO: cls == defn.PairClass when > 22
431438
else anonymousMirror(monoType, MirrorImpl.OfProduct(pre), span)
432439
withNoErrors(mirrorRef.cast(mirrorType).withSpan(span))
@@ -458,14 +465,17 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
458465
val reason = s"it reduces to a tuple with arity $arity, expected arity <= $maxArity"
459466
withErrors(i"${defn.PairClass} is not a generic product because $reason")
460467
case MirrorSource.ClassSymbol(pre, cls) =>
468+
461469
if cls.isGenericProduct then
462470
if ctx.runZincPhases then
463471
// The mirror should be resynthesized if the constructor of the
464472
// case class `cls` changes. See `sbt-test/source-dependencies/mirror-product`.
465473
val rec = ctx.compilationUnit.depRecorder
466474
rec.addClassDependency(cls, DependencyByMemberRef)
467475
rec.addUsedName(cls.primaryConstructor)
468-
makeProductMirror(pre, cls, None)
476+
val stuff = makeProductMirror(pre, cls, None)
477+
println(stuff._1.show)
478+
stuff
469479
else withErrors(i"$cls is not a generic product because ${cls.whyNotGenericProduct}")
470480
case Left(msg) =>
471481
withErrors(i"type `$mirroredType` is not a generic product because $msg")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package scala.runtime
2+
3+
import java.lang.Record
4+
import java.lang.reflect.Constructor
5+
import scala.reflect.ClassTag
6+
7+
// TODO: Rename to JavaRecordReflectMirror
8+
object JavaRecordMirror:
9+
10+
def apply[T <: Record](clazz: Class[T]): JavaRecordMirror[T] =
11+
val components = clazz.getRecordComponents.nn
12+
val constructorTypes = components.map(_.nn.getType.nn)
13+
val constr = clazz.getDeclaredConstructor(constructorTypes*).nn
14+
new JavaRecordMirror(components.length, constr)
15+
16+
def of[T <: Record : ClassTag]: JavaRecordMirror[T] =
17+
JavaRecordMirror(summon[ClassTag[T]].runtimeClass.asInstanceOf[Class[T]])
18+
19+
// TODO: Is a constructor serializable?
20+
final class JavaRecordMirror[T] private(arity: Int, constr: Constructor[T]) extends scala.deriving.Mirror.Product with Serializable:
21+
22+
override type MirroredMonoType <: Record
23+
24+
final def fromProduct(product: Product): MirroredMonoType =
25+
if product.productArity != arity then
26+
throw IllegalArgumentException(s"expected Product with $arity elements, got ${product.productArity}")
27+
else
28+
// TODO: Check this byte code, we want to unroll to give a happy medium between JIT'ing and having tons of extra classes
29+
val t = arity match
30+
case 0 => constr.newInstance()
31+
case 1 => constr.newInstance(product.productElement(0))
32+
case 2 => constr.newInstance(product.productElement(0), product.productElement(1))
33+
34+
t.nn.asInstanceOf[MirroredMonoType]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import scala.deriving.Mirror
2+
3+
object C:
4+
def useR2: Unit =
5+
summon[Mirror.Of[R2]]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
public record R2(int i, String s) {}

0 commit comments

Comments
 (0)