diff --git a/compiler/src/dotty/tools/dotc/Compiler.scala b/compiler/src/dotty/tools/dotc/Compiler.scala index 16b7a890f7ce..49c28f1ea030 100644 --- a/compiler/src/dotty/tools/dotc/Compiler.scala +++ b/compiler/src/dotty/tools/dotc/Compiler.scala @@ -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 diff --git a/compiler/src/dotty/tools/dotc/transform/BetaReduce.scala b/compiler/src/dotty/tools/dotc/transform/BetaReduce.scala index eb1b1860d67d..6591e847c099 100644 --- a/compiler/src/dotty/tools/dotc/transform/BetaReduce.scala +++ b/compiler/src/dotty/tools/dotc/transform/BetaReduce.scala @@ -3,6 +3,7 @@ package dotc package transform import core._ +import Flags._ import MegaPhase._ import Symbols._, Contexts._, Types._, Decorators._ import StdNames.nme @@ -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 diff --git a/compiler/src/dotty/tools/dotc/transform/InlinePatterns.scala b/compiler/src/dotty/tools/dotc/transform/InlinePatterns.scala new file mode 100644 index 000000000000..c1e11be46a6c --- /dev/null +++ b/compiler/src/dotty/tools/dotc/transform/InlinePatterns.scala @@ -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) + + 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 = + 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 diff --git a/compiler/src/dotty/tools/dotc/typer/Inliner.scala b/compiler/src/dotty/tools/dotc/typer/Inliner.scala index fe21741080cd..a62c3508d02a 100644 --- a/compiler/src/dotty/tools/dotc/typer/Inliner.scala +++ b/compiler/src/dotty/tools/dotc/typer/Inliner.scala @@ -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 * diff --git a/compiler/src/dotty/tools/dotc/typer/PrepareInlineable.scala b/compiler/src/dotty/tools/dotc/typer/PrepareInlineable.scala index d19e4f5b6f0d..c00ee84431d5 100644 --- a/compiler/src/dotty/tools/dotc/typer/PrepareInlineable.scala +++ b/compiler/src/dotty/tools/dotc/typer/PrepareInlineable.scala @@ -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) { diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 0b10792d84a8..263dc4e32575 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -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 } } diff --git a/tests/neg/inine-unnapply.scala b/tests/neg/inine-unnapply.scala deleted file mode 100644 index db82997e5648..000000000000 --- a/tests/neg/inine-unnapply.scala +++ /dev/null @@ -1,5 +0,0 @@ - -object Foo { - inline def unapply(x: Any): Boolean = ??? // error: Implementation restriction: inline unapply methods are not supported - inline def unapplySeq(x: Any): Seq[Any] = ??? // error: Implementation restriction: inline unapplySeq methods are not supported -} diff --git a/tests/neg/inline-unapply.scala b/tests/neg/inline-unapply.scala deleted file mode 100644 index 818bc38f9c82..000000000000 --- a/tests/neg/inline-unapply.scala +++ /dev/null @@ -1,15 +0,0 @@ -object Test { - - class C(val x: Int, val y: Int) - - inline def unapply(c: C): Some[(Int, Int)] = Some((c.x, c.y)) // error: Implementation restriction: inline unapply methods are not supported - -} -object Test2 { - - class C(x: Int, y: Int) - - inline def unapply(c: C): Option[(Int, Int)] = inline c match { // error: Implementation restriction: inline unapply methods are not supported - case x: C => Some((1, 1)) - } -} \ No newline at end of file diff --git a/tests/pos/i8530.scala b/tests/pos/i8530.scala new file mode 100644 index 000000000000..2eab9f9e74c8 --- /dev/null +++ b/tests/pos/i8530.scala @@ -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 diff --git a/tests/pos/inine-unnapply.scala b/tests/pos/inine-unnapply.scala new file mode 100644 index 000000000000..22ad8eb3526c --- /dev/null +++ b/tests/pos/inine-unnapply.scala @@ -0,0 +1,5 @@ + +object Foo { + inline def unapply(x: Any): Boolean = ??? + inline def unapplySeq(x: Any): Seq[Any] = ??? +} diff --git a/tests/pos/inline-unapply.scala b/tests/pos/inline-unapply.scala new file mode 100644 index 000000000000..e352f08e2315 --- /dev/null +++ b/tests/pos/inline-unapply.scala @@ -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)) + } +} diff --git a/tests/run-macros/i8530-2.check b/tests/run-macros/i8530-2.check new file mode 100644 index 000000000000..257cc5642cb1 --- /dev/null +++ b/tests/run-macros/i8530-2.check @@ -0,0 +1 @@ +foo diff --git a/tests/run-macros/i8530/App_2.scala b/tests/run-macros/i8530/App_2.scala new file mode 100644 index 000000000000..3d0d6b665128 --- /dev/null +++ b/tests/run-macros/i8530/App_2.scala @@ -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) + } + +} diff --git a/tests/run-macros/i8530/Macro_1.scala b/tests/run-macros/i8530/Macro_1.scala new file mode 100644 index 000000000000..2d49b71383e3 --- /dev/null +++ b/tests/run-macros/i8530/Macro_1.scala @@ -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)} diff --git a/tests/run/i8530-b.scala b/tests/run/i8530-b.scala new file mode 100644 index 000000000000..80171bc4d178 --- /dev/null +++ b/tests/run/i8530-b.scala @@ -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") \ No newline at end of file diff --git a/tests/run/i8530.check b/tests/run/i8530.check new file mode 100644 index 000000000000..c8f75a704ff7 --- /dev/null +++ b/tests/run/i8530.check @@ -0,0 +1,5 @@ +MyBoooleanUnapply +2 +3 +(4,5) +5 diff --git a/tests/run/i8530.scala b/tests/run/i8530.scala new file mode 100644 index 000000000000..bbbc52587ee0 --- /dev/null +++ b/tests/run/i8530.scala @@ -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