diff --git a/compiler/src/dotty/tools/dotc/inlines/Inliner.scala b/compiler/src/dotty/tools/dotc/inlines/Inliner.scala index 2b2f012c688c..f61d85c3a18f 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inliner.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inliner.scala @@ -394,6 +394,11 @@ class Inliner(val call: tpd.Tree)(using Context): case (from, to) if from.symbol == ref.symbol && from =:= ref => to } + private def mapRefBack(ref: TermRef): Option[TermRef] = + opaqueProxies.collectFirst { + case (from, to) if to.symbol == ref.symbol && to =:= ref => from + } + /** If `tp` contains TermRefs that refer to objects with opaque * type aliases, add proxy definitions to `opaqueProxies` that expose these aliases. */ @@ -438,6 +443,19 @@ class Inliner(val call: tpd.Tree)(using Context): } ) + /** Map back all TermRefs that match the right element in `opaqueProxies` to the + * corresponding left element. + */ + protected val mapBackToOpaques = TreeTypeMap( + typeMap = new TypeMap: + override def stopAt = StopAt.Package + def apply(t: Type) = mapOver { + t match + case ref: TermRef => mapRefBack(ref).getOrElse(ref) + case _ => t + } + ) + /** If `binding` contains TermRefs that refer to objects with opaque * type aliases, add proxy definitions that expose these aliases * and substitute such TermRefs with theproxies. Example from pos/opaque-inline1.scala: @@ -487,6 +505,28 @@ class Inliner(val call: tpd.Tree)(using Context): private def adaptToPrefix(tp: Type) = tp.asSeenFrom(inlineCallPrefix.tpe, inlinedMethod.owner) + def thisTypeProxyExists = !thisProxy.isEmpty + + // Unpacks `val ObjectDef$_this: ObjectDef.type = ObjectDef` reference back into ObjectDef reference + // For nested transparent inline calls, ObjectDef will be an another proxy, but that is okay + val thisTypeUnpacker = + TreeTypeMap( + typeMap = new TypeMap: + override def stopAt = StopAt.Package + def apply(t: Type) = mapOver { + t match + case a: TermRef if thisProxy.values.exists(_ == a) => + a.termSymbol.defTree match + case untpd.ValDef(a, tpt, _) => tpt.tpe + case _ => t + } + ) + + def unpackProxiesFromResultType(inlined: Inlined): Type = + if thisTypeProxyExists then mapBackToOpaques.typeMap(thisTypeUnpacker.typeMap(inlined.expansion.tpe)) + else inlined.tpe + + /** Populate `thisProxy` and `paramProxy` as follows: * * 1a. If given type refers to a static this, thisProxy binds it to corresponding global reference, diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index 59386dd9bd4d..802db5bf68b0 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -573,10 +573,16 @@ object Inlines: // different for bindings from arguments and bindings from body. val inlined = tpd.Inlined(call, bindings, expansion) - if !hasOpaqueProxies then inlined + val hasOpaquesInResultFromCallWithTransparentContext = + call.tpe.widenTermRefExpr.existsPart( + part => part.typeSymbol.is(Opaque) && call.symbol.ownersIterator.contains(part.typeSymbol.owner) + ) + + if !hasOpaqueProxies && !hasOpaquesInResultFromCallWithTransparentContext then inlined else val target = - if inlinedMethod.is(Transparent) then call.tpe & inlined.tpe + if inlinedMethod.is(Transparent) then + call.tpe & unpackProxiesFromResultType(inlined) else call.tpe inlined.ensureConforms(target) // Make sure that the sealing with the declared type diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index def6fac0556e..1f0908b29d17 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -109,6 +109,13 @@ object Typer { */ private[typer] val HiddenSearchFailure = new Property.Key[List[SearchFailure]] + + /** An attachment on a Typed node. Indicates that the Typed node was synthetically + * inserted by the Typer phase. We might want to remove it for the purpose of inlining, + * but only if it was not manually inserted by the user. + */ + private[typer] val InsertedTyped = new Property.Key[Unit] + /** Is tree a compiler-generated `.apply` node that refers to the * apply of a function class? */ @@ -3029,7 +3036,10 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val rhs1 = excludeDeferredGiven(ddef.rhs, sym): rhs => PrepareInlineable.dropInlineIfError(sym, if sym.isScala2Macro then typedScala2MacroBody(rhs)(using rhsCtx) - else typedExpr(rhs, tpt1.tpe.widenExpr)(using rhsCtx)) + else + typedExpr(rhs, tpt1.tpe.widenExpr)(using rhsCtx)) match + case typed @ Typed(outer, _) if typed.hasAttachment(InsertedTyped) => outer + case other => other if sym.isInlineMethod then if StagingLevel.level > 0 then @@ -4678,7 +4688,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer insertGadtCast(tree, wtp, pt) case CompareResult.OKwithOpaquesUsed if !tree.tpe.frozen_<:<(pt)(using ctx.withOwner(defn.RootClass)) => // guard to avoid extra Typed trees, eg. from testSubType(O.T, O.T) which returns OKwithOpaquesUsed - Typed(tree, TypeTree(pt)) + Typed(tree, TypeTree(pt)).withAttachment(InsertedTyped, ()) case _ => //typr.println(i"OK ${tree.tpe}\n${TypeComparer.explained(_.isSubType(tree.tpe, pt))}") // uncomment for unexpected successes tree diff --git a/docs/_docs/reference/other-new-features/opaques-details.md b/docs/_docs/reference/other-new-features/opaques-details.md index d285ec8e8325..e6c2ff80649e 100644 --- a/docs/_docs/reference/other-new-features/opaques-details.md +++ b/docs/_docs/reference/other-new-features/opaques-details.md @@ -109,6 +109,45 @@ object obj: ``` The opaque type alias `A` is transparent in its scope, which includes the definition of `x`, but not the definitions of `obj` and `y`. +## Opaque Types in Transparent Inline Methods + +Additional care is required if an opaque type is returned from a transparent inline method, located inside a context where that opaque type is defined. +Since the typechecking and type inference of the body of the method is done from the perspective of that context, the returned types might contain dealiased opaque types. Generally, this means that calls to those transparent methods will return a `DECLARED & ACTUAL`, where `DECLARED` is the return type defined in the method declaration, and `ACTUAL` is the type returned after the inlining, which might include dealiased opaque types. + +API designers can ensure that the correct type is returned by explicitly annotating it inside of the method body with `: ExpectedType` or by explicitly passing type parameters to the method being returned. Explicitly annotating like this will help for the outermost transparent inline method calls, but will not affect the nested calls, as, from the perspective of the new context into which we are inlining, those might still have to be dealiased to avoid compilation errors: + +```scala +object Time: + opaque type Time = String + opaque type Seconds <: Time = String + + // opaque type aliases have to be dealiased in nested calls, + // otherwise the resulting program might not be typed correctly + // in the below methods this will be typed as Seconds & String despite + // the explicit type declaration + transparent inline def sec(n: Double): Seconds = + s"${n}s": Seconds + + transparent inline def testInference(): List[Time] = + List(sec(5)) // infers List[String] and returns List[Time] & List[String], not List[Seconds] + transparent inline def testGuarded(): List[Time] = + List(sec(5)): List[Seconds] // returns List[Seconds] + transparent inline def testExplicitTime(): List[Time] = + List[Seconds](sec(5)) // returns List[Seconds] + transparent inline def testExplicitString(): List[Time] = + List[String](sec(5)) // returns List[Time] & List[String] + +end Time + +@main def main() = + val t1: List[String] = Time.testInference() // returns List[Time.Time] & List[String] + val t2: List[Time.Seconds] = Time.testGuarded() // returns List[Time.Seconds] + val t3: List[Time.Seconds] = Time.testExplicitTime() // returns List[Time.Seconds] + val t4: List[String] = Time.testExplicitString() // returns List[Time.Time] & List[String] +``` + +Be careful especially if what is being inlined depends on the type of those nested transparent calls. +``` ## Relationship to SIP 35 diff --git a/tests/neg/i13461.scala b/tests/neg/i13461.scala new file mode 100644 index 000000000000..70ec689601f1 --- /dev/null +++ b/tests/neg/i13461.scala @@ -0,0 +1,9 @@ +package i13461: + + opaque type Opaque = Int + transparent inline def op: Opaque = (123: Opaque) + + object Main: + def main(args: Array[String]): Unit = + val o22: 123 = op // error + diff --git a/tests/pos/i13461-c.scala b/tests/pos/i13461-c.scala new file mode 100644 index 000000000000..ee5eff463b5a --- /dev/null +++ b/tests/pos/i13461-c.scala @@ -0,0 +1,23 @@ +object Time: + opaque type Time = String + opaque type Seconds <: Time = String + + transparent inline def sec(n: Double): Seconds = + s"${n}s": Seconds // opaque type aliases have to be dealiased in nested calls, otherwise the resulting program might not be typed correctly + + transparent inline def testInference(): List[Time] = + List(sec(5)) // infers List[String] and returns List[Time] & List[String], not List[Seconds] + transparent inline def testGuarded(): List[Time] = + List(sec(5)): List[Seconds] // returns List[Seconds] + transparent inline def testExplicitTime(): List[Time] = + List[Seconds](sec(5)) // returns List[Seconds] + transparent inline def testExplicitString(): List[Time] = + List[String](sec(5)) // returns List[Time] & List[String] + +end Time + +@main def main() = + val t1: List[String] = Time.testInference() // returns List[Time.Time] & List[String] + val t2: List[Time.Seconds] = Time.testGuarded() // returns List[Time.Seconds] + val t3: List[Time.Seconds] = Time.testExplicitTime() // returns List[Time.Seconds] + val t4: List[String] = Time.testExplicitString() // returns List[Time.Time] & List[String] diff --git a/tests/pos/i13461.scala b/tests/pos/i13461.scala new file mode 100644 index 000000000000..6391681c07b1 --- /dev/null +++ b/tests/pos/i13461.scala @@ -0,0 +1,12 @@ +package i13461: + + opaque type Opaque = Int + transparent inline def op: Opaque = 123 + transparent inline def oop: i13461.Opaque = 123 + + object Main: + def main(args: Array[String]): Unit = + val o2: Opaque = op + val o3: Opaque = oop // needs to be unwrapped from Typed generated in adapt + val o22: 123 = op + val o23: 123 = oop diff --git a/tests/run/i13461-b.check b/tests/run/i13461-b.check new file mode 100644 index 000000000000..62c33240b000 --- /dev/null +++ b/tests/run/i13461-b.check @@ -0,0 +1,4 @@ +35s +35m +15s, 20m, 15m, 20s +15s, 15m, 15s, 20m diff --git a/tests/run/i13461-b.scala b/tests/run/i13461-b.scala new file mode 100644 index 000000000000..e384168158d2 --- /dev/null +++ b/tests/run/i13461-b.scala @@ -0,0 +1,60 @@ +// TODO taken from issue +object Time: + opaque type Time = String + opaque type Seconds <: Time = String + opaque type Minutes <: Time = String + opaque type Mixed <: Time = String + + type Units = Seconds | Minutes + + def sec(n: Int): Seconds = + s"${n}s" + + def min(n: Int): Minutes = + s"${n}m" + + def mixed(t1: Time, t2: Time): Mixed = + s"${t1}, ${t2}" + + extension (t: Units) + def number: Int = + (t : String).init.toInt + + extension [T1 <: Time](inline a: T1) + transparent inline def +[T2 <: Time](inline b: T2): Time = + inline (a, b) match + case x: (Seconds, Seconds) => + (sec(x._1.number + x._2.number)) + + case x: (Minutes, Minutes) => + (min(x._1.number + x._2.number)) + + case x: (Time, Time) => + (mixed(x._1, x._2)) + end + +end Time + +import Time.* + +// Test seconds +val a = sec(15) +val b = sec(20) + +// Test minutes +val x = min(15) +val y = min(20) + +// Test mixes +val m1 = a + y +val m2 = x + b + +// Test upper type +val t1: Time = a +val t2: Time = x +val t3: Time = m1 + +@main def Test() = + println(a + b) + println(x + y) + println(m1 + m2) + println(t1 + t2 + t3)