Skip to content
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
26 changes: 26 additions & 0 deletions compiler/src/dotty/tools/dotc/ast/TreeInfo.scala
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,32 @@ trait TreeInfo[T <: Untyped] { self: Trees.Instance[T] =>
case _ => None
case _ => None
end WitnessNamesAnnot

/** Constructor and extractor for `annotation.internal.JavaRecordFields(isVararg, name_1, ..., name_n)`
* represented as an untyped or typed tree.
*/
object JavaRecordFieldsAnnot:
def tpdTree(isVararg: Boolean, names: List[String])(using Context): tpd.Tree =
tpd.New(
defn.JavaRecordFieldsAnnot.typeRef,
List(
tpd.Literal(Constant(isVararg)),
tpd.SeqLiteral(names.map(n => tpd.Literal(Constant(n))), tpd.TypeTree(defn.StringType))
)
)

def apply(isVararg: Boolean, names: List[String])(using Context): untpd.Tree =
untpd.TypedSplice(tpdTree(isVararg, names))

def unapply(tree: Tree)(using Context): Option[(Boolean, List[TermName])] =
unsplice(tree) match
case Apply(Select(New(tpt: tpd.TypeTree), nme.CONSTRUCTOR), Literal(Constant(isVararg: Boolean)) :: SeqLiteral(elems, _) :: Nil)
if tpt.tpe.classSymbol == defn.JavaRecordFieldsAnnot =>
val names = elems.map:
case Literal(Constant(str: String)) => str.toTermName
Some((isVararg, names))
case _ => None
end JavaRecordFieldsAnnot
}

trait UntypedTreeInfo extends TreeInfo[Untyped] { self: Trees.Instance[Untyped] =>
Expand Down
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 @@ -1129,6 +1129,7 @@ class Definitions {
@tu lazy val UnusedAnnot: ClassSymbol = requiredClass("scala.annotation.unused")
@tu lazy val UnrollAnnot: ClassSymbol = requiredClass("scala.annotation.unroll")
@tu lazy val NativeAnnot: ClassSymbol = requiredClass("scala.native")
@tu lazy val JavaRecordFieldsAnnot: ClassSymbol = requiredClass("scala.annotation.internal.JavaRecordFields")
@tu lazy val RepeatedAnnot: ClassSymbol = requiredClass("scala.annotation.internal.Repeated")
@tu lazy val RuntimeCheckedAnnot: ClassSymbol = requiredClass("scala.annotation.internal.RuntimeChecked")
@tu lazy val SourceFileAnnot: ClassSymbol = requiredClass("scala.annotation.internal.SourceFile")
Expand Down
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/core/StdNames.scala
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ object StdNames {
final val MethodParametersATTR: N = "MethodParameters"
final val LineNumberTableATTR: N = "LineNumberTable"
final val LocalVariableTableATTR: N = "LocalVariableTable"
final val RecordATTR: N = "Record"
final val RuntimeVisibleAnnotationATTR: N = "RuntimeVisibleAnnotations" // RetentionPolicy.RUNTIME
final val RuntimeInvisibleAnnotationATTR: N = "RuntimeInvisibleAnnotations" // RetentionPolicy.CLASS
final val RuntimeParamAnnotationATTR: N = "RuntimeVisibleParameterAnnotations" // RetentionPolicy.RUNTIME (annotations on parameters)
Expand Down
2 changes: 2 additions & 0 deletions compiler/src/dotty/tools/dotc/core/SymUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,8 @@ class SymUtils:
|| isDefaultArgumentOfCheckedMethod
|| (!self.is(Package) && checkOwner(self.owner))

def isJavaRecord(using Context) = self.is(JavaDefined) && self.derivesFrom(defn.JavaRecordClass)

/** The declared self type of this class, as seen from `site`, stripping
* all refinements for opaque types.
*/
Expand Down
21 changes: 21 additions & 0 deletions compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,9 @@ final class ClassfileParser(
private var currentClassName: SimpleName = uninitialized // JVM name of the current class
private var classTParams: Map[Name, Symbol] = Map()

// descriptors of the constructors with the ACC_VARARGS flag, see `tpnme.RecordATTR`
private var varargsConstructors: Set[String] = Set.empty

private val Scala2UnpicklingMode = Mode.Scala2Unpickling
private var classfileVersion: Header.Version = Header.Version.Unknown

Expand Down Expand Up @@ -446,6 +449,8 @@ final class ClassfileParser(
val preName = pool.getName(in.nextChar)
if (!sflags.isOneOf(Flags.PrivateOrArtifact) || preName.name == nme.CONSTRUCTOR) {
val sig = pool.getExternalName(in.nextChar).value
if preName.name == nme.CONSTRUCTOR && (jflags & JAVA_ACC_VARARGS) != 0 then
varargsConstructors += sig
val completer = MemberCompleter(preName.name, jflags, sig)
val member = newSymbol(
getOwner(jflags), preName.name, sflags, completer,
Expand Down Expand Up @@ -1039,6 +1044,22 @@ final class ClassfileParser(
res.permittedSubclasses ::= childName
}

case tpnme.RecordATTR =>
// JVMS 4.7.30: each record component has a name, a descriptor, and attributes
val components = List.fill(in.nextChar):
val name = pool.getName(in.nextChar).value
val descriptor = pool.getExternalName(in.nextChar).value
skipAttributes()
(name, descriptor)
val (names, descriptors) = components.unzip
// JLS 8.10.4: the canonical constructor's descriptor is the concatenation of the component
// descriptors, no other constructor can have that descriptor. It is vararg if the record is.
val canonicalConstructor = descriptors.mkString("(", "", ")V")
val isVararg = varargsConstructors.contains(canonicalConstructor)
// Record the component names and whether it's vararg, see `Applications.javaRecordFields`
res.annotations ::= Annotation.deferredSymAndTree(defn.JavaRecordFieldsAnnot):
JavaRecordFieldsAnnot.tpdTree(isVararg, names)

case _ =>
in.skip(attrLen)
}
Expand Down
11 changes: 10 additions & 1 deletion compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -897,6 +897,11 @@ object JavaParsers {
fieldsByName -= name
end for

def isVarargComponent(tpt: Tree) = tpt match
case PostfixOp(_, Ident(tpnme.raw.STAR)) => true
case _ => false
val isVararg = header.lastOption.exists(v => isVarargComponent(v.tpt))

// accessor for record's vararg field (T...) returns array type (T[])
def adaptVarargsType(tpt: Tree) = tpt match
case PostfixOp(tpt2, Ident(tpnme.raw.STAR)) => arrayOf(tpt2)
Expand All @@ -922,8 +927,12 @@ object JavaParsers {
tparams = tparams,
needsDummyConstr = true
)
).withMods(mods.withFlags(Flags.JavaDefined | Flags.Final))
).withMods(mods
.withFlags(Flags.JavaDefined | Flags.Final)
// Record the component names and whether it's vararg, see `Applications.javaRecordFields`
.withAddedAnnotation(ast.untpd.JavaRecordFieldsAnnot(isVararg, header.map(_.name.toString))))
}

addCompanionObject(statics, recordTypeDef)
end recordDecl

Expand Down
9 changes: 5 additions & 4 deletions compiler/src/dotty/tools/dotc/transform/InlinePatterns.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,18 @@ import scala.collection.mutable.ListBuffer

/** Rewrite an application
*
* {new { def unapply(x0: X0)(x1: X1,..., xn: Xn) = b }}.unapply(y0)(y1, ..., yn)
* {new { def unapply[T1, ..., Tm](x0: X0)(x1: X1,..., xn: Xn) = b }}.unapply[S1, ..., Sm](y0)(y1, ..., yn)
*
* where
*
* - the method is `unapply` or `unapplySeq`
* - the method does not have type parameters
*
* to
*
* [xi := yi]b
* [Ti := Si, xi := yi]b
*
* This removes placeholders added by inline `unapply`/`unapplySeq` patterns.
* This removes placeholders added by inline `unapply`/`unapplySeq` patterns
* and the `unapply`/`unapplySeq` methods synthesized for Java record patterns.
*/
class InlinePatterns extends MiniPhase:
import ast.tpd.*
Expand Down Expand Up @@ -51,6 +51,7 @@ class InlinePatterns extends MiniPhase:
def unapply(app: Tree): (Tree, List[List[Tree]]) =
app match
case Apply(App(fn, argss), args) => (fn, argss :+ args)
case TypeApply(App(fn, argss), targs) => (fn, argss :+ targs)
case _ => (app, Nil)

// TODO merge with BetaReduce.scala
Expand Down
23 changes: 19 additions & 4 deletions compiler/src/dotty/tools/dotc/transform/patmat/Space.scala
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,12 @@ object SpaceEngine {
}
}

/** If `unapp` is the synthetic extractor of a Java record, the record type, otherwise None */
private def javaRecordUnapplyParam(unapp: TermRef)(using Context): Option[Type] =
if unapp.symbol.is(Synthetic) then
unapp.widen.firstParamTypes.headOption.filter(_.classSymbol.isJavaRecord)
else None

/** Is the unapply or unapplySeq irrefutable?
* @param unapp The unapply function reference
*/
Expand All @@ -323,6 +329,7 @@ object SpaceEngine {
|| (unapp.symbol.is(Synthetic) && unapp.symbol.owner.linkedClass.is(Case)) // scala2 compatibility
|| unapplySeqTypeElemTp(unappResult).exists // only for unapplySeq
|| isProductMatch(unappResult.stripNamedTuple, argLen)
|| javaRecordUnapplyParam(unapp).isDefined // a Java record's extractor always matches
|| extractorMemberType(unappResult, nme.isEmpty, NoSourcePosition) <:< ConstantType(Constant(false))
|| unappResult.derivesFrom(defn.NonEmptyTupleClass)
|| unapp.symbol == defn.TupleXXL_unapplySeq // Fixes TupleXXL.unapplySeq which returns Some but declares Option
Expand Down Expand Up @@ -556,9 +563,15 @@ object SpaceEngine {
def isStable(tp: TermRef) =
!tp.symbol.is(ExtensionMethod) // The "prefix" of an extension method may be, but the receiver isn't, so exclude
&& tp.prefix.isStable
// always assume two TypeTest[S, T].unapply are the same if they are equal in types
(isStable(tp1) && isStable(tp2) || tp1.symbol == defn.TypeTest_unapply)
&& tp1 =:= tp2
// The synthetic extractor of a Java record lives in a fresh anonymous class for
// each pattern, so its prefix is neither stable nor equal across patterns. Treat
// two such extractors as the same when they unapply the same record type.
(javaRecordUnapplyParam(tp1), javaRecordUnapplyParam(tp2)) match
case (Some(rec1), Some(rec2)) => rec1 =:= rec2
case _ =>
// always assume two TypeTest[S, T].unapply are the same if they are equal in types
(isStable(tp1) && isStable(tp2) || tp1.symbol == defn.TypeTest_unapply)
&& tp1 =:= tp2
}

/** Return term parameter types of the extractor `unapp`.
Expand Down Expand Up @@ -892,6 +905,8 @@ object SpaceEngine {
else if tp.isRef(defn.ConsType.symbol) then
val body = params.map(doShow(_, flattenList = true)).filter(_.nonEmpty).mkString(", ")
if flattenList then body else s"List($body)"
else if tp.classSymbol.isJavaRecord then
tp.typeConstructor.show + params.map(doShow(_)).mkString("(", ", ", ")")
else
val isUnapplySeq = fun.symbol.name eq nme.unapplySeq
val paramsStr = params.map(doShow(_, flattenList = isUnapplySeq)).mkString("(", ", ", ")")
Expand Down Expand Up @@ -923,7 +938,7 @@ object SpaceEngine {
}) ||
tpw.isRef(defn.BooleanClass) ||
classSym.isAllOf(JavaEnum) ||
classSym.is(Case) || tpw.isNamedTupleType ||
classSym.is(Case) || classSym.isJavaRecord || tpw.isNamedTupleType ||
(tpw.isInstanceOf[TypeRef] && {
val tref = tpw.asInstanceOf[TypeRef]
tref.isUpperBoundedAbstract && isCheckable(tref.info.hiBound)
Expand Down
104 changes: 102 additions & 2 deletions compiler/src/dotty/tools/dotc/typer/Applications.scala
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,22 @@ object Applications {
(0 until argsNum).map(i => if (i < arity - 1) selectorTypes(i) else elemTp).toList
end seqSelectors

/** The component names of the Java record type `tp`, and whether its last
* component is a repeated (vararg) parameter, from its
* `@JavaRecordFields` annotation. The annotation is attached by
* `JavaParsers.recordDecl` (from the record header) and by the
* `ClassfileParser` (from the `Record` classfile attribute); since
* annotations are pickled, it is also available on record symbols
* unpickled from TASTy in pipelined compilation.
*/
def javaRecordFields(tp: Type)(using Context): (Boolean, List[Name]) =
tp.classSymbol.getAnnotation(defn.JavaRecordFieldsAnnot) match
case Some(annot) =>
annot.tree match
case JavaRecordFieldsAnnot(isVararg, names) => (isVararg, names)
case _ => (false, Nil)
case None => (false, Nil)

/** A utility class that matches results of unapplys with patterns. Two queryable members:
* val argTypes: List[Type]
* def typedPatterns(qual: untpd.Tree, typer: Typer): List[Tree]
Expand Down Expand Up @@ -1867,16 +1883,100 @@ trait Applications extends Compatibility {
}
}

// If `qual` denotes a Java record class, its class symbol, otherwise None
def javaRecordClass(qual: untpd.Tree): Option[ClassSymbol] = qual match
case qual: untpd.RefTree =>
val nestedCtx = ctx.fresh.setNewTyperState()
val typeTree = typedType(untpd.rename(qual, qual.name.toTypeName))(using nestedCtx)
typeTree.tpe.classSymbol match
case cls: ClassSymbol if cls.isJavaRecord && !nestedCtx.reporter.hasErrors => Some(cls)
case _ => None
case _ => None

/** For Java record, generate synthetic unapply/unapplySeq:
* ```
* {
* class $anon:
* def unapply[...](x: JavaRecord[...]): (T_1, ..., T_n) = (x.f_1(), ..., x.f_n())
* new $anon
* }.unapply
* ```
* For a record with no components the result type is `Boolean` and the body is `true`.
*
* For a vararg record - Rec(T_1, ..., T_n, T*) - generate unapplySeq.
* The vararg component is exposed through `Array.UnapplySeqWrapper`. The result type is:
* - Array.UnapplySeqWrapper[T] when n = 0
* - (T_1, ..., T_n, Array.UnapplySeqWrapper[T]) when n > 0
*/
def javaRecordUnapply(recCls: ClassSymbol): Tree =
val recType = recCls.typeRef
val (isVararg, fields) = javaRecordFields(recType)

def methType(recTp: Type) =
val componentTypes = fields.map: name =>
recTp.member(name).suchThat(_.paramSymss == List(Nil)).info.resultType

val resType =
// For `Rec()` we do `Boolean`
if componentTypes.isEmpty then defn.BooleanType
else if isVararg then
val defn.ArrayOf(elemType) = componentTypes.last.runtimeChecked
val wrapperType = defn.ArrayModuleClass.requiredType("UnapplySeqWrapper").typeRef.appliedTo(elemType)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if about the coding standards, probably we should pull out the UnapplySeqWrapper symbol to Definitions.

Also, @sjrd, any reservations about the compiler emitting references to scala.Array.UnapplySeqWrapper?

// For `Rec(T*)` we do `Array.UnapplySeqWrapper[T]`
if componentTypes.length == 1 then wrapperType
// For `Rec(T1, ..., Tn, T*)` we do `(T1, ..., Tn, Array.UnapplySeqWrapper[T])`
else defn.tupleType(componentTypes.init :+ wrapperType)
// For `Rec(T1, ..., Tn)` we do `(T1, ..., Tn)`
else defn.tupleType(componentTypes)
MethodType(List(nme.x_0), List(recTp), resType)

val tparams = recCls.typeParams
val unapplyInfo =
if tparams.isEmpty then
methType(recType)
else
PolyType(tparams.map(_.name))(
pt => tparams.map(_.info.subst(tparams, pt.paramRefs).bounds),
pt => methType(recType.appliedTo(pt.paramRefs))
)
val methName = if isVararg then nme.unapplySeq else nme.unapply
val anon = AnonClass(ctx.owner, List(defn.ObjectType), coord = tree.span) { cls =>
val unapplySym = newSymbol(cls, methName, Synthetic | Method, unapplyInfo, coord = tree.span).entered
val unapplyDef = DefDef(unapplySym.asTerm, paramss =>
val x0 = paramss.last.last
def accessor(field: Name) = x0.select(field, _.paramSymss == List(Nil)).appliedToArgs(Nil)
if fields.isEmpty then Literal(Constant(true))
else if isVararg then
val lastField = accessor(fields.last)
val defn.ArrayOf(lastElemType) = lastField.tpe.runtimeChecked
val lastFieldSeq = ref(defn.ArrayModule.requiredMethod(nme.unapplySeq))
.appliedToType(lastElemType).appliedTo(lastField)
if fields.length == 1 then lastFieldSeq
else tupleTree(fields.init.map(accessor) :+ lastFieldSeq)
else tupleTree(fields.map(accessor))
)
List(unapplyDef)
}

trySelectUnapply(untpd.TypedSplice(anon)):
(sel, state) => reportErrors(sel, state)
end javaRecordUnapply

def tryJavaRecordUnapply(qual: untpd.Tree)(fallback: => Tree): Tree =
javaRecordClass(qual) match
case Some(recCls) => javaRecordUnapply(recCls)
case None => fallback

/** Produce a typed qual.unapply or qual.unapplySeq tree, or
* else if this fails follow a type alias and try again.
*/
var unapplyFn =
trySelectUnapply(qual) {
(sel, state) =>
val qual1 = followTypeAlias(qual)
if (qual1.isEmpty) reportErrors(sel, state)
if (qual1.isEmpty) tryJavaRecordUnapply(qual)(reportErrors(sel, state))
else trySelectUnapply(qual1) {
(_, state) => reportErrors(sel, state)
(_, state) => tryJavaRecordUnapply(qual)(reportErrors(sel, state))
}
}

Expand Down
4 changes: 1 addition & 3 deletions compiler/src/dotty/tools/dotc/typer/Namer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -976,8 +976,6 @@ class Namer { typer: Typer =>
*/
private def invalidateIfClashingSynthetic(denot: SymDenotation): Unit =

def isJavaRecord(owner: Symbol) =
owner.is(JavaDefined) && owner.derivesFrom(defn.JavaRecordClass)

def isCaseClassOrCompanion(owner: Symbol) =
owner.isClass && {
Expand All @@ -1003,7 +1001,7 @@ class Namer { typer: Typer =>
)
||
// remove synthetic constructor or method of a java Record if it clashes with a non-synthetic constructor
(isJavaRecord(denot.owner)
(denot.owner.isJavaRecord
&& denot.is(Method)
&& denot.owner.unforcedDecls.lookupAll(denot.name).exists(c => c != denot.symbol && c.info.matches(denot.info))
)
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3140,7 +3140,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
val canBeInvalidated: Boolean =
sym.is(Synthetic)
&& (desugar.isRetractableCaseClassMethodName(sym.name) ||
(sym.owner.is(JavaDefined) && sym.owner.derivesFrom(defn.JavaRecordClass) && sym.is(Method)))
(sym.owner.isJavaRecord && sym.is(Method)))
assert(canBeInvalidated)
sym.owner.info.decls.openForMutations.unlink(sym)
EmptyTree
Expand Down
Loading
Loading