-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Move TailRec after Erasure. #5112
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
Move TailRec after Erasure. #5112
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! ☀️
At this point, I still have bootstrap failures locally, but I'd like to see what the CI says. |
We try to avoid using tree attachments unless there's no other choice in Dotty since they're a bit too magic, and using them to communicate across phases seems especially scary. Can we try to find an alternative solution? I suggest simply changing erasure to not drop annotations and check if that breaks anything. |
Yeah, I'm not happy with the tree attachment either. I just wanted to make those tests pass rather than moving them to pending (because, you know, one doesn't just remove tests), but it certainly is open for suggestions. I don't think I have the stomach to try and change erasure right now, though. I suggest we discuss that at the next meeting. |
4eaa795
to
6d8198d
Compare
6d8198d
to
5be1d73
Compare
After a discussion with @odersky today, I added a commit that simply removes the call-site |
bbe0a97
to
b8be1e7
Compare
Rebased. |
test performance please |
performance test scheduled: 1 job(s) in queue, 0 running. |
Performance test finished successfully: Visit http://dotty-bench.epfl.ch/5112/ to see the changes. Benchmarks is based on merging with master (a0b429b) |
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.
The doc at the top of the file needs to be updated. It mentions things about pattern matching and type parameters which are no longer true. Maybe add a paragraph about labeled blocks
val rhs = transformer.transform(dd.rhs) | ||
rewrote = transformer.rewrote | ||
rhs | ||
} | ||
|
||
if (rewrote) { | ||
val dummyDefDef = cpy.DefDef(tree)(rhs = rhsSemiTransformed) | ||
assert(dd.tparams.isEmpty, dd) |
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 is always true
. Maybe rewrite the match case as:
tree match {
- case dd@DefDef(name, Nil, vparams0 :: Nil, tpt, _)
+ case dd@DefDef(name, _, vparams0 :: Nil, tpt, _)
val name = TailLabelName.fresh() | ||
val enclosingClass = method.enclosingClass.asClass | ||
val thisParamType = | ||
if (enclosingClass.is(Flags.Module)) enclosingClass.thisType |
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.
Why do we have this distinction for ModuleClass
?
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.
Because the type of this
in a module class is something like TheModule.this.type
(not TheModule$
, for example) but the type of this
in a normal class is TheClass
. Using anything else fails to Ycheck. I haven't found a way to produce those subtly different things using one uniform call, although I feel like there should be something in the internal API to do so.
As to why it must be TheModule.this.type
and not TheModule$
, I don't know, but apparently that's how it is in dotty.
val vrefs = vrefss.head | ||
val thisRef = vrefs.head | ||
val origMeth = tree.symbol | ||
val origVParams = tree.vparamss.flatten map (_.symbol) |
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.
Isn't this vparams0.map(_.symbol)
? Is the assumption after erasure that there is only one parameter list?
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.
Yes, you're right.
And yes, after erasure all methods have exactly one parameter list (not 0 nor 2+).
@@ -195,84 +208,45 @@ class TailRec extends MiniPhase with FullParameterization { | |||
|
|||
override def transform(tree: Tree)(implicit c: Context): Tree = { | |||
/* A possibly polymorphic apply to be considered for tail call transformation. */ |
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.
Update comment? polymorphic
after erasure?
.substThisUnlessStatic(classSym, thisRef.tpe) | ||
.subst(origVParams, vrefs.tail.map(_.tpe)), | ||
treeMap = { | ||
case tree: This if tree.symbol == classSym => thisRef |
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.
Why was this not needed before?
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 was needed before, but it was handled by fullyParameterizedDef
(along with many other complications for type parameters, which are not needed anymore).
} else { | ||
val receiverIsSuper = (method.name eq sym) && enclosingClass.appliedRef.widen <:< recvWiden | ||
val receiverIsSuper = (method.name eq sym) && enclosingClass.appliedRef.widen <:< prefix.tpe.widenDealias |
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 your change but this is very suspicious: method.name
is a Name
and sym
is a Symbol
. I guess we don't have tests with recursive call to supertype
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 agree. It is suspicious. I'd leave out an investigation to a different PR, though, if you think that's reasonable.
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.
Sure! Do you mind opening an issue, once the PR is merged?
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.
case x: This => (x, x, accArgs, accT, x.symbol) | ||
case x: Ident if x.symbol eq method => (EmptyTree, x, accArgs, accT, x.symbol) | ||
case x => (x, x, accArgs, accT, x.symbol) | ||
def rewriteApply(tree: Apply, sym: Symbol): Tree = { |
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.
rewriteApply
does not need to take the symbol as parameter since the function is called only once and sym
really is tree.fun.symbol
val targs = typeArguments.map(noTailTransform) | ||
val argumentss = arguments.map(noTailTransforms) | ||
def continue = | ||
Apply(noTailTransform(call), arguments) |
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.
If this used a tree copier and noTailTransforms
used mapConserve
, I believe we could save some unnecessary tree copies
if (this.method.owner.isClass) receiver :: arguments | ||
else arguments | ||
|
||
Apply(method, argumentsWithReceiver) |
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.
Should we us a tree copier to properly set the tree position?
tpd.cpy(tree)(method, argumentsWithReceiver)
Otherwise, I (personally) think using our DSL is cleaner:
method.appliedToArgs(argumentsWithReceiver)
if (prefix eq EmptyTree) This(enclosingClass.asClass) | ||
else noTailTransform(prefix) | ||
|
||
val method = Ident(label.termRef) |
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.
ref(label)
?
This used not to be possible because it was too complicated for TailRec to understand trees produced by the pattern matcher. Now that patmat uses `Labeled` blocks instead of label-defs, it is trivial to do so: the argument of a `return` from a labeled block `lab` is in tail position if and only if the labeled block `lab` is itself in tail position. Running TailRec after erasure has two major benefits: * it is much simpler, as it does not have to deal with type parameters, `TypeApply`s, and a bunch of other stuff. * it supports polymorphic tail-recursive calls by construction (recursive calls whose receiver or method has different type parameters). It also has one difficulty: it cannot see the *call-site* `@tailrec` annotations anymore. This is why we add a mini-phase to record such annotations as tree attachments. Having TailRec after erasure will also be necessary to later make it use `Labeled` blocks and loops instead of label-defs itself. Indeed, in that case it will need to declare local `var`s for its term parameters, which would break path-dependent types within the method if it were done before erasure.
This feature does not exist in Scala 2, and was never documented in Scala 3. Its only internal use comes from a swiping commit that added as many `@tailrec` annotations as possible all over the codebase: see be5720c. Supporting that feature became problematic when TailRec was moved after erasure, because we needed an attachment to remember call-site `@tailrec` annotations, which would otherwise be removed by erasure. This commit removes the feature, on the grounds that its cost/benefit is too high.
7b1e8d9
to
b1008d8
Compare
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.
Updated according to your comments :)
val name = TailLabelName.fresh() | ||
val enclosingClass = method.enclosingClass.asClass | ||
val thisParamType = | ||
if (enclosingClass.is(Flags.Module)) enclosingClass.thisType |
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.
Because the type of this
in a module class is something like TheModule.this.type
(not TheModule$
, for example) but the type of this
in a normal class is TheClass
. Using anything else fails to Ycheck. I haven't found a way to produce those subtly different things using one uniform call, although I feel like there should be something in the internal API to do so.
As to why it must be TheModule.this.type
and not TheModule$
, I don't know, but apparently that's how it is in dotty.
val vrefs = vrefss.head | ||
val thisRef = vrefs.head | ||
val origMeth = tree.symbol | ||
val origVParams = tree.vparamss.flatten map (_.symbol) |
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.
Yes, you're right.
And yes, after erasure all methods have exactly one parameter list (not 0 nor 2+).
.substThisUnlessStatic(classSym, thisRef.tpe) | ||
.subst(origVParams, vrefs.tail.map(_.tpe)), | ||
treeMap = { | ||
case tree: This if tree.symbol == classSym => thisRef |
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 was needed before, but it was handled by fullyParameterizedDef
(along with many other complications for type parameters, which are not needed anymore).
} else { | ||
val receiverIsSuper = (method.name eq sym) && enclosingClass.appliedRef.widen <:< recvWiden | ||
val receiverIsSuper = (method.name eq sym) && enclosingClass.appliedRef.widen <:< prefix.tpe.widenDealias |
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 agree. It is suspicious. I'd leave out an investigation to a different PR, though, if you think that's reasonable.
else tpd.cpy.Select(tree)(noTailTransform(tree.qualifier), tree.name) | ||
|
||
case Apply(fun, args) => | ||
case tree@Apply(fun, args) if !fun.isInstanceOf[TypeApply] => |
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.
Yes, for special methods like isInstanceOf
. Those can never be tail-recursive calls since they don't have a definition in trees. It's better not to guard, though. They'll be ignored because isRecursiveCall
will be false
anyway.
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.
LGTM 🎉
@@ -24,7 +24,7 @@ class MixinOps(cls: ClassSymbol, thisPhase: DenotTransformer)(implicit ctx: Cont | |||
name = member.name.stripScala2LocalSuffix, | |||
flags = member.flags &~ Deferred, | |||
info = cls.thisType.memberInfo(member)).enteredAfter(thisPhase).asTerm | |||
res.addAnnotations(member.annotations) | |||
res.addAnnotations(member.annotations.filter(_.symbol != defn.TailrecAnnot)) |
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.
Sorry, I missed this. Is this needed if you move TailRec
after Mixin
within the same group?
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.
Even more so. But right now it's necessary above all for extension methods (for AnyVal
s, which also use this forwarder()
method). In general, when creating a forwarder of any sort, we must remove the @tailrec
annotation.
Previously, no forwarder was ever created before TailRec
ran. But now that TailRec
runs after the creation of extension methods, we must ensure that forwarder
drops @tailrec
).
This used not to be possible because it was too complicated for TailRec to understand trees produced by the pattern matcher. Now that patmat uses
Labeled
blocks instead of label-defs, it is trivial to do so: the argument of areturn
from a labeled blocklab
is in tail position if and only if the labeled blocklab
isitself in tail position.
Running TailRec after erasure has two major benefits:
TypeApply
s, and a bunch of other stuff.It also has one difficulty: it cannot see the call-site
@tailrec
annotations anymore. This is why we add a mini-phase to record such annotations as tree attachments.Having TailRec after erasure will also be necessary to later make it use
Labeled
blocks and loops instead of label-defs itself. Indeed, in that case it will need to declare localvar
s for its term parameters, which would break path-dependent types within the method if it were done before erasure.