diff --git a/compiler/src/dotty/tools/dotc/Compiler.scala b/compiler/src/dotty/tools/dotc/Compiler.scala index 683ced4cb699..16b7a890f7ce 100644 --- a/compiler/src/dotty/tools/dotc/Compiler.scala +++ b/compiler/src/dotty/tools/dotc/Compiler.scala @@ -59,6 +59,7 @@ class Compiler { new ElimPackagePrefixes, // Eliminate references to package prefixes in Select nodes new CookComments, // Cook the comments: expand variables, doc, etc. new CheckStatic, // Check restrictions that apply to @static members + new BetaReduce, // Reduce closure applications new init.Checker) :: // Check initialization of objects List(new CompleteJavaEnums, // Fill in constructors for Java enums new ElimRepeated, // Rewrite vararg parameters and arguments diff --git a/compiler/src/dotty/tools/dotc/transform/BetaReduce.scala b/compiler/src/dotty/tools/dotc/transform/BetaReduce.scala new file mode 100644 index 000000000000..eb1b1860d67d --- /dev/null +++ b/compiler/src/dotty/tools/dotc/transform/BetaReduce.scala @@ -0,0 +1,63 @@ +package dotty.tools +package dotc +package transform + +import core._ +import MegaPhase._ +import Symbols._, Contexts._, Types._, Decorators._ +import StdNames.nme +import ast.Trees._ +import ast.TreeTypeMap + +/** Rewrite an application + * + * (((x1, ..., xn) => b): T)(y1, ..., yn) + * + * where + * + * - all yi are pure references without a prefix + * - the closure can also be contextual or erased, but cannot be a SAM type + * _ the type ascription ...: T is optional + * + * to + * + * [xi := yi]b + * + * This is more limited than beta reduction in inlining since it only works for simple variables `yi`. + * It is more general since it also works for type-ascripted closures. + * + * A typical use case is eliminating redundant closures for blackbox macros that + * return context functions. See i6375.scala. + */ +class BetaReduce extends MiniPhase: + import ast.tpd._ + + def phaseName: String = "betaReduce" + + override def transformApply(app: Apply)(using ctx: Context): Tree = app.fun match + case Select(fn, nme.apply) if defn.isFunctionType(fn.tpe) => + val app1 = betaReduce(app, fn, app.args) + if app1 ne app then ctx.log(i"beta reduce $app -> $app1") + app1 + case _ => + app + + private def betaReduce(tree: Apply, fn: Tree, args: List[Tree])(using ctx: Context): Tree = + fn match + case Typed(expr, _) => betaReduce(tree, expr, args) + case Block(Nil, expr) => betaReduce(tree, expr, args) + case Block((anonFun: DefDef) :: Nil, closure: Closure) => + val argSyms = + for arg <- args yield + arg.tpe.dealias match + case ref @ TermRef(NoPrefix, _) if isPurePath(arg) => ref.symbol + case _ => NoSymbol + val vparams = anonFun.vparamss.head + if argSyms.forall(_.exists) && argSyms.hasSameLengthAs(vparams) then + TreeTypeMap( + oldOwners = anonFun.symbol :: Nil, + newOwners = ctx.owner :: Nil, + substFrom = vparams.map(_.symbol), + substTo = argSyms).transform(anonFun.rhs) + else tree + case _ => tree diff --git a/compiler/test/dotty/tools/backend/jvm/InlineBytecodeTests.scala b/compiler/test/dotty/tools/backend/jvm/InlineBytecodeTests.scala index 10de64f45df0..69f00168e93f 100644 --- a/compiler/test/dotty/tools/backend/jvm/InlineBytecodeTests.scala +++ b/compiler/test/dotty/tools/backend/jvm/InlineBytecodeTests.scala @@ -320,4 +320,40 @@ class InlineBytecodeTests extends DottyBytecodeTest { } } + + @Test def i6375 = { + val source = """class Test: + | given Int = 0 + | def f(): Int ?=> Boolean = true : (Int ?=> Boolean) + | inline def g(): Int ?=> Boolean = true + | def test = g() + """.stripMargin + + checkBCode(source) { dir => + val clsIn = dir.lookupName("Test.class", directory = false).input + val clsNode = loadClassNode(clsIn) + + val fun = getMethod(clsNode, "test") + val instructions = instructionsFromMethod(fun) + val expected = + List( + // Head tested separatly + VarOp(ALOAD, 0), + Invoke(INVOKEVIRTUAL, "Test", "given_Int", "()I", false), + Invoke(INVOKESTATIC, "scala/runtime/BoxesRunTime", "boxToInteger", "(I)Ljava/lang/Integer;", false), + Invoke(INVOKEINTERFACE, "scala/Function1", "apply", "(Ljava/lang/Object;)Ljava/lang/Object;", true), + Invoke(INVOKESTATIC, "scala/runtime/BoxesRunTime", "unboxToBoolean", "(Ljava/lang/Object;)Z", false), + Op(IRETURN) + ) + + instructions.head match { + case InvokeDynamic(INVOKEDYNAMIC, "apply$mcZI$sp", "()Ldotty/runtime/function/JFunction1$mcZI$sp;", _, _) => + case _ => assert(false, "`g` was not properly inlined in `test`\n") + } + + assert(instructions.tail == expected, + "`fg was not properly inlined in `test`\n" + diffInstructions(instructions, expected)) + + } + } } diff --git a/tests/pos/i6375.scala b/tests/pos/i6375.scala new file mode 100644 index 000000000000..aa682483b52d --- /dev/null +++ b/tests/pos/i6375.scala @@ -0,0 +1,38 @@ +/* In the following we should never have two nested closures after phase betaReduce + * The output of the program should instead look like this: + + package { + @scala.annotation.internal.SourceFile("i6375.scala") class Test() extends + Object + () { + final given def given_Int: Int = 0 + @scala.annotation.internal.ContextResultCount(1) def f(): (Int) ?=> Boolean + = + { + def $anonfun(using evidence$1: Int): Boolean = true + closure($anonfun) + } + @scala.annotation.internal.ContextResultCount(1) inline def g(): + (Int) ?=> Boolean + = + { + def $anonfun(using evidence$3: Int): Boolean = true + closure($anonfun) + } + { + { + def $anonfun(using evidence$3: Int): Boolean = true + closure($anonfun) + } + }.apply(this.given_Int) + } + } + */ +class Test: + given Int = 0 + + def f(): Int ?=> Boolean = true : (Int ?=> Boolean) + + inline def g(): Int ?=> Boolean = true + g() +