Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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 @@ -1105,6 +1105,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 @@ -272,6 +272,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
24 changes: 24 additions & 0 deletions compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1039,6 +1039,30 @@ 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 _ = in.nextChar
skipAttributes()
name
// Record the component names and whether it's vararg, see `Applications.javaRecordFields`
res.annotations ::= Annotation.deferredSymAndTree(defn.JavaRecordFieldsAnnot):
val recSym = classRoot.symbol
val componentTypes = components.map: name =>
recSym.info.member(termName(name)).suchThat(_.paramSymss == List(Nil)).info.finalResultType
// The vararg bit is ACC_VARARGS on the canonical constructor, found by matching component types
def isCanonical(ctor: Symbol): Boolean =
ctor.info.stripPoly match
case mt: MethodType if mt.paramInfos.length == componentTypes.length =>
mt.paramInfos.zip(componentTypes).forall:
case (param, defn.ArrayOf(elem)) if param.isRepeatedParam => param.argInfos.head =:= elem
case (param, component) => param =:= component
case _ => false
val isVararg =
recSym.info.decls.lookupAll(nme.CONSTRUCTOR).find(isCanonical).exists(_.info.isVarArgsMethod)
Comment thread
Florian3k marked this conversation as resolved.
Outdated
JavaRecordFieldsAnnot.tpdTree(isVararg, components)

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 @@ -896,6 +896,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 @@ -921,8 +926,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
102 changes: 100 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 @@ -1863,16 +1879,98 @@ 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, with return type:
* - Seq[T] when n = 0
* - (T_1, ..., T_n, Seq[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 seqType = defn.SeqType.appliedTo(elemType)
// For `Rec(T*)` we do `Seq[T]`
if componentTypes.length == 1 then seqType
// For `Rec(T1, ..., Tn, T*)` we do `(T1, ..., Tn, Seq[T])`
else defn.tupleType(componentTypes.init :+ seqType)
// 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 = wrapArray(lastField, lastElemType)

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.

This wraps the array in an immutable Seq, which can be observed.

scala> val a = A("x", "y", "z")
val a: A = A[x=x, xs=[Ljava.lang.String;@2ca50ae3]

scala> val xs = a.xs
val xs: Array[String] = Array("y", "z")

scala> val sq = a match { case A(_, sq*) => sq }
val sq: Seq[String] = ArraySeq("y", "z")

scala> xs(0) = "buh"

scala> sq
val res0: Seq[String] = ArraySeq("buh", "z")

I'm not sure if we should keep it this way, or always clone the array. Should I ask in core?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Well, it'd be consistent with pattern matching on arrays.

scala> val xs = Array("x", "y", "z")
val xs: Array[String] = Array(x, y, z)
                                                                                                                    
scala> val ys = xs match { case Array(r*) => r }
val ys: Seq[String] = ArraySeq(x, y, z)
                                                                                                                    
scala> val zs = xs match { case Array(_, r*) => r }
val zs: Seq[String] = ArraySeq(y, z)
                                                                                                                    
scala> xs(1) = "buh"
                                                                                                                    
scala> ys
val res0: Seq[String] = ArraySeq(x, y, z)
                                                                                                                    
scala> zs
val res1: Seq[String] = ArraySeq(y, z)

But I think you can ask

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.

Right, that's implemented in UnapplySeqWrapper. So let's clone as well.

Perhaps we can have a similar wrapper? It would have to accept the initial non-varargs params as well. Or we keep the tuple and use array.toSeq.

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 @@ -3135,7 +3135,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
34 changes: 34 additions & 0 deletions compiler/test/dotty/tools/backend/jvm/DottyBytecodeTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2087,6 +2087,40 @@ class DottyBytecodeTests extends DottyBytecodeTest {
assert(bridge.signature == null, "vararg bridges should not have generic signatures")
}
}

/** Java record patterns compile to plain accessor calls: the synthesized identity
* `unapply` and the anonymous class hosting it are eliminated by InlinePatterns,
* for monomorphic and polymorphic records alike.
*/
@Test def javaRecordPatternsAreInlined = {
val recJava = "public record Rec(int x, String y) {}"
val recGenJava = "public record RecGen<T>(int x, T y) {}"
val source =
"""class Test {
| def mono(r: Rec): String = r match {
| case Rec(i, s) => s * i
| }
| def poly(r: RecGen[String]): String = r match {
| case RecGen(i, s) => s * i
| }
|}
""".stripMargin

checkBCode(List(source), List(recJava, recGenJava)) { dir =>
val classfiles = getGeneratedClassfiles(dir).map(_._1)
assert(!classfiles.exists(_.contains("$anon")),
s"no anonymous classes should be generated for record patterns, found: $classfiles")

val clsNode = loadClassNode(lookupClass(dir, "Test.class"))
for methName <- List("mono", "poly") do
val meth = getMethod(clsNode, methName)
val invokes = instructionsFromMethod(meth).collect { case i: Invoke => i.name }
assert(!invokes.contains("unapply"),
s"no unapply call should remain in $methName, found calls to: $invokes")
assert(invokes.contains("x") && invokes.contains("y"),
s"$methName should call the record's accessors directly, found calls to: $invokes")
}
}
}

object invocationReceiversTestCode {
Expand Down
Loading