Skip to content

Prevent opaque types leaking from transparent inline methods #23792

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions compiler/src/dotty/tools/dotc/inlines/Inliner.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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,
Expand Down
10 changes: 8 additions & 2 deletions compiler/src/dotty/tools/dotc/inlines/Inlines.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 12 additions & 2 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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?
*/
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
39 changes: 39 additions & 0 deletions docs/_docs/reference/other-new-features/opaques-details.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
9 changes: 9 additions & 0 deletions tests/neg/i13461.scala
Original file line number Diff line number Diff line change
@@ -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

23 changes: 23 additions & 0 deletions tests/pos/i13461-c.scala
Original file line number Diff line number Diff line change
@@ -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]
12 changes: 12 additions & 0 deletions tests/pos/i13461.scala
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions tests/run/i13461-b.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
35s
35m
15s, 20m, 15m, 20s
15s, 15m, 15s, 20m
60 changes: 60 additions & 0 deletions tests/run/i13461-b.scala
Original file line number Diff line number Diff line change
@@ -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)
Loading