-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Add support for generic tuples #4938
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
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hello, and thank you for opening this PR! 🎉
All contributors have signed the CLA, thank you! ❤️
Commit Messages
We want to keep history, but for that to actually be useful we have
some rules on how to format our commit messages (relevant xkcd).
Please stick to these guidelines for commit messages:
- Separate subject from body with a blank line
- When fixing an issue, start your commit message with
Fix #<ISSUE-NBR>:
- Limit the subject line to 72 characters
- Capitalize the subject line
- Do not end the subject line with a period
- Use the imperative mood in the subject line ("Add" instead of "Added")
- Wrap the body at 80 characters
- Use the body to explain what and why vs. how
adapted from https://chris.beams.io/posts/git-commit
Have an awesome day! ☀️
This PR is currently blocked since dotty/library is compiled with Scala-2. I tried several workarounds but there's basically no way to do the kind of typelevel computation we want with Scala-2 not throwing up. So we need to the full bootstrap to be completed first. |
In what kind of way does Scala 2 explode? Keep in mind that we could have two versions of "Tuple.scala", one compiled by Scala 2 and one compiled by Dotty, sbt makes this fairly easy to do. |
The blockers right now are
@`rewrite` private def _size(xs: Tuple): Int = //rewrite
xs match {
case _: Unit => 0
case _: *:[_, xs1] => _size(erasedValue[xs1]) + 1
} because it does not know that Both would be fixed if I could define a Dotty-only Tuple class. How do I do it? Put it in scalaShadowing? |
It requires some sbt trickery, I can push a commit to this PR with an example and you can check if that works for you. |
OK, thanks! |
See the latest commit, you should be able to put any dotty-only files in library/src-scala3 |
library/src/scala/TupleXXL.scala
Outdated
override def toString = elems.mkString("(", ",", ")") | ||
override def hashCode = getClass.hashCode * 41 + elems.deep.hashCode | ||
override def equals(that: Any) = that match { | ||
case that: TupleXXL => this.elems.deep.equals(that.elems.deep) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.deep
has been removed from 2.13 (scala/scala#6912), but you can use Arrays.deepEquals instead: https://docs.oracle.com/javase/7/docs/api/java/util/Arrays.html#deepEquals(java.lang.Object[],%20java.lang.Object[])
Cool! the src-scala2/src-scala3 seems to have done the trick! |
@smarter I found an issue: sbt:dotty> dotty-bootstrapped/clean |
@odersky I just tried it on the latest commit of this branch and couldn't reproduce the error. Does it happen if you do both |
Also try restarting sbt maybe. |
Now it works for me as well. Sorry for the false alarm! |
Update on performance: object Test extends App {
val xs0 = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)
assert(xs0(15) == 16)
// 2.753s
val xs1 = xs0 ++ xs0
assert(xs1(31) == 16)
// 3.426s
val xs2 = xs1 ++ xs1
assert(xs2(63) == 16)
// 4.082s
val xs3 = xs2 ++ xs2
assert(xs3(127) == 16)
// 4.786s
val xs4 = xs3 ++ xs3
assert(xs4(255) == 16)
// 6.543s
val xs5 = xs4 ++ xs4
assert(xs5(511) == 16)
// 15.041s
} Times behind One has has to change -Xmax-inlines accordingly. E.g. for the last block
Summary: it would be good if we could improve this. |
Code size: The |
|
What could be done to make the compile times faster? The problem are extensive type-level rewritings. I.e. in the last block we rewrite 256-512 step concat trees 256 times (times some constant factor because the same trees are traversed and rewritten several times in each inlining step). That's a lot of nodes to rewrite and typecheck, and little to show for it, since in the end the result of these rewritings is a small generic tree that works with TupleXXL. The rewritings are needed to
One major savings would be if we would compute the type as a type and not as a recursive type tree. So that means lifting term computations to types, analogous to the "TypeOf" approach. If we can do this, we could compute the size also as a (constant) type, and thereby save all typelevel computations on large terms. Typelevel computations on terms up to size 22 are still needed to |
@liufengyun We get a wrong exhaustivity warning in run/tuples1.scala. It has to do with TupleXXL matching. Can you take a look? |
test performance please |
performance test scheduled: 7 job(s) in queue, 1 running. |
Performance test finished successfully: Visit http://dotty-bench.epfl.ch/4938/ to see the changes. Benchmarks is based on merging with master (61cba97) |
@odersky OK, I'll investigate. |
There are two problems with object TupleXXL {
def unapplySeq(x: TupleXXL): Option[Seq[Any]] = Some(x.elems.toSeq)
} First, we need to change the return type to Second, after typer, we get a pattern match as follows: Test.x23 match
{
case
TupleXXL.unapplySeq(x1 @ _, x2 @ _, x3 @ _, x4 @ _, x5 @ _, x6 @ _,
x7 @ _
, x8 @ _, x9 @ _, x10 @ _, x11 @ _, x12 @ _, x13 @ _, x14 @ _, x15 @ _
,
x16 @ _, x17 @ _, x18 @ _, x19 @ _, x20 @ _, x21 @ _, x22 @ _, x23 @ _
)
: TupleXXL There is no information from |
Right. We do not have enough information in TupleXXL to be able to tell whether the warning is real or not. So I believe we have to suppress it. Or we have to special-case |
tests/run/i4803d/App_2.scala
Outdated
@@ -10,7 +10,7 @@ object Test { | |||
println(power2(x3)) | |||
} | |||
|
|||
rewrite def power2(x: Double) = { | |||
def power2(x: Double) = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given the possible future changes in rewrite
we should not do this change in the tests as it does not represent the use case from #4803 anymore. Instead, move it to disabled tests.
/** Add a `Tuple` as a parent to `Unit`. | ||
* Add the right `*:` instance as a parent to Tuple1..Tuple22 | ||
*/ | ||
def fixTupleCompleter(cls: ClassSymbol): Unit = cls.infoOrCompleter match { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This logic looks is quite convoluted. Are there more direct approaches that do not require patching the completers?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I looked hard but did not find anything that would work better than patching the completers. The syntheticParent
code can be simplified using an utility method that I introduced in the subsequent PR #4964. I'll do the simplification there.
@@ -376,7 +379,7 @@ object Erasure { | |||
* on selections `e.m`, where `OT` is the type of the owner of `m` and `ET` | |||
* is the erased type of the selection's original qualifier expression. | |||
* | |||
* e.m1 -> e.m2 if `m1` is a member of Any or AnyVal and `m2` is | |||
* e.m1 -> e.m2 if `m1` is a member of a class that erases to object and `m2` is |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo object
-> Object
ctx.error(ex"implementation restriction: nested rewrite methods are not supported", inlined.pos) | ||
if (inlined.name == nme.unapply && tupleArgs(body).isEmpty) | ||
ctx.warning( | ||
em"rewrite unapply method can be rewritten only if its tight hand side is a tuple (e1, ..., eN)", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo tight
-> right
|
||
final class TupleXXL private (es: Array[Object]) { | ||
override def toString = elems.mkString("(", ",", ")") | ||
override def hashCode = getClass.hashCode * 41 + deepHashCode(elems) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure what getClass.hashCode * 41
gives to the hash.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's just to distinguish TupleXXL
hashes from other hashes over elems
.
/** The arity of this tuple type, which can be made up of Unit, TupleX and `*:` pairs, | ||
* or -1 if this is not a tuple type. | ||
*/ | ||
def tupleArity(implicit ctx: Context): Int = self match { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could be tailtec
:
def tupleArity(implicit ctx: Context): Int = {
def tupleArity(tp: Type, arity: Int): Int = tp match {
case AppliedType(tycon, _ :: tl :: Nil) if tycon.isRef(defn.PairClass) =>
tupleArity(tl, arity + 1)
case tp1 =>
if (tp1.isRef(defn.UnitClass)) arity
else if (defn.isTupleClass(tp1.classSymbol)) arity + tp1.dealias.argInfos.length
else -1
}
tupleArity(self, 0)
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe that version behaves differently for *: sequences that do not end in Unit
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe, still there exists a correct tailrec version of the method. Can be revisited later.
These types are injected as parents of Tuple1,...,Tuple22, but are eliminated again at erasure. To support them properly before full dotty bootstrap we needed a @`rewrite` annotation that erases the annotated method (unlike `@forceInline`, which generates code).
And replace Tuple.scala with a Scala 2 implementation and a Scala 3 implementation. The Scala 2 implementaion of Tuple.scala is not strictly necessary but could be useful if non-version-specific library sources end up referencing scala.Tuple.
Erased code does not keep full Inlined trees. The previous version converted the Inlined tree back to the call, but this can expose leaks (since calls are not traversed for references). We now keep the Inlined node, so that its call part can be subsequently simplified.
With the Scala 2/3 split, we don't need it anymore.
Does not rely anymore on `deep`, which is dropped in Scala 2.14.
Was erased to Object before, but this loses precision and breaks binary compatibility with Scala 2.
Currently, this is not exploited but some rewrites might become more effective that way in the future.
There's a difference to be made between projections of instance creations in a preceding val on the one hand and instance creations in a def or directly written creations on the other hand.
Currently only unapplys that return a tuple are supported.
- no nested rewrites - calls to rewrite unapplys only in rewrite code
tuples1.scala is explainable and harmless. The other two (i4758 and t7426) give a stale symbol but in my setup they do that already on master. I wonder how they slipped through the CI.
@@ -33,6 +33,7 @@ i4125.scala | |||
i4167 | |||
i4586.scala | |||
i4734 | |||
i4758 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They slipped through the CI because before #5059 the CI didn't run -Ytest-pickler
on pos and run tests..
Based on #4927.