Skip to content

Commit dd340af

Browse files
authored
Merge pull request #8452 from dotty-staging/fix-#6375
Fix #6375: Add miniphase for simple beta reductions
2 parents 31fc9ab + 48e27fe commit dd340af

File tree

4 files changed

+138
-0
lines changed

4 files changed

+138
-0
lines changed

compiler/src/dotty/tools/dotc/Compiler.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ class Compiler {
5959
new ElimPackagePrefixes, // Eliminate references to package prefixes in Select nodes
6060
new CookComments, // Cook the comments: expand variables, doc, etc.
6161
new CheckStatic, // Check restrictions that apply to @static members
62+
new BetaReduce, // Reduce closure applications
6263
new init.Checker) :: // Check initialization of objects
6364
List(new CompleteJavaEnums, // Fill in constructors for Java enums
6465
new ElimRepeated, // Rewrite vararg parameters and arguments
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package dotty.tools
2+
package dotc
3+
package transform
4+
5+
import core._
6+
import MegaPhase._
7+
import Symbols._, Contexts._, Types._, Decorators._
8+
import StdNames.nme
9+
import ast.Trees._
10+
import ast.TreeTypeMap
11+
12+
/** Rewrite an application
13+
*
14+
* (((x1, ..., xn) => b): T)(y1, ..., yn)
15+
*
16+
* where
17+
*
18+
* - all yi are pure references without a prefix
19+
* - the closure can also be contextual or erased, but cannot be a SAM type
20+
* _ the type ascription ...: T is optional
21+
*
22+
* to
23+
*
24+
* [xi := yi]b
25+
*
26+
* This is more limited than beta reduction in inlining since it only works for simple variables `yi`.
27+
* It is more general since it also works for type-ascripted closures.
28+
*
29+
* A typical use case is eliminating redundant closures for blackbox macros that
30+
* return context functions. See i6375.scala.
31+
*/
32+
class BetaReduce extends MiniPhase:
33+
import ast.tpd._
34+
35+
def phaseName: String = "betaReduce"
36+
37+
override def transformApply(app: Apply)(using ctx: Context): Tree = app.fun match
38+
case Select(fn, nme.apply) if defn.isFunctionType(fn.tpe) =>
39+
val app1 = betaReduce(app, fn, app.args)
40+
if app1 ne app then ctx.log(i"beta reduce $app -> $app1")
41+
app1
42+
case _ =>
43+
app
44+
45+
private def betaReduce(tree: Apply, fn: Tree, args: List[Tree])(using ctx: Context): Tree =
46+
fn match
47+
case Typed(expr, _) => betaReduce(tree, expr, args)
48+
case Block(Nil, expr) => betaReduce(tree, expr, args)
49+
case Block((anonFun: DefDef) :: Nil, closure: Closure) =>
50+
val argSyms =
51+
for arg <- args yield
52+
arg.tpe.dealias match
53+
case ref @ TermRef(NoPrefix, _) if isPurePath(arg) => ref.symbol
54+
case _ => NoSymbol
55+
val vparams = anonFun.vparamss.head
56+
if argSyms.forall(_.exists) && argSyms.hasSameLengthAs(vparams) then
57+
TreeTypeMap(
58+
oldOwners = anonFun.symbol :: Nil,
59+
newOwners = ctx.owner :: Nil,
60+
substFrom = vparams.map(_.symbol),
61+
substTo = argSyms).transform(anonFun.rhs)
62+
else tree
63+
case _ => tree

compiler/test/dotty/tools/backend/jvm/InlineBytecodeTests.scala

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,4 +320,40 @@ class InlineBytecodeTests extends DottyBytecodeTest {
320320

321321
}
322322
}
323+
324+
@Test def i6375 = {
325+
val source = """class Test:
326+
| given Int = 0
327+
| def f(): Int ?=> Boolean = true : (Int ?=> Boolean)
328+
| inline def g(): Int ?=> Boolean = true
329+
| def test = g()
330+
""".stripMargin
331+
332+
checkBCode(source) { dir =>
333+
val clsIn = dir.lookupName("Test.class", directory = false).input
334+
val clsNode = loadClassNode(clsIn)
335+
336+
val fun = getMethod(clsNode, "test")
337+
val instructions = instructionsFromMethod(fun)
338+
val expected =
339+
List(
340+
// Head tested separatly
341+
VarOp(ALOAD, 0),
342+
Invoke(INVOKEVIRTUAL, "Test", "given_Int", "()I", false),
343+
Invoke(INVOKESTATIC, "scala/runtime/BoxesRunTime", "boxToInteger", "(I)Ljava/lang/Integer;", false),
344+
Invoke(INVOKEINTERFACE, "scala/Function1", "apply", "(Ljava/lang/Object;)Ljava/lang/Object;", true),
345+
Invoke(INVOKESTATIC, "scala/runtime/BoxesRunTime", "unboxToBoolean", "(Ljava/lang/Object;)Z", false),
346+
Op(IRETURN)
347+
)
348+
349+
instructions.head match {
350+
case InvokeDynamic(INVOKEDYNAMIC, "apply$mcZI$sp", "()Ldotty/runtime/function/JFunction1$mcZI$sp;", _, _) =>
351+
case _ => assert(false, "`g` was not properly inlined in `test`\n")
352+
}
353+
354+
assert(instructions.tail == expected,
355+
"`fg was not properly inlined in `test`\n" + diffInstructions(instructions, expected))
356+
357+
}
358+
}
323359
}

tests/pos/i6375.scala

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/* In the following we should never have two nested closures after phase betaReduce
2+
* The output of the program should instead look like this:
3+
4+
package <empty> {
5+
@scala.annotation.internal.SourceFile("i6375.scala") class Test() extends
6+
Object
7+
() {
8+
final given def given_Int: Int = 0
9+
@scala.annotation.internal.ContextResultCount(1) def f(): (Int) ?=> Boolean
10+
=
11+
{
12+
def $anonfun(using evidence$1: Int): Boolean = true
13+
closure($anonfun)
14+
}
15+
@scala.annotation.internal.ContextResultCount(1) inline def g():
16+
(Int) ?=> Boolean
17+
=
18+
{
19+
def $anonfun(using evidence$3: Int): Boolean = true
20+
closure($anonfun)
21+
}
22+
{
23+
{
24+
def $anonfun(using evidence$3: Int): Boolean = true
25+
closure($anonfun)
26+
}
27+
}.apply(this.given_Int)
28+
}
29+
}
30+
*/
31+
class Test:
32+
given Int = 0
33+
34+
def f(): Int ?=> Boolean = true : (Int ?=> Boolean)
35+
36+
inline def g(): Int ?=> Boolean = true
37+
g()
38+

0 commit comments

Comments
 (0)