-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Closed
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
e30dffa
Address lampepfl/dotty#10889 by adding an optimizer pass to remove sy…
elfprince13 a54bfd4
Stylistic changes per feedback on #13750
elfprince13 54f50e5
Renamed ElimRedundantContextualClosure to ElimContextClosures per fee…
elfprince13 9133065
Remove unused method leftover from copy-and-pasting
elfprince13 e76f078
Apply suggestions from code review (#13750)
elfprince13 009f18b
More concise implementation, remove unneeded dealiasing per comments …
elfprince13 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
76 changes: 76 additions & 0 deletions
76
compiler/src/dotty/tools/dotc/transform/ElimContextClosures.scala
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
package dotty.tools | ||
package dotc | ||
package transform | ||
|
||
import MegaPhase._ | ||
import core._ | ||
import Symbols._ | ||
import SymDenotations._ | ||
import Contexts._ | ||
import Types._ | ||
import Flags._ | ||
import Decorators._ | ||
import DenotTransformers._ | ||
import core.StdNames.nme | ||
import core.StdNames | ||
import ast.Trees._ | ||
import reporting.trace | ||
|
||
/** Transforms function arguments which are context functions to | ||
* avoid a build-up of redundant thunks when passed repeatedly, | ||
* e.g. due to recursion. | ||
* | ||
* This is necessary because the compiler produces a contextual | ||
* closure around values passed as arguments where a context function | ||
* is expected, unless that value has the syntactic form of a context | ||
* function literal. | ||
* | ||
* This makes for very ergonomic client code, but the implementation | ||
* requires the wrapper to be generated before type information is available. | ||
* Thus, it can't be determined if the passed value is already a context function | ||
* of the expected type, and the closure must be generated either way. | ||
* | ||
* Without this phase, when a contextual function is passed as an argument to a | ||
* recursive function, that would have the unfortunate effect of a linear growth | ||
* in transient thunks of identical type wrapped around each other, leading | ||
* to performance degradation, and in some cases, stack overflows. | ||
* | ||
* For additional reading material, please refer to the Simplicitly paper and/or | ||
* the discussion at https://github.com/lampepfl/dotty/issues/10889 | ||
*/ | ||
class ElimContextClosures extends MiniPhase with IdentityDenotTransformer { thisPhase: DenotTransformer => | ||
import ast.tpd._ | ||
import ast.untpd | ||
|
||
override def phaseName:String = ElimContextClosures.name | ||
|
||
override def transformApply(tree: Apply)(using Context): Tree = | ||
trace(s"transforming ${tree.show} at phase ${ctx.phase}", show = true) { | ||
|
||
def transformArg(arg: Tree, formal: Type): Tree = { | ||
val formal1 = formal.widen | ||
if defn.isContextFunctionType(formal1) && untpd.isContextualClosure(arg) then | ||
unsplice(closureBody(arg)) match { | ||
case Apply(Select(body, nme.apply), _) => | ||
val underlyingBodyType = body.tpe.widen | ||
val bodyIsContextual = defn.isContextFunctionType(underlyingBodyType) | ||
val bodyTypeMatches = TypeComparer.isSubType(underlyingBodyType, formal1) | ||
if bodyIsContextual && bodyTypeMatches then | ||
body | ||
else | ||
arg | ||
case other => other | ||
} // no-op if not a nested closure of some kind | ||
else | ||
arg | ||
} | ||
|
||
val mt @ MethodType(_) = tree.fun.tpe.widen | ||
val args1 = tree.args.zipWithConserve(mt.paramInfos)(transformArg) | ||
cpy.Apply(tree)(tree.fun, args1) | ||
} | ||
} | ||
|
||
object ElimContextClosures { | ||
val name: String = "elimContextClosures" | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
true |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import scala.annotation.tailrec | ||
|
||
import java.lang.management.ManagementFactory | ||
import java.lang.management.RuntimeMXBean | ||
|
||
import scala.jdk.CollectionConverters._ | ||
|
||
object Test { | ||
trait Txn {} | ||
type AtomicOp[Z] = Txn ?=> Z | ||
type VanillaAtomicOp[Z] = Txn => Z | ||
|
||
object AbortAndRetry extends scala.util.control.ControlThrowable("abort and retry") {} | ||
|
||
def beginTxn:Txn = new Txn {} | ||
|
||
@tailrec def retryN[Z](n:Int)(txn:AtomicOp[Z], i:Int = 0):Z = { | ||
try { | ||
given Txn = beginTxn | ||
val ret:Z = txn | ||
if(i < n) { throw AbortAndRetry } | ||
ret | ||
} catch { | ||
case AbortAndRetry => retryN(n)(txn, i + 1) | ||
} | ||
} | ||
|
||
|
||
@tailrec def safeRetryN[Z](n:Int)(txn:VanillaAtomicOp[Z], i:Int = 0):Z = { | ||
try { | ||
given Txn = beginTxn | ||
val ret:Z = txn.asInstanceOf[AtomicOp[Z]] | ||
if(i < n) { throw AbortAndRetry } | ||
ret | ||
} catch { | ||
case AbortAndRetry => safeRetryN(n)(txn, i + 1) | ||
} | ||
} | ||
|
||
object StackSize { | ||
def unapply(arg:String):Option[Int] = { | ||
Option(arg match { | ||
case s"-Xss$rest" => rest | ||
case s"-XX:ThreadStackSize=$rest" => rest | ||
case _ => null | ||
}).map{ rest => | ||
val shift = (rest.toLowerCase.last match { | ||
case 'k' => 10 | ||
case 'm' => 20 | ||
case 'g' => 30 | ||
case _ => 0 | ||
}) | ||
(if(shift > 0) { | ||
rest.dropRight(1) | ||
} else { | ||
rest | ||
}).toInt << shift | ||
} | ||
} | ||
} | ||
def main(args:Array[String]) = { | ||
val arguments = ManagementFactory.getRuntimeMXBean.getInputArguments | ||
// 64bit VM defaults to 1024K / 1M stack size, 32bit defaults to 320k | ||
// Use 1024 as upper bound. | ||
val maxStackSize:Int = arguments.asScala.reverseIterator.collectFirst{case StackSize(stackSize) => stackSize}.getOrElse(1 << 20) | ||
|
||
val testTxn:VanillaAtomicOp[Boolean] = { | ||
(txn:Txn) => | ||
given Txn = txn | ||
true | ||
} | ||
|
||
Console.println(try { | ||
// maxStackSize is a good upper bound on linear stack growth | ||
// without assuming too much about frame size | ||
// (1 byte / frame is conservative even for a z80) | ||
retryN(maxStackSize)(testTxn.asInstanceOf[AtomicOp[Boolean]]) == safeRetryN(maxStackSize)(testTxn) | ||
} catch { | ||
case e:StackOverflowError => | ||
Console.println(s"Exploded after ${e.getStackTrace.length} frames") | ||
false | ||
}) | ||
} | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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 it easier to just probe stack depth with
new Throwable().getStackTrace
? forn
andn+1
recursions.(Generally wary of tests for SOE and OOM.)
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'm not sure I see what you have in mind, can you be a little bit more explicit?
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.
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.
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.
Ah, sure, we could have the return value from
testTxn
be the current stack depth and then checkretryN
with different iteration counts. Happy to do that if it's preferable.