Skip to content

Make implicit function variables safe as arguments (fixes #10889) #13750

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

Closed
wants to merge 6 commits into from

Conversation

elfprince13
Copy link

@elfprince13 elfprince13 commented Oct 14, 2021

Address #10889 by adding an optimizer pass to remove synthesized contextual closures if they wrap a context function of the expected type. Add a runtime test to probe for stackoverflow if this optimization is not performed.

Fixes #10889

… contextual closures if they wrap a context function of the expected type. Add a runtime test to probe for stackoverflow if this optimization is not performed.
@elfprince13
Copy link
Author

CLA is now signed.

Copy link
Contributor

@odersky odersky left a comment

Choose a reason for hiding this comment

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

Mostly stylistic comments, and some explanatory comments should be added. Otherwise code LGTM.

import reporting.trace
import config.Printers.{ transforms => debug }

/** Abstract base class of ByNameClosures and ElimByName, factoring out the
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy paste error here. The phase needs an detailed explanation.


def transformArg(arg: Tree, formal: Type): Tree = {
val veryFormal = formal.widenDealias
if(defn.isContextFunctionType(veryFormal) && untpd.isContextualClosure(arg)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Use Scala 3 conditionals in new code. Never use if( without a space.

trace(s"transforming ${tree.show} at phase ${ctx.phase}", show = true) {

def transformArg(arg: Tree, formal: Type): Tree = {
val veryFormal = formal.widenDealias
Copy link
Contributor

Choose a reason for hiding this comment

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

Why veryFormal? Maybe formalw, or simply formal1?

@@ -73,6 +73,7 @@ class Compiler {
new ProtectedAccessors, // Add accessors for protected members
new ExtensionMethods, // Expand methods of value classes with extension methods
new UncacheGivenAliases, // Avoid caching RHS of simple parameterless given aliases
new ElimRedundantContextualClosure, // Unwrapped context closures if the contents is already a context closure of compatible type
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd shorten the name to ElimContextClosures. Also, the comment should be active form.

Suggested change
new ElimRedundantContextualClosure, // Unwrapped context closures if the contents is already a context closure of compatible type
new ElimContextClosures, // Unwrap context closure arguments that contain already a context closure of compatible type
Suggested change
new ElimRedundantContextualClosure, // Unwrapped context closures if the contents is already a context closure of compatible type
new ElimRedundantContextualClosure, // Unwrapped context closures if the contents is already a context closure of compatible type

val bodyIsContextual = defn.isContextFunctionType(underlyingBodyType)
val bodyTypeMatches = TypeComparer.isSubType(underlyingBodyType, veryFormal)
if(bodyIsContextual && bodyTypeMatches) {
body
Copy link
Contributor

Choose a reason for hiding this comment

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

Drop the redundant braces. We tend to use Scala 3 style throughout in new code.

@odersky
Copy link
Contributor

odersky commented Oct 15, 2021

test performance please

@elfprince13
Copy link
Author

Is there a standard methodology I should follow for the performance testing?

@odersky
Copy link
Contributor

odersky commented Oct 15, 2021

That was a command to the bot 😀 Performance test should start automatically. But something seems to be wrong again /cc @anatoliykmetyuk

@elfprince13
Copy link
Author

I believe all comments have now been addressed. =)

@anatoliykmetyuk
Copy link
Contributor

test performance please

@dottybot
Copy link
Member

performance test scheduled: 1 job(s) in queue, 0 running.

@anatoliykmetyuk
Copy link
Contributor

Looks like the bot crashed again. Last fix wasn't addressing the current cause of crash. I've restarted the bot and will investigate more after the weekend.

@dottybot
Copy link
Member

Performance test finished successfully:

Visit https://dotty-bench.epfl.ch/13750/ to see the changes.

Benchmarks is based on merging with master (54ff628)

trace(s"transforming ${tree.show} at phase ${ctx.phase}", show = true) {

def transformArg(arg: Tree, formal: Type): Tree = {
val formal1 = formal.widenDealias
Copy link
Contributor

Choose a reason for hiding this comment

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

In fact, widen is enough here. isContextFunction and isSubType already do dealiasing. (It's better to be consistent here since otherwise the next people looking at the compiler see multiple styles and get confused).

Copy link
Author

Choose a reason for hiding this comment

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

Presumably this applies to underlyingBodyType as well as formal1?

Copy link
Author

Choose a reason for hiding this comment

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

Sorted in 009f18b.

def transformArg(arg: Tree, formal: Type): Tree = {
val formal1 = formal.widenDealias
if defn.isContextFunctionType(formal1) && untpd.isContextualClosure(arg) then
val body = unsplice(closureBody(arg)) match {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it would be better to start like this

unsplice(closureBody(arg)) match
  case Apply(Select(body, nme.apply, _) => 
    ... // transformation code
  case _ =>
    arg

If the argument is not a nested closure there's no need to go through the following tests, right?

Copy link
Author

Choose a reason for hiding this comment

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

closureBody returns arg if arg is not a closure, so we would have to essentially reimplement the closureBody logic inline here to make sure we aren't destructuring a random Apply for no reason. I assumed checking the outer structure first with this if would be more efficient / cleaner.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry, I meant: after the if defn.isContextFunctionType ..., instead of val body = ... continue with what I suggested.

Copy link
Author

Choose a reason for hiding this comment

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

oh, I see now, yes. Should be addressed in 009f18b.

@dwijnand dwijnand assigned odersky and unassigned elfprince13 Nov 8, 2021
@elfprince13
Copy link
Author

@odersky - I believe all requested changes have been addressed.

// (1 byte / frame is conservative even for a z80)
retryN(maxStackSize)(testTxn.asInstanceOf[AtomicOp[Boolean]]) == safeRetryN(maxStackSize)(testTxn)
} catch {
case e:StackOverflowError =>
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't it easier to just probe stack depth with new Throwable().getStackTrace? for n and n+1 recursions.

(Generally wary of tests for SOE and OOM.)

Copy link
Author

Choose a reason for hiding this comment

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

I'm not sure I see what you have in mind, can you be a little bit more explicit?

Copy link
Contributor

Choose a reason for hiding this comment

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

apologies in advance if I'm missing the whole point, I'm still reading the issue. I was thinking of https://github.com/scala/scala/blob/2.13.x/test/junit/scala/collection/IteratorTest.scala#L36 although counting frames is subject to optimizations.

Copy link
Author

Choose a reason for hiding this comment

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

Ah, sure, we could have the return value from testTxn be the current stack depth and then check retryN with different iteration counts. Happy to do that if it's preferable.

@odersky
Copy link
Contributor

odersky commented Dec 3, 2021

Tests are failing after 009f18b, so this needs to be addressed.

@odersky odersky assigned elfprince13 and unassigned odersky Dec 3, 2021
@elfprince13
Copy link
Author

@odersky - do you have an alternative hypothesis as to the cause, besides one of those widenDealiases was actually needed?

@odersky
Copy link
Contributor

odersky commented Dec 3, 2021 via email

@odersky
Copy link
Contributor

odersky commented Jan 3, 2022

We seem to have deeper problems here. I rebased the PR to current master but several tests fail, including run/tagless.scala. And that holds for all commits, not just the last ones.

@som-snytt
Copy link
Contributor

Actually, I took a look and will follow up the PR. The current solution strips too much.

@odersky
Copy link
Contributor

odersky commented Jan 4, 2022

This could be generalized. We could add a general form of eta-reduction

     (x1, ..., xN) => f(x1, ..., xN)     --->    f

if f is a pure expression of function type and the closure is synthetic (i.e. its span is the span of f). Equally for ?=> instead of =>. This might replace the corresponding rule for cbn parameters. Such a miniphase would best be placed after erasure when all such closures have been generated.

@som-snytt
Copy link
Contributor

In https://github.com/lampepfl/dotty/blob/master/tests/pos/i2671.scala, checking isStable helps.

The curious one is def f: (A ?=> B) = ??? where I would expect f to be unimplemented.

The change for CBN was to defer evaluation of the arg; the feature request here is "just an optimization". But I'm willing to call it eta-reduction.

The missed opportunity in Wonder Woman: 1984 was not giving Diana an assistant at the museum named "Etta Reduction".

@odersky
Copy link
Contributor

odersky commented Jan 4, 2022

The curious one is def f: (A ?=> B) = ??? where I would expect f to be unimplemented.

Eta reduction only applies if f is a function value, not a method. f by itself is not a value anyway.

odersky added a commit to dotty-staging/dotty that referenced this pull request Jan 4, 2022
Fixes scala#10889. Alternative to scala#13750, from which the test is taken.
olsdavis pushed a commit to olsdavis/dotty that referenced this pull request Apr 4, 2022
Fixes scala#10889. Alternative to scala#13750, from which the test is taken.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

rewrite rules mean implicit function variables cannot be safely passed as arguments
5 participants