Skip to content

Commit 7ae44d4

Browse files
authored
Merge pull request #8542 from dotty-staging/fix-#8530
Fix #8530: Support inline unapply
2 parents 0bd0fcf + efc258d commit 7ae44d4

17 files changed

+259
-36
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ class Compiler {
7979
new StringInterpolatorOpt, // Optimizes raw and s string interpolators by rewriting them to string concatentations
8080
new CrossCastAnd) :: // Normalize selections involving intersection types.
8181
List(new PruneErasedDefs, // Drop erased definitions from scopes and simplify erased expressions
82+
new InlinePatterns, // Remove placeholders of inlined patterns
8283
new VCInlineMethods, // Inlines calls to value class methods
8384
new SeqLiterals, // Express vararg arguments as arrays
8485
new InterceptedMethods, // Special handling of `==`, `|=`, `getClass` methods

compiler/src/dotty/tools/dotc/transform/BetaReduce.scala

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package dotc
33
package transform
44

55
import core._
6+
import Flags._
67
import MegaPhase._
78
import Symbols._, Contexts._, Types._, Decorators._
89
import StdNames.nme
@@ -46,18 +47,34 @@ class BetaReduce extends MiniPhase:
4647
fn match
4748
case Typed(expr, _) => betaReduce(tree, expr, args)
4849
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
50+
case Block((anonFun: DefDef) :: Nil, closure: Closure) => BetaReduce(anonFun, args)
6351
case _ => tree
52+
53+
object BetaReduce:
54+
import ast.tpd._
55+
56+
/** Beta-reduces a call to `ddef` with arguments `argSyms` */
57+
def apply(ddef: DefDef, args: List[Tree])(using ctx: Context) =
58+
val bindings = List.newBuilder[ValDef]
59+
val vparams = ddef.vparamss.iterator.flatten.toList
60+
assert(args.hasSameLengthAs(vparams))
61+
val argSyms =
62+
for (arg, param) <- args.zip(vparams) yield
63+
arg.tpe.dealias match
64+
case ref @ TermRef(NoPrefix, _) if isPurePath(arg) =>
65+
ref.symbol
66+
case _ =>
67+
val flags = Synthetic | (param.symbol.flags & Erased)
68+
val binding = ValDef(ctx.newSymbol(ctx.owner, param.name, flags, arg.tpe.widen, coord = arg.span), arg)
69+
bindings += binding
70+
binding.symbol
71+
72+
val expansion = TreeTypeMap(
73+
oldOwners = ddef.symbol :: Nil,
74+
newOwners = ctx.owner :: Nil,
75+
substFrom = vparams.map(_.symbol),
76+
substTo = argSyms
77+
).transform(ddef.rhs)
78+
79+
seq(bindings.result(), expansion)
80+
end apply
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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 NameOps._
10+
import Names._
11+
import ast.Trees._
12+
import ast.TreeTypeMap
13+
14+
/** Rewrite an application
15+
*
16+
* {new { def unapply(x0: X0)(x1: X1,..., xn: Xn) = b }}.unapply(y0)(y1, ..., yn)
17+
*
18+
* where
19+
*
20+
* - the method is `unapply` or `unapplySeq`
21+
* - the method does not have type parameters
22+
*
23+
* to
24+
*
25+
* [xi := yi]b
26+
*
27+
* This removes placeholders added by inline `unapply`/`unapplySeq` patterns.
28+
*/
29+
class InlinePatterns extends MiniPhase:
30+
import ast.tpd._
31+
32+
def phaseName: String = "inlinePatterns"
33+
34+
// This phase needs to run after because it need to transform trees that are generated
35+
// by the pattern matcher but are still not visible in that group of phases.
36+
override def runsAfterGroupsOf: Set[String] = Set(PatternMatcher.name)
37+
38+
override def transformApply(app: Apply)(using ctx: Context): Tree =
39+
if app.symbol.name.isUnapplyName && !app.tpe.isInstanceOf[MethodicType] then
40+
app match
41+
case App(Select(fn, name), argss) =>
42+
val app1 = betaReduce(app, fn, name, argss.flatten)
43+
if app1 ne app then ctx.log(i"beta reduce $app -> $app1")
44+
app1
45+
case _ =>
46+
app
47+
else app
48+
49+
private object App:
50+
def unapply(app: Tree): (Tree, List[List[Tree]]) =
51+
app match
52+
case Apply(App(fn, argss), args) => (fn, argss :+ args)
53+
case _ => (app, Nil)
54+
55+
private def betaReduce(tree: Apply, fn: Tree, name: Name, args: List[Tree])(using ctx: Context): Tree =
56+
fn match
57+
case Block(TypeDef(_, template: Template) :: Nil, Apply(Select(New(_),_), Nil)) if template.constr.rhs.isEmpty =>
58+
template.body match
59+
case List(ddef @ DefDef(`name`, _, _, _, _)) => BetaReduce(ddef, args)
60+
case _ => tree
61+
case _ => tree

compiler/src/dotty/tools/dotc/typer/Inliner.scala

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,48 @@ object Inliner {
124124
)
125125
}
126126

127+
/** Try to inline a pattern with an inline unapply method. Fail with error if the maximal
128+
* inline depth is exceeded.
129+
*
130+
* @param unapp The tree of the pattern to inline
131+
* @return An `Unapply` with a `fun` containing the inlined call to the unapply
132+
*/
133+
def inlinedUnapply(unapp: tpd.UnApply)(using ctx: Context): Tree = {
134+
// We cannot inline the unapply directly, since the pattern matcher relies on unapply applications
135+
// as signposts what to do. On the other hand, we can do the inlining only in typer, not afterwards.
136+
// So the trick is to create a "wrapper" unapply in an anonymous class that has the inlined unapply
137+
// as its right hand side. The call to the wrapper unapply serves as the signpost for pattern matching.
138+
// After pattern matching, the anonymous class is removed in phase InlinePatterns with a beta reduction step.
139+
//
140+
// An inline unapply `P.unapply` in a plattern `P(x1,x2,...)` is transformed into
141+
// `{ class $anon { def unapply(t0: T0)(using t1: T1, t2: T2, ...): R = P.unapply(t0)(using t1, t2, ...) }; new $anon }.unapply`
142+
// and the call `P.unapply(x1, x2, ...)` is inlined.
143+
// This serves as a placeholder for the inlined body until the `patternMatcher` phase. After pattern matcher
144+
// transforms the patterns into terms, the `inlinePatterns` phase removes this anonymous class by β-reducing
145+
// the call to the `unapply`.
146+
147+
val UnApply(fun, implicits, patterns) = unapp
148+
val sym = unapp.symbol
149+
val cls = ctx.newNormalizedClassSymbol(ctx.owner, tpnme.ANON_CLASS, Synthetic | Final, List(defn.ObjectType), coord = sym.coord)
150+
val constr = ctx.newConstructor(cls, Synthetic, Nil, Nil, coord = sym.coord).entered
151+
152+
val targs = fun match
153+
case TypeApply(_, targs) => targs
154+
case _ => Nil
155+
val unapplyInfo = sym.info match
156+
case info: PolyType => info.instantiate(targs.map(_.tpe))
157+
case info => info
158+
159+
val unappplySym = ctx.newSymbol(cls, sym.name.toTermName, Synthetic | Method, unapplyInfo, coord = sym.coord).entered
160+
val unapply = DefDef(unappplySym, argss =>
161+
inlineCall(fun.appliedToArgss(argss).withSpan(unapp.span))(ctx.withOwner(unappplySym))
162+
)
163+
val cdef = ClassDef(cls, DefDef(constr), List(unapply))
164+
val newUnapply = Block(cdef :: Nil, New(cls.typeRef, Nil))
165+
val newFun = newUnapply.select(unappplySym).withSpan(unapp.span)
166+
cpy.UnApply(unapp)(newFun, implicits, patterns)
167+
}
168+
127169
/** For a retained inline method, another method that keeps track of
128170
* the body that is kept at runtime. For instance, an inline method
129171
*

compiler/src/dotty/tools/dotc/typer/PrepareInlineable.scala

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,8 +246,6 @@ object PrepareInlineable {
246246
ctx.error(em"Implementation restriction: No inline methods allowed where opaque type aliases are in scope", inlined.sourcePos)
247247
if (ctx.outer.inInlineMethod)
248248
ctx.error(ex"Implementation restriction: nested inline methods are not supported", inlined.sourcePos)
249-
if (inlined.name.isUnapplyName)
250-
ctx.error(em"Implementation restriction: inline ${inlined.name} methods are not supported", inlined.sourcePos)
251249

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

compiler/src/dotty/tools/dotc/typer/Typer.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1250,6 +1250,7 @@ class Typer extends Namer
12501250
if (bounds != null) sym.info = bounds
12511251
}
12521252
b
1253+
case t: UnApply if t.symbol.is(Inline) => Inliner.inlinedUnapply(t)
12531254
case t => t
12541255
}
12551256
}

tests/neg/inine-unnapply.scala

Lines changed: 0 additions & 5 deletions
This file was deleted.

tests/neg/inline-unapply.scala

Lines changed: 0 additions & 15 deletions
This file was deleted.

tests/pos/i8530.scala

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
object MyBoooleanUnapply:
2+
inline def unapply(x: Int): Boolean = true
3+
4+
object MyOptionUnapply:
5+
inline def unapply(x: Int): Option[Long] = Some(x)
6+
7+
object MyUnapplyImplicits:
8+
inline def unapply(x: Int)(using DummyImplicit): Option[Long] = Some(x)
9+
10+
object MyPolyUnapply:
11+
inline def unapply[T](x: T): Option[T] = Some(x)
12+
13+
object MySeqUnapply:
14+
inline def unapplySeq(x: Int): Seq[Int] = Seq(x, x)
15+
16+
object MyWhiteboxUnapply:
17+
transparent inline def unapply(x: Int): Option[Any] = Some(x)
18+
19+
def test: Unit =
20+
val x = 5 match
21+
case MyBoooleanUnapply() =>
22+
case MyOptionUnapply(y) => y: Long
23+
case MyUnapplyImplicits(y) => y: Long
24+
case MyPolyUnapply(a) => a: Int
25+
case MySeqUnapply(a, b) => (a: Int, b: Int)
26+
case MyWhiteboxUnapply(x) => x: Int

tests/pos/inine-unnapply.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
2+
object Foo {
3+
inline def unapply(x: Any): Boolean = ???
4+
inline def unapplySeq(x: Any): Seq[Any] = ???
5+
}

tests/pos/inline-unapply.scala

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
object Test {
2+
3+
class C(val x: Int, val y: Int)
4+
5+
inline def unapply(c: C): Some[(Int, Int)] = Some((c.x, c.y))
6+
7+
}
8+
object Test2 {
9+
10+
class C(x: Int, y: Int)
11+
12+
inline def unapply(c: C): Option[(Int, Int)] = inline c match {
13+
case x: C => Some((1, 1))
14+
}
15+
}

tests/run-macros/i8530-2.check

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
foo

tests/run-macros/i8530/App_2.scala

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
2+
object Test {
3+
def main(args: Array[String]): Unit = {
4+
0 match
5+
case Succ(n) => ???
6+
case _ =>
7+
8+
2 match
9+
case Succ(n) => assert(n == 1)
10+
}
11+
12+
}

tests/run-macros/i8530/Macro_1.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import scala.quoted._
2+
3+
object Succ:
4+
5+
inline def unapply(n: Int): Option[Int] = ${ impl('n) }
6+
7+
private def impl(n: Expr[Int])(using QuoteContext): Expr[Option[Int]] =
8+
'{ if $n == 0 then None else Some($n - 1)}

tests/run/i8530-b.scala

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import scala.compiletime.erasedValue
2+
3+
class MyRegex[Pattern <: String & Singleton/*Literal constant*/]:
4+
inline def unapplySeq(s: CharSequence): Option[List[String]] =
5+
inline erasedValue[Pattern] match
6+
case "foo" => if s == "foo" then Some(Nil) else None
7+
case _ => valueOf[Pattern].r.unapplySeq(s)
8+
9+
@main def Test: Unit =
10+
val myRegexp1 = new MyRegex["foo"]
11+
val myRegexp2 = new MyRegex["f(o+)"]
12+
"foo" match
13+
case myRegexp1() => // Match ok
14+
case myRegexp2(x) => ???
15+
"foooo" match
16+
case myRegexp1() => ???
17+
case myRegexp2(x) =>
18+
assert(x == "oooo")

tests/run/i8530.check

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
MyBoooleanUnapply
2+
2
3+
3
4+
(4,5)
5+
5

tests/run/i8530.scala

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
object MyBoooleanUnapply:
2+
inline def unapply(x: Int): Boolean = true
3+
4+
object MyOptionUnapply:
5+
inline def unapply(x: Int): Option[Long] = Some(x)
6+
7+
object MyPolyUnapply:
8+
inline def unapply[T](x: T): Option[T] = Some(x)
9+
10+
object MySeqUnapply:
11+
inline def unapplySeq(x: Int): Seq[Int] = Seq(x, x + 1)
12+
13+
object MyWhiteboxUnapply:
14+
transparent inline def unapply(x: Int): Option[Any] = Some(x)
15+
16+
17+
@main def Test =
18+
1 match
19+
case MyBoooleanUnapply() => println("MyBoooleanUnapply")
20+
21+
2 match
22+
case MyOptionUnapply(y) => println(y)
23+
24+
3 match
25+
case MyPolyUnapply(a) => println(a)
26+
27+
4 match
28+
case MySeqUnapply(a, b) => println((a, b))
29+
30+
5 match
31+
case MyWhiteboxUnapply(x) => println(x: Int)
32+
33+
end Test

0 commit comments

Comments
 (0)