Skip to content

Commit b992ab2

Browse files
authored
Merge pull request #601 from scala/backport-lts-3.3-23792
Backport "Prevent opaque types leaking from transparent inline methods" to 3.3 LTS
2 parents b9da060 + 2172342 commit b992ab2

File tree

11 files changed

+276
-6
lines changed

11 files changed

+276
-6
lines changed

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

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,11 @@ class Inliner(val call: tpd.Tree)(using Context):
394394
case (from, to) if from.symbol == ref.symbol && from =:= ref => to
395395
}
396396

397+
private def mapRefBack(ref: TermRef): Option[TermRef] =
398+
opaqueProxies.collectFirst {
399+
case (from, to) if to.symbol == ref.symbol && to =:= ref => from
400+
}
401+
397402
/** If `tp` contains TermRefs that refer to objects with opaque
398403
* type aliases, add proxy definitions to `opaqueProxies` that expose these aliases.
399404
*/
@@ -438,6 +443,22 @@ class Inliner(val call: tpd.Tree)(using Context):
438443
}
439444
)
440445

446+
/** Map back all TermRefs that match the right element in `opaqueProxies` to the
447+
* corresponding left element.
448+
* E.g. for a previously created
449+
* `val proxy$1 = Time {type OpaqueInt = Int}` as part of the ongoing inlining
450+
* a `List[proxy$1.OpaqueInt]` will be mapped back into a `List[Time.OpaqueInt]`.
451+
*/
452+
protected val mapBackToOpaques = TreeTypeMap(
453+
typeMap = new TypeMap:
454+
override def stopAt = StopAt.Package
455+
def apply(t: Type) = mapOver {
456+
t match
457+
case ref: TermRef => mapRefBack(ref).getOrElse(ref)
458+
case _ => t
459+
}
460+
)
461+
441462
/** If `binding` contains TermRefs that refer to objects with opaque
442463
* type aliases, add proxy definitions that expose these aliases
443464
* and substitute such TermRefs with theproxies. Example from pos/opaque-inline1.scala:
@@ -487,6 +508,48 @@ class Inliner(val call: tpd.Tree)(using Context):
487508

488509
private def adaptToPrefix(tp: Type) = tp.asSeenFrom(inlineCallPrefix.tpe, inlinedMethod.owner)
489510

511+
def thisTypeProxyExists = !thisProxy.isEmpty
512+
513+
/** Maps a type that includes a thisProxy (e.g. `TermRef(NoPrefix,val Foo$_this)`)
514+
* by reading the defTree belonging to that thisProxy (`val Foo$_this: Foo.type = AnotherProxy`)
515+
* back into its original reference (`AnotherProxy`, which is directly or indirectly a refinement on `Foo`)
516+
*
517+
* Usually when we end up with another proxy like this, we will be able to further unwrap it back
518+
* into `Foo` with mapBackToOpaques, but, for nested transparent inline calls, `AnotherProxy` will be
519+
* a proxy created by inlining the outer calls, that we might not be able to further unwrap this way
520+
* (as those proxies will not be a part of opaqueProxies created during this inlining).
521+
* We leave that as it is and treat this behavior as intended (see documentation with an example in
522+
* `Opaque Types in Transparent Inline Methods` section in `opaques-details.md`),
523+
* as we might need those opaques to have visible right hand sides for successful
524+
* typechecking of the outer inline call.
525+
*/
526+
val thisTypeUnpacker =
527+
TreeTypeMap(
528+
typeMap = new TypeMap:
529+
override def stopAt = StopAt.Package
530+
def apply(t: Type) = mapOver {
531+
t match
532+
case a: TermRef if thisProxy.values.exists(_ == a) =>
533+
a.termSymbol.defTree match
534+
case untpd.ValDef(a, tpt, _) => tpt.tpe
535+
case _ => t
536+
}
537+
)
538+
539+
/** Returns the result type of the Inlined code block after removing thisProxy and opaqueProxy TermRefs.
540+
* E.g. for an Inlined tree returning Block of type `Option[Foo$_this.OpaqueInt]`,
541+
* and for proxies:
542+
* ```
543+
* val $proxy1: Foo.type{type OpaqueInt = Int} = = ...
544+
* val Foo$_this: ($proxy1 : Foo.type{type OpaqueInt = Int}) = ...
545+
* ```
546+
* the method will return: `Foo.OpaqueInt`
547+
*/
548+
def unpackProxiesFromResultType(inlined: Inlined): Type =
549+
if thisTypeProxyExists then mapBackToOpaques.typeMap(thisTypeUnpacker.typeMap(inlined.expansion.tpe))
550+
else inlined.tpe
551+
552+
490553
/** Populate `thisProxy` and `paramProxy` as follows:
491554
*
492555
* 1a. If given type refers to a static this, thisProxy binds it to corresponding global reference,

compiler/src/dotty/tools/dotc/inlines/Inlines.scala

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -532,12 +532,47 @@ object Inlines:
532532
// different for bindings from arguments and bindings from body.
533533
val inlined = tpd.Inlined(call, bindings, expansion)
534534

535-
if !hasOpaqueProxies then inlined
535+
val hasOpaquesInResultFromCallWithTransparentContext =
536+
val owners = call.symbol.ownersIterator.toSet
537+
call.tpe.widenTermRefExpr.existsPart(
538+
part => part.typeSymbol.is(Opaque) && owners.contains(part.typeSymbol.owner)
539+
)
540+
541+
/** Remap ThisType nodes that are incorrect in the inlined context.
542+
* Incorrect ThisType nodes can cause unwanted opaque type dealiasing later.
543+
* E.g. if inlined in a `<root>.Foo` package (but outside of <root>.Foo.Bar object) we will map
544+
* `TermRef(ThisType(TypeRef(ThisType(TypeRef(TermRef(ThisType(TypeRef(NoPrefix,module class <root>)),object Foo),Bar$)),MyOpaque$)),one)`
545+
* into
546+
* `TermRef(TermRef(TermRef(TermRef(ThisType(TypeRef(NoPrefix,module class <root>)),object Foo),object Bar),object MyOpaque),val one)`
547+
* See test i13461-d
548+
*/
549+
def fixThisTypeModuleClassReferences(tpe: Type): Type =
550+
val owners = ctx.owner.ownersIterator.toSet
551+
TreeTypeMap(
552+
typeMap = new TypeMap:
553+
override def stopAt = StopAt.Package
554+
def apply(t: Type) = mapOver {
555+
t match
556+
case ThisType(tref @ TypeRef(prefix, _)) if tref.symbol.flags.is(Module) && !owners.contains(tref.symbol) =>
557+
TermRef(apply(prefix), tref.symbol.companionModule)
558+
case _ => mapOver(t)
559+
}
560+
).typeMap(tpe)
561+
562+
if !hasOpaqueProxies && !hasOpaquesInResultFromCallWithTransparentContext then inlined
536563
else
537-
val target =
538-
if inlinedMethod.is(Transparent) then call.tpe & inlined.tpe
539-
else call.tpe
540-
inlined.ensureConforms(target)
564+
val (target, forceCast) =
565+
if inlinedMethod.is(Transparent) then
566+
val unpacked = unpackProxiesFromResultType(inlined)
567+
val withAdjustedThisTypes = if call.symbol.is(Macro) then fixThisTypeModuleClassReferences(unpacked) else unpacked
568+
(call.tpe & withAdjustedThisTypes, withAdjustedThisTypes != unpacked)
569+
else (call.tpe, false)
570+
if forceCast then
571+
// we need to force the cast for issues with ThisTypes, as ensureConforms will just
572+
// check subtyping and then choose not to cast, leaving the previous, incorrect type
573+
inlined.cast(target)
574+
else
575+
inlined.ensureConforms(target)
541576
// Make sure that the sealing with the declared type
542577
// is type correct. Without it we might get problems since the
543578
// expression's type is the opaque alias but the call's type is

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,13 @@ object Typer {
102102
*/
103103
private[typer] val HiddenSearchFailure = new Property.Key[List[SearchFailure]]
104104

105+
106+
/** An attachment on a Typed node. Indicates that the Typed node was synthetically
107+
* inserted by the Typer phase. We might want to remove it for the purpose of inlining,
108+
* but only if it was not manually inserted by the user.
109+
*/
110+
private[typer] val InsertedTyped = new Property.Key[Unit]
111+
105112
/** Is tree a compiler-generated `.apply` node that refers to the
106113
* apply of a function class?
107114
*/
@@ -2579,7 +2586,10 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
25792586
if sym.is(ExtensionMethod) then rhsCtx.addMode(Mode.InExtensionMethod)
25802587
val rhs1 = PrepareInlineable.dropInlineIfError(sym,
25812588
if sym.isScala2Macro then typedScala2MacroBody(ddef.rhs)(using rhsCtx)
2582-
else typedExpr(ddef.rhs, tpt1.tpe.widenExpr)(using rhsCtx))
2589+
else
2590+
typedExpr(ddef.rhs, tpt1.tpe.widenExpr)(using rhsCtx)) match
2591+
case typed @ Typed(outer, _) if typed.hasAttachment(InsertedTyped) => outer
2592+
case other => other
25832593

25842594
if sym.isInlineMethod then
25852595
if StagingLevel.level > 0 then

docs/_docs/reference/other-new-features/opaques-details.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,45 @@ object obj:
109109
```
110110
The opaque type alias `A` is transparent in its scope, which includes the definition of `x`, but not the definitions of `obj` and `y`.
111111

112+
## Opaque Types in Transparent Inline Methods
113+
114+
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.
115+
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.
116+
117+
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:
118+
119+
```scala
120+
object Time:
121+
opaque type Time = String
122+
opaque type Seconds <: Time = String
123+
124+
// opaque type aliases have to be dealiased in nested calls,
125+
// otherwise the resulting program might not be typed correctly
126+
// in the below methods this will be typed as Seconds & String despite
127+
// the explicit type declaration
128+
transparent inline def sec(n: Double): Seconds =
129+
s"${n}s": Seconds
130+
131+
transparent inline def testInference(): List[Time] =
132+
List(sec(5)) // infers List[String] and returns List[Time] & List[String], not List[Seconds]
133+
transparent inline def testGuarded(): List[Time] =
134+
List(sec(5)): List[Seconds] // returns List[Seconds]
135+
transparent inline def testExplicitTime(): List[Time] =
136+
List[Seconds](sec(5)) // returns List[Seconds]
137+
transparent inline def testExplicitString(): List[Time] =
138+
List[String](sec(5)) // returns List[Time] & List[String]
139+
140+
end Time
141+
142+
@main def main() =
143+
val t1: List[String] = Time.testInference() // returns List[Time.Time] & List[String]
144+
val t2: List[Time.Seconds] = Time.testGuarded() // returns List[Time.Seconds]
145+
val t3: List[Time.Seconds] = Time.testExplicitTime() // returns List[Time.Seconds]
146+
val t4: List[String] = Time.testExplicitString() // returns List[Time.Time] & List[String]
147+
```
148+
149+
Be careful especially if what is being inlined depends on the type of those nested transparent calls.
150+
```
112151
113152
## Relationship to SIP 35
114153

tests/neg/i13461-a.scala

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package i13461:
2+
3+
opaque type Opaque = Int
4+
transparent inline def op: Opaque = (123: Opaque)
5+
6+
object Main:
7+
def main(args: Array[String]): Unit =
8+
val o22: 123 = op // error
9+
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import scala.quoted.*
2+
opaque type MyOpaque = Int
3+
object MyOpaque:
4+
val one: MyOpaque = 1
5+
transparent inline def apply(): MyOpaque = ${ applyMacro }
6+
private def applyMacro(using Quotes): Expr[MyOpaque] =
7+
import quotes.reflect.*
8+
'{ one }
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
trait Leak[T]:
2+
type Out
3+
given [T]: Leak[T] with
4+
type Out = T
5+
extension [T](t: T)(using l: Leak[T]) def leak: l.Out = ???
6+
7+
val x = MyOpaque().leak
8+
val shouldWork = summon[x.type <:< MyOpaque]

tests/pos/i13461-a.scala

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package i13461:
2+
3+
opaque type Opaque = Int
4+
transparent inline def op: Opaque = 123
5+
transparent inline def oop: i13461.Opaque = 123
6+
7+
object Main:
8+
def main(args: Array[String]): Unit =
9+
val o2: Opaque = op
10+
val o3: Opaque = oop // needs to be unwrapped from Typed generated in adapt
11+
val o22: 123 = op
12+
val o23: 123 = oop

tests/pos/i13461-c.scala

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
object Time:
2+
opaque type Time = String
3+
opaque type Seconds <: Time = String
4+
5+
transparent inline def sec(n: Double): Seconds =
6+
s"${n}s": Seconds // opaque type aliases have to be dealiased in nested calls, otherwise the resulting program might not be typed correctly
7+
8+
transparent inline def testInference(): List[Time] =
9+
List(sec(5)) // infers List[String] and returns List[Time] & List[String], not List[Seconds]
10+
transparent inline def testGuarded(): List[Time] =
11+
List(sec(5)): List[Seconds] // returns List[Seconds]
12+
transparent inline def testExplicitTime(): List[Time] =
13+
List[Seconds](sec(5)) // returns List[Seconds]
14+
transparent inline def testExplicitString(): List[Time] =
15+
List[String](sec(5)) // returns List[Time] & List[String]
16+
17+
end Time
18+
19+
@main def main() =
20+
val t1: List[String] = Time.testInference() // returns List[Time.Time] & List[String]
21+
val t2: List[Time.Seconds] = Time.testGuarded() // returns List[Time.Seconds]
22+
val t3: List[Time.Seconds] = Time.testExplicitTime() // returns List[Time.Seconds]
23+
val t4: List[String] = Time.testExplicitString() // returns List[Time.Time] & List[String]

tests/run/i13461-b.check

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
35s
2+
35m
3+
15s, 20m, 15m, 20s
4+
15s, 15m, 15s, 20m

0 commit comments

Comments
 (0)