-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Add support for Java records in pattern matching #26497
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
Changes from 6 commits
b08367e
fce8acb
8821342
0b80d06
92213cf
4ea5d0d
333858b
2fd0f50
96eb58b
d2db163
b097117
977e434
55b4a00
c69605b
ebf2c48
6d0a94c
db9abcc
0332a8c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -185,7 +185,23 @@ object Applications { | |
| (0 until argsNum).map(i => if (i < arity - 1) selectorTypes(i) else elemTp).toList | ||
| end seqSelectors | ||
|
|
||
| /** A utility class that matches results of unapplys with patterns. Two queryable members: | ||
| /** 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 queriable members: | ||
| * val argTypes: List[Type] | ||
| * def typedPatterns(qual: untpd.Tree, typer: Typer): List[Tree] | ||
| * TODO: Move into Applications trait. No need to keep it outside. But it's a large | ||
|
|
@@ -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) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This wraps the array in an immutable 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?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, that's implemented in 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 |
||
| 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)) | ||
| } | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.