Skip to content
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

Fix #8530: Support inline unapply #8542

Merged
merged 7 commits into from
Apr 2, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/Compiler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ class Compiler {
new StringInterpolatorOpt, // Optimizes raw and s string interpolators by rewriting them to string concatentations
new CrossCastAnd) :: // Normalize selections involving intersection types.
List(new PruneErasedDefs, // Drop erased definitions from scopes and simplify erased expressions
new InlinePatterns, // Remove placeholders of inlined patterns
new VCInlineMethods, // Inlines calls to value class methods
new SeqLiterals, // Express vararg arguments as arrays
new InterceptedMethods, // Special handling of `==`, `|=`, `getClass` methods
Expand Down
45 changes: 31 additions & 14 deletions compiler/src/dotty/tools/dotc/transform/BetaReduce.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package dotc
package transform

import core._
import Flags._
import MegaPhase._
import Symbols._, Contexts._, Types._, Decorators._
import StdNames.nme
Expand Down Expand Up @@ -46,18 +47,34 @@ class BetaReduce extends MiniPhase:
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 Block((anonFun: DefDef) :: Nil, closure: Closure) => BetaReduce(anonFun, args)
case _ => tree

object BetaReduce:
import ast.tpd._

/** Beta-reduces a call to `ddef` with arguments `argSyms` */
def apply(ddef: DefDef, args: List[Tree])(using ctx: Context) =
val bindings = List.newBuilder[ValDef]
val vparams = ddef.vparamss.iterator.flatten.toList
assert(args.hasSameLengthAs(vparams))
val argSyms =
for (arg, param) <- args.zip(vparams) yield
arg.tpe.dealias match
case ref @ TermRef(NoPrefix, _) if isPurePath(arg) =>
ref.symbol
case _ =>
val flags = Synthetic | (param.symbol.flags & Erased)
val binding = ValDef(ctx.newSymbol(ctx.owner, param.name, flags, arg.tpe.widen, coord = arg.span), arg)
bindings += binding
binding.symbol

val expansion = TreeTypeMap(
oldOwners = ddef.symbol :: Nil,
newOwners = ctx.owner :: Nil,
substFrom = vparams.map(_.symbol),
substTo = argSyms
).transform(ddef.rhs)

seq(bindings.result(), expansion)
end apply
61 changes: 61 additions & 0 deletions compiler/src/dotty/tools/dotc/transform/InlinePatterns.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package dotty.tools
package dotc
package transform

import core._
import MegaPhase._
import Symbols._, Contexts._, Types._, Decorators._
import StdNames.nme
import NameOps._
import Names._
import ast.Trees._
import ast.TreeTypeMap

/** Rewrite an application
*
* {new { def unapply(x0: X0)(x1: X1,..., xn: Xn) = b }}.unapply(y0)(y1, ..., yn)
*
* where
*
* - the method is `unapply` or `unapplySeq`
* - the method does not have type parameters
*
* to
*
* [xi := yi]b
*
* This removes placeholders added by inline `unapply`/`unapplySeq` patterns.
*/
class InlinePatterns extends MiniPhase:
import ast.tpd._

def phaseName: String = "inlinePatterns"

// This phase needs to run after because it need to transform trees that are generated
// by the pattern matcher but are still not visible in that group of phases.
override def runsAfterGroupsOf: Set[String] = Set(PatternMatcher.name)
nicolasstucki marked this conversation as resolved.
Show resolved Hide resolved

override def transformApply(app: Apply)(using ctx: Context): Tree =
if app.symbol.name.isUnapplyName && !app.tpe.isInstanceOf[MethodicType] then
app match
case App(Select(fn, name), argss) =>
val app1 = betaReduce(app, fn, name, argss.flatten)
if app1 ne app then ctx.log(i"beta reduce $app -> $app1")
app1
case _ =>
app
else app

private object App:
def unapply(app: Tree): (Tree, List[List[Tree]]) =
app match
case Apply(App(fn, argss), args) => (fn, argss :+ args)
case _ => (app, Nil)

private def betaReduce(tree: Apply, fn: Tree, name: Name, args: List[Tree])(using ctx: Context): Tree =
nicolasstucki marked this conversation as resolved.
Show resolved Hide resolved
fn match
case Block(TypeDef(_, template: Template) :: Nil, Apply(Select(New(_),_), Nil)) if template.constr.rhs.isEmpty =>
template.body match
case List(ddef @ DefDef(`name`, _, _, _, _)) => BetaReduce(ddef, args)
case _ => tree
case _ => tree
42 changes: 42 additions & 0 deletions compiler/src/dotty/tools/dotc/typer/Inliner.scala
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,48 @@ object Inliner {
)
}

/** Try to inline a pattern with an inline unapply method. Fail with error if the maximal
* inline depth is exceeded.
*
* @param unapp The tree of the pattern to inline
* @return An `Unapply` with a `fun` containing the inlined call to the unapply
*/
def inlinedUnapply(unapp: tpd.UnApply)(using ctx: Context): Tree = {
// We cannot inline the unapply directly, since the pattern matcher relies on unapply applications
// as signposts what to do. On the other hand, we can do the inlining only in typer, not afterwards.
// So the trick is to create a "wrapper" unapply in an anonymous class that has the inlined unapply
// as its right hand side. The call to the wrapper unapply serves as the signpost for pattern matching.
// After pattern matching, the anonymous class is removed in phase InlinePatterns with a beta reduction step.
//
// An inline unapply `P.unapply` in a plattern `P(x1,x2,...)` is transformed into
// `{ class $anon { def unapply(t0: T0)(using t1: T1, t2: T2, ...): R = P.unapply(t0)(using t1, t2, ...) }; new $anon }.unapply`
// and the call `P.unapply(x1, x2, ...)` is inlined.
// This serves as a placeholder for the inlined body until the `patternMatcher` phase. After pattern matcher
// transforms the patterns into terms, the `inlinePatterns` phase removes this anonymous class by β-reducing
// the call to the `unapply`.

val UnApply(fun, implicits, patterns) = unapp
val sym = unapp.symbol
val cls = ctx.newNormalizedClassSymbol(ctx.owner, tpnme.ANON_CLASS, Synthetic | Final, List(defn.ObjectType), coord = sym.coord)
val constr = ctx.newConstructor(cls, Synthetic, Nil, Nil, coord = sym.coord).entered

val targs = fun match
case TypeApply(_, targs) => targs
case _ => Nil
val unapplyInfo = sym.info match
case info: PolyType => info.instantiate(targs.map(_.tpe))
case info => info

val unappplySym = ctx.newSymbol(cls, sym.name.toTermName, Synthetic | Method, unapplyInfo, coord = sym.coord).entered
val unapply = DefDef(unappplySym, argss =>
inlineCall(fun.appliedToArgss(argss).withSpan(unapp.span))(ctx.withOwner(unappplySym))
)
val cdef = ClassDef(cls, DefDef(constr), List(unapply))
val newUnapply = Block(cdef :: Nil, New(cls.typeRef, Nil))
val newFun = newUnapply.select(unappplySym).withSpan(unapp.span)
cpy.UnApply(unapp)(newFun, implicits, patterns)
}

/** For a retained inline method, another method that keeps track of
* the body that is kept at runtime. For instance, an inline method
*
Expand Down
2 changes: 0 additions & 2 deletions compiler/src/dotty/tools/dotc/typer/PrepareInlineable.scala
Original file line number Diff line number Diff line change
Expand Up @@ -246,8 +246,6 @@ object PrepareInlineable {
ctx.error(em"Implementation restriction: No inline methods allowed where opaque type aliases are in scope", inlined.sourcePos)
if (ctx.outer.inInlineMethod)
ctx.error(ex"Implementation restriction: nested inline methods are not supported", inlined.sourcePos)
if (inlined.name.isUnapplyName)
ctx.error(em"Implementation restriction: inline ${inlined.name} methods are not supported", inlined.sourcePos)

if (inlined.is(Macro) && !ctx.isAfterTyper) {

Expand Down
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1250,6 +1250,7 @@ class Typer extends Namer
if (bounds != null) sym.info = bounds
}
b
case t: UnApply if t.symbol.is(Inline) => Inliner.inlinedUnapply(t)
case t => t
}
}
Expand Down
5 changes: 0 additions & 5 deletions tests/neg/inine-unnapply.scala

This file was deleted.

15 changes: 0 additions & 15 deletions tests/neg/inline-unapply.scala

This file was deleted.

26 changes: 26 additions & 0 deletions tests/pos/i8530.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
object MyBoooleanUnapply:
inline def unapply(x: Int): Boolean = true

object MyOptionUnapply:
inline def unapply(x: Int): Option[Long] = Some(x)

object MyUnapplyImplicits:
inline def unapply(x: Int)(using DummyImplicit): Option[Long] = Some(x)

object MyPolyUnapply:
inline def unapply[T](x: T): Option[T] = Some(x)

object MySeqUnapply:
inline def unapplySeq(x: Int): Seq[Int] = Seq(x, x)

object MyWhiteboxUnapply:
transparent inline def unapply(x: Int): Option[Any] = Some(x)

def test: Unit =
val x = 5 match
case MyBoooleanUnapply() =>
case MyOptionUnapply(y) => y: Long
case MyUnapplyImplicits(y) => y: Long
case MyPolyUnapply(a) => a: Int
case MySeqUnapply(a, b) => (a: Int, b: Int)
case MyWhiteboxUnapply(x) => x: Int
5 changes: 5 additions & 0 deletions tests/pos/inine-unnapply.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

object Foo {
inline def unapply(x: Any): Boolean = ???
inline def unapplySeq(x: Any): Seq[Any] = ???
}
15 changes: 15 additions & 0 deletions tests/pos/inline-unapply.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
object Test {

class C(val x: Int, val y: Int)

inline def unapply(c: C): Some[(Int, Int)] = Some((c.x, c.y))

}
object Test2 {

class C(x: Int, y: Int)

inline def unapply(c: C): Option[(Int, Int)] = inline c match {
case x: C => Some((1, 1))
}
}
1 change: 1 addition & 0 deletions tests/run-macros/i8530-2.check
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
foo
12 changes: 12 additions & 0 deletions tests/run-macros/i8530/App_2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

object Test {
def main(args: Array[String]): Unit = {
0 match
case Succ(n) => ???
case _ =>

2 match
case Succ(n) => assert(n == 1)
}

}
8 changes: 8 additions & 0 deletions tests/run-macros/i8530/Macro_1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import scala.quoted._

object Succ:

inline def unapply(n: Int): Option[Int] = ${ impl('n) }

private def impl(n: Expr[Int])(using QuoteContext): Expr[Option[Int]] =
'{ if $n == 0 then None else Some($n - 1)}
18 changes: 18 additions & 0 deletions tests/run/i8530-b.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import scala.compiletime.erasedValue

class MyRegex[Pattern <: String & Singleton/*Literal constant*/]:
inline def unapplySeq(s: CharSequence): Option[List[String]] =
inline erasedValue[Pattern] match
case "foo" => if s == "foo" then Some(Nil) else None
case _ => valueOf[Pattern].r.unapplySeq(s)

@main def Test: Unit =
val myRegexp1 = new MyRegex["foo"]
val myRegexp2 = new MyRegex["f(o+)"]
"foo" match
case myRegexp1() => // Match ok
case myRegexp2(x) => ???
"foooo" match
case myRegexp1() => ???
case myRegexp2(x) =>
assert(x == "oooo")
5 changes: 5 additions & 0 deletions tests/run/i8530.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
MyBoooleanUnapply
2
3
(4,5)
5
33 changes: 33 additions & 0 deletions tests/run/i8530.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
object MyBoooleanUnapply:
inline def unapply(x: Int): Boolean = true

object MyOptionUnapply:
inline def unapply(x: Int): Option[Long] = Some(x)

object MyPolyUnapply:
inline def unapply[T](x: T): Option[T] = Some(x)

object MySeqUnapply:
inline def unapplySeq(x: Int): Seq[Int] = Seq(x, x + 1)

object MyWhiteboxUnapply:
transparent inline def unapply(x: Int): Option[Any] = Some(x)


@main def Test =
1 match
case MyBoooleanUnapply() => println("MyBoooleanUnapply")

2 match
case MyOptionUnapply(y) => println(y)

3 match
case MyPolyUnapply(a) => println(a)

4 match
case MySeqUnapply(a, b) => println((a, b))

5 match
case MyWhiteboxUnapply(x) => println(x: Int)

end Test