Skip to content

Commit 279c185

Browse files
committed
Handle opaque types when inlining.
The idea is described in part in the comments to PR #12815. 1. If a this-proxy definition contains references to classes that see opaque types, replace those references by other proxies that expose the opaque alias in a refinement type of the original reference type. 2. Make sure this proxies exist even for static moules containing opaque types, so that step 1 can be applied. 3. With steps 1 and 2, we can drop the restriction that inline methods may not be defined where opaque aliases are visible.
1 parent 9a998d4 commit 279c185

17 files changed

+312
-33
lines changed

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

Lines changed: 107 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -556,18 +556,92 @@ class Inliner(call: tpd.Tree, rhsToInline: tpd.Tree)(using Context) {
556556
ref(lastSelf).outerSelect(lastLevel - level, selfSym.info)
557557
else
558558
inlineCallPrefix
559-
val binding = ValDef(selfSym.asTerm, QuoteUtils.changeOwnerOfTree(rhs, selfSym)).withSpan(selfSym.span)
559+
val binding = accountForOpaques(
560+
ValDef(selfSym.asTerm, QuoteUtils.changeOwnerOfTree(rhs, selfSym)).withSpan(selfSym.span))
560561
bindingsBuf += binding
561562
inlining.println(i"proxy at $level: $selfSym = ${bindingsBuf.last}")
562563
lastSelf = selfSym
563564
lastLevel = level
564565
}
565566
}
566567

568+
/** A list of pairs between TermRefs appearing in thisProxy bindings that
569+
* refer to objects with opaque type aliases and local proxy symbols
570+
* that contain refined versions of these TermRefs where the aliases
571+
* are exposed.
572+
*/
573+
private val opaqueProxies = new mutable.ListBuffer[(TermRef, TermRef)]
574+
575+
/** Map first halfs of opaqueProxies pairs to second halfs, using =:= as equality */
576+
def mapRef(ref: TermRef): Option[TermRef] =
577+
opaqueProxies
578+
.find((from, to) => from.symbol == ref.symbol && from =:= ref)
579+
.map(_._2)
580+
581+
/** If `binding` contains TermRefs that refer to objects with opaque
582+
* type aliases, add proxy definitions that expose these aliases
583+
* and substitute such TermRefs with theproxies. Example from pos/opaque-inline1.scala:
584+
*
585+
* object refined:
586+
* opaque type Positive = Int
587+
* inline def Positive(value: Int): Positive = f(value)
588+
* def f(x: Positive): Positive = x
589+
* def run: Unit = { val x = 9; val nine = refined.Positive(x) }
590+
*
591+
* This generates the following proxies:
592+
*
593+
* val $proxy1: refined.type{type Positive = Int} =
594+
* refined.$asInstanceOf$[refined.type{type Positive = Int}]
595+
* val refined$_this: ($proxy1 : refined.type{Positive = Int}) =
596+
* $proxy1
597+
*
598+
* and every reference to `refined` in the inlined expression is replaced by
599+
* `refined_$this`.
600+
*/
601+
def accountForOpaques(binding: ValDef)(using Context): ValDef =
602+
binding.symbol.info.foreachPart {
603+
case ref: TermRef =>
604+
for cls <- ref.widen.classSymbols do
605+
if cls.containsOpaques && mapRef(ref).isEmpty then
606+
def openOpaqueAliases(selfType: Type): List[(Name, Type)] = selfType match
607+
case RefinedType(parent, rname, TypeAlias(alias)) =>
608+
val opaq = cls.info.member(rname).symbol
609+
if opaq.isOpaqueAlias then
610+
(rname, alias.stripLazyRef.asSeenFrom(ref, cls))
611+
:: openOpaqueAliases(parent)
612+
else Nil
613+
case _ =>
614+
Nil
615+
val refinements = openOpaqueAliases(cls.givenSelfType)
616+
val refinedType = refinements.foldLeft(ref: Type) ((parent, refinement) =>
617+
RefinedType(parent, refinement._1, TypeAlias(refinement._2))
618+
)
619+
val refiningSym = newSym(InlineBinderName.fresh(), Synthetic, refinedType).asTerm
620+
val refiningDef = ValDef(refiningSym, tpd.ref(ref).cast(refinedType)).withSpan(binding.span)
621+
inlining.println(i"add opaque alias proxy $refiningDef")
622+
bindingsBuf += refiningDef
623+
opaqueProxies += ((ref, refiningSym.termRef))
624+
case _ =>
625+
}
626+
if opaqueProxies.isEmpty then binding
627+
else
628+
val mapType = new TypeMap:
629+
override def stopAt = StopAt.Package
630+
def apply(t: Type) = mapOver {
631+
t match
632+
case ref: TermRef => mapRef(ref).getOrElse(ref)
633+
case _ => t
634+
}
635+
binding.symbol.info = mapType(binding.symbol.info)
636+
val mapTree = TreeTypeMap(typeMap = mapType)
637+
mapTree.transform(binding).asInstanceOf[ValDef]
638+
.showing(i"transformed this binding exposing opaque aliases: $result", inlining)
639+
end accountForOpaques
640+
567641
private def canElideThis(tpe: ThisType): Boolean =
568-
inlineCallPrefix.tpe == tpe && ctx.owner.isContainedIn(tpe.cls) ||
569-
tpe.cls.isContainedIn(inlinedMethod) ||
570-
tpe.cls.is(Package)
642+
inlineCallPrefix.tpe == tpe && ctx.owner.isContainedIn(tpe.cls)
643+
|| tpe.cls.isContainedIn(inlinedMethod)
644+
|| tpe.cls.is(Package)
571645

572646
/** Very similar to TreeInfo.isPureExpr, but with the following inliner-only exceptions:
573647
* - synthetic case class apply methods, when the case class constructor is empty, are
@@ -666,12 +740,25 @@ class Inliner(call: tpd.Tree, rhsToInline: tpd.Tree)(using Context) {
666740
case _ =>
667741
}
668742

743+
private val registerTypes = new TypeTraverser:
744+
override def stopAt = StopAt.Package
745+
// Only register ThisType prefixes that see opaques. No need to register the others
746+
// since they are static prefixes.
747+
def registerStaticPrefix(t: Type): Unit = t match
748+
case t: ThisType if t.cls.seesOpaques => registerType(t)
749+
case t: NamedType => registerStaticPrefix(t.prefix)
750+
case _ =>
751+
override def traverse(t: Type) = t match
752+
case t: NamedType if t.currentSymbol.isStatic =>
753+
registerStaticPrefix(t.prefix)
754+
case t =>
755+
registerType(t)
756+
traverseChildren(t)
757+
669758
/** Register type of leaf node */
670-
private def registerLeaf(tree: Tree): Unit = tree match {
671-
case _: This | _: Ident | _: TypeTree =>
672-
tree.typeOpt.foreachPart(registerType, StopAt.Static)
759+
private def registerLeaf(tree: Tree): Unit = tree match
760+
case _: This | _: Ident | _: TypeTree => registerTypes.traverse(tree.typeOpt)
673761
case _ =>
674-
}
675762

676763
/** Make `tree` part of inlined expansion. This means its owner has to be changed
677764
* from its `originalOwner`, and, if it comes from outside the inlined method
@@ -797,6 +884,8 @@ class Inliner(call: tpd.Tree, rhsToInline: tpd.Tree)(using Context) {
797884
val inliner = new InlinerMap(
798885
typeMap =
799886
new DeepTypeMap {
887+
override def stopAt =
888+
if opaqueProxies.isEmpty then StopAt.Static else StopAt.Package
800889
def apply(t: Type) = t match {
801890
case t: ThisType => thisProxy.getOrElse(t.cls, t)
802891
case t: TypeRef => paramProxy.getOrElse(t, mapOver(t))
@@ -843,7 +932,16 @@ class Inliner(call: tpd.Tree, rhsToInline: tpd.Tree)(using Context) {
843932

844933
// Apply inliner to `rhsToInline`, split off any implicit bindings from result, and
845934
// make them part of `bindingsBuf`. The expansion is then the tree that remains.
846-
val expansion = inliner.transform(rhsToInline)
935+
val expansion0 = inliner.transform(rhsToInline)
936+
val expansion =
937+
if opaqueProxies.nonEmpty && !inlinedMethod.is(Transparent) then
938+
expansion0.cast(call.tpe)(using ctx.withSource(expansion0.source))
939+
// the cast makes sure that the sealing with the declared type
940+
// is type correct. Without it we might get problems since the
941+
// expression's type is the opaque alias but the call's type is
942+
// the opaque type itself. An example is in pos/opaque-inline1.scala.
943+
else
944+
expansion0
847945

848946
def issueError() = callValueArgss match {
849947
case (msgArg :: Nil) :: Nil =>

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -259,8 +259,6 @@ object PrepareInlineable {
259259
}
260260

261261
private def checkInlineMethod(inlined: Symbol, body: Tree)(using Context): body.type = {
262-
if (inlined.owner.isClass && inlined.owner.seesOpaques)
263-
report.error(em"Implementation restriction: No inline methods allowed where opaque type aliases are in scope", inlined.srcPos)
264262
if Inliner.inInlineMethod(using ctx.outer) then
265263
report.error(ex"Implementation restriction: nested inline methods are not supported", inlined.srcPos)
266264

tests/neg/i6662.scala

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

tests/pos/i6662.scala

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
object opt:
2+
opaque type Opt[A >: Null] = A
3+
object Opt:
4+
inline def unOpt[A >: Null](x: Opt[A]): A = x
5+
inline def apply[A >: Null](x: A): Opt[A] = x
6+
inline def some[A >: Null](x: A): Opt[A] = x
7+
inline def none[A >: Null]: Opt[A] = null
8+
inline def fromOption[A >: Null](x: Option[A]) = x.orNull
9+
10+
import opt.Opt
11+
extension [A >: Null](x: Opt[A])
12+
inline def nonEmpty : Boolean = x.get != null
13+
inline def isEmpty : Boolean = x.get == null
14+
inline def isDefined: Boolean = x.nonEmpty
15+
inline def get : A = Opt.unOpt(x)
16+
17+
@main def Test =
18+
val x: Opt[String] = Opt.some("abc")
19+
assert(x.nonEmpty)
20+
val y: String = Opt.unOpt(x)

tests/neg/i6854.scala renamed to tests/pos/i6854.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@ object Lib {
77
opaque type IArray2[+T] = Array[_ <: T]
88

99
object IArray2 {
10-
inline def apply(x: =>Int): IArray2[Int] = Array(x) // error
10+
inline def apply(x: =>Int): IArray2[Int] = Array(x)
1111
}
1212
}

tests/neg/inline3.scala renamed to tests/pos/inline3.scala

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,13 @@ object K0 {
44

55
opaque type ProductInstances[F[_], T] = ErasedProductInstances[F[T]]
66

7-
inline def summonAsArray[F[_], T]: Array[Any] = ??? // error: Implementation restriction: No inline methods allowed
8-
9-
inline def mkProductInstances[F[_], T]: ProductInstances[F, T] = // error: Implementation restriction: No inline methods allowed
7+
inline def summonAsArray[F[_], T]: Array[Any] = ???
8+
inline def mkProductInstances[F[_], T]: ProductInstances[F, T] =
109
new ErasedProductInstances(summonAsArray[F, T]).asInstanceOf[ProductInstances[F, T]]
1110

1211
val x: T = ""
1312

14-
inline def foo(x: T): T = "foo".asInstanceOf[T] // error: Implementation restriction: No inline methods allowed
15-
13+
inline def foo(x: T): T = "foo".asInstanceOf[T]
1614
}
1715

1816
final class ErasedProductInstances[FT](is0: => Array[Any])
@@ -21,7 +19,6 @@ trait Monoid[A]
2119
case class ISB(i: Int)
2220

2321
object Test {
24-
//val K0 = new K0
2522
K0.foo(K0.x)
2623
K0.mkProductInstances[Monoid, ISB]
2724

tests/pos/opaque-inline.scala

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
2+
object refined:
3+
opaque type Positive = Int
4+
5+
object Positive extends PositiveFactory
6+
7+
trait PositiveFactory:
8+
inline def apply(value: Int): Positive = value
9+
10+
def f(x: Positive): Positive = x
11+
inline def fapply(value: Int): Positive =
12+
val vv = (value, value) // error: implementation restriction
13+
f(vv._1)
14+
15+
@main def run: Unit =
16+
import refined.*
17+
val x = 9
18+
val nine = Positive.apply(x)
19+
val nine1 = Positive.fapply(x)
20+
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
2+
object refined:
3+
opaque type Positive = Int
4+
transparent inline def Positive(value: Int): Positive = f(value)
5+
def f(x: Positive): Positive = x
6+
7+
object test:
8+
def run: Unit =
9+
val x = 9
10+
val nine = refined.Positive(x)
11+

tests/pos/opaque-inline1.scala

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
2+
object refined:
3+
opaque type Positive = Int
4+
inline def Positive(value: Int): Positive = f(value)
5+
def f(x: Positive): Positive = x
6+
7+
object test:
8+
def run: Unit =
9+
val x = 9
10+
val nine = refined.Positive(x)
11+
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
2+
import compiletime.*
3+
4+
object refined:
5+
opaque type Positive = Int
6+
7+
object Positive extends PositiveFactory
8+
9+
trait PositiveFactory:
10+
transparent inline def apply(inline value: Int): Positive =
11+
inline if value < 0 then error(codeOf(value) + " is not positive.")
12+
else value
13+
14+
transparent inline def safe(value: Int): Positive | IllegalArgumentException =
15+
if value < 0 then IllegalArgumentException(s"$value is not positive")
16+
else value: Positive
17+
18+
@main def Test: Unit =
19+
import refined.*
20+
val eight = Positive(8)
21+
// val negative = Positive(-1) // This correctly produces a compile error "-1 is not positive."
22+
// val random = Positive(scala.util.Random.nextInt()) // This correctly produces a compile error about being unable to inline the method call
23+
val random = Positive.safe(scala.util.Random.nextInt())
24+
val safeNegative = Positive.safe(-1)
25+
val safeFive = Positive.safe(5)
26+
println(eight)
27+
println(random)
28+
println(safeFive)

0 commit comments

Comments
 (0)