Skip to content

Commit f47f2a4

Browse files
committed
Fix regressions: use typer skolems as inline proxy's type
Fixes #26153 Fixes #26031 Fixes #26624 **problem** When an inline expansion has a type that depends on one of the inline method arguments, and that argument is not stable, there's a type mismatch error on inline function call. For example, ```scala trait Mon[F[_]] { type Arg } class Wrap[F[_], A]: inline def apply[T](inline f: A => T): F[T] = ... inline def async[F[_]](using am: Mon[F]) = new Wrap[F, am.Arg] object Test: val r = async[[A] =>> (String, A)](_ => 42) // given TupleMon[E] extends Mon[[A] ==> (E, A)] ``` The result of `async` depends on the argument `am`: `Wrap[F, am.Arg]`. At the call site, the given argument for `am` is not a stable path. When `TypeAssigner` computes the dependent result type, it skolemizes that argument. So the inline call is typed as something like: `Wrap[[A] =>> (String, A), ?1.Arg]` where: `?1` is some value of type `Mon[[A] =>> (String, A)]`. On the other hand, `Inlining` creates a runtime proxy for the same arugment: `am$proxy` = <actual given>. When the proxy is typed as `am$proxy: Mon[[A] ==> (String, A)]`, then types in the expanded tree is like `am$proxy.Arg`. While the call tree was typed as `?1.Arg`. As a result, compile fails: ```scala val r = async[[A] =>> (String, A)](_ => 42) ^^^^^^^ Found: ?1.Arg => Int Required: am$proxy1.Arg => Int where: ?1 is an unknown value of type Mon[[A] =>> (String, A)] ``` The same root issue also causes regression #26031 ```scala trait Ctx[F[_]] trait Mon[F[_]]: type Context <: Ctx[F] object Mon: type Aux[F[_], C <: Ctx[F]] = Mon[F] { type Context = C } class Wrap[F[_], C <: Ctx[F]](using val am: Mon.Aux[F, C]) transparent inline def doIt[F[_]](using am: Mon[F]) = new Wrap(using am) ``` The call tree fixes `C = ?1.Context`, so the expansion needs an argument of type `Mon.Aux[F, ?1.Context]`. A proxy typed only as `Mon[F]` does not prove that its `Context` member is `?1.Context`. **Fix** The idea is Use the skolem created by typer as the type of the inline proxy. `val am$proxy: ?1 = <actual given>.asInstanceOf[?1]`. The runtime value is still the proxy, but its type-level prefix is now the same skolem generated by typer. This fixes the mismatch in #26153, and also fixes #26031 because the proxy term itself now has the skolem type. The idea is from: > I was wondering it would be great if we can use something like `Wrap[F, m.Arg] forSome { val m: Mon[F] }` instead of `?`, which is too coarse. And ... I realized that typer already uses skolem type `?1.Arg` when the result type is dependent. > #26033 (comment) --- This commit also dealias `NamedTypes` when homogenizing for `-Ytest-pickler`, otherwise, skolem before pickle vs unpickled has different type printing `Predef.String` vs `String` (tests/run/i14020) **note** Original PR #26033 was to special-case `selectionType` for an `Inlined` receiver. if the saved type of the `Inlined` tree was avoided to `?`, use the expansion's type instead. That can fix the immediate receiver mismatch, but it is workaround-ish. It recovers `am$proxy.Arg` after avoidance, which isn't right. while typer already had the better stable prefix `?1.Arg`. Another fix I tried was to rewrite only type positions: `am$proxy.Arg` => `?1.Arg` This helps #26153, where the mismatch is in the expanded type. But it does not fix #26031. A term with type `Mon[F]` still does not conform, it's required refinements.
1 parent 48ea019 commit f47f2a4

7 files changed

Lines changed: 163 additions & 9 deletions

File tree

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

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,36 @@ class Inliner(val call: tpd.Tree)(using Context):
229229
private val inlineCallPrefix =
230230
qualifier(methPart).orElse(This(inlinedMethod.enclosingClass.asClass))
231231

232+
/** Skolems created from typing this call tree.
233+
*
234+
* ```
235+
* trait Ctx[F[_]]
236+
* trait Mon[F[_]]:
237+
* type Context <: Ctx[F]
238+
* inline def async[F[_]](using am: Mon[F]): Wrap[F, am.Context] = ???
239+
* ```
240+
*
241+
* When TypeAssigner types `async(...)`, it skolemizes unstable `am` in the
242+
* dependent result type, and the call tree is typed as `Wrap[F, ?1.Context]`.
243+
* Then the inliner expands the method body and replace `am` by a proxy `am$proxy`.
244+
* Then `new Wrap[F, am.Context]` becomes `new Wrap[F, am$proxy.Context]`.
245+
*
246+
* In `paramBindingDef`, we type `am$proxy` as $1, so that both expanded tree and
247+
* call tree is typed `Wrap[F, ?1.Context]`. Otherwise, call tree and expanded tree
248+
* has different types `?1.Context` vs `am$proxy.Context` and compile fails.
249+
* See #26153 and #26031.
250+
*/
251+
private val callValueSkolemss: List[List[Option[SkolemType]]] =
252+
def loop(tree: Tree, skolemss: List[List[Option[SkolemType]]]): List[List[Option[SkolemType]]] = tree match
253+
case app @ Apply(fn, args) =>
254+
val mapping = app.getAttachment(TypeAssigner.SkolemizedArgs).getOrElse(Map.empty)
255+
loop(fn, args.map(mapping.get) :: skolemss)
256+
case TypeApply(fn, _) =>
257+
loop(fn, skolemss)
258+
case _ =>
259+
skolemss
260+
loop(call, Nil)
261+
232262
// Make sure all type arguments to the call are fully determined,
233263
// but continue if that's not achievable (or else i7459.scala would crash).
234264
for arg <- callTypeArgs do
@@ -279,9 +309,11 @@ class Inliner(val call: tpd.Tree)(using Context):
279309
* @param formal the type of the parameter
280310
* @param arg0 the argument corresponding to the parameter
281311
* @param buf the buffer to which the definition should be appended
312+
* @param skolem optional skolem from the call's dependent result type.
313+
* If present, use it as the proxy type.
282314
*/
283315
private[inlines] def paramBindingDef(name: Name, formal: Type, arg0: Tree,
284-
buf: DefBuffer)(using Context): ValOrDefDef = {
316+
buf: DefBuffer, skolem: Option[SkolemType] = None)(using Context): ValOrDefDef = {
285317
val isByName = formal.dealias.isInstanceOf[ExprType]
286318
val arg =
287319
def dropNameArg(arg: Tree): Tree = arg match
@@ -297,10 +329,17 @@ class Inliner(val call: tpd.Tree)(using Context):
297329
dropNameArg(arg0)
298330
val argtpe = arg.tpe.dealiasKeepAnnots.translateFromRepeated(toArray = false)
299331
val argIsBottom = argtpe.isBottomTypeAfterErasure
300-
val bindingType =
332+
val baseBindingType =
301333
if argIsBottom then formal
302334
else if isByName then ExprType(argtpe.widen)
303335
else argtpe.widen
336+
// If the call result type used a skolem for this argument, use the same skolem
337+
// as the proxy type. `?1` has `argtpe.widen` as its underlying type.
338+
val proxySkolem = if argIsBottom then None else skolem
339+
val bindingType = proxySkolem match
340+
case Some(sk) => if isByName then ExprType(sk) else sk
341+
case None => baseBindingType
342+
304343
var bindingFlags: FlagSet = InlineProxy
305344
if formal.widenExpr.hasAnnotation(defn.InlineParamAnnot) then
306345
bindingFlags |= Inline
@@ -313,6 +352,10 @@ class Inliner(val call: tpd.Tree)(using Context):
313352
var newArg = arg.changeOwner(ctx.owner, boundSym)
314353
if bindingFlags.is(Inline) && argIsBottom then
315354
newArg = Typed(newArg, TypeTree(formal.widenExpr)) // type ascribe RHS to avoid type errors in expansion. See i8612.scala
355+
else proxySkolem match
356+
case Some(sk) =>
357+
newArg = newArg.cast(sk) // adapt the rhs to the skolem-typed proxy
358+
case None => ()
316359
if isByName then DefDef(boundSym, newArg)
317360
else ValDef(boundSym, newArg, inferred = true)
318361
}.withSpan(boundSym.span)
@@ -328,31 +371,35 @@ class Inliner(val call: tpd.Tree)(using Context):
328371
private def computeParamBindings(
329372
tp: Type, targs: List[Tree],
330373
argss: List[List[Tree]], formalss: List[List[Type]],
374+
skolemss: List[List[Option[SkolemType]]],
331375
buf: DefBuffer): Boolean =
332376
tp match
333377
case tp: PolyType =>
334378
tp.paramNames.lazyZip(targs).foreach { (name, arg) =>
335379
paramSpan(name) = arg.span
336380
paramBinding(name) = arg.tpe.stripTypeVar
337381
}
338-
computeParamBindings(tp.resultType, targs.drop(tp.paramNames.length), argss, formalss, buf)
382+
computeParamBindings(tp.resultType, targs.drop(tp.paramNames.length), argss, formalss, skolemss, buf)
339383
case tp: MethodType =>
340384
if argss.isEmpty then
341385
report.error(em"missing arguments for inline method $inlinedMethod", call.srcPos)
342386
false
343387
else
344-
tp.paramNames.lazyZip(formalss.head).lazyZip(argss.head).foreach { (name, formal, arg) =>
388+
val skolems = skolemss.headOption.getOrElse(List.fill(argss.head.length)(None))
389+
tp.paramNames.lazyZip(formalss.head).lazyZip(argss.head).lazyZip(skolems).foreach {
390+
(name, formal, arg, skolem) =>
345391
paramSpan(name) = arg.span
346392
paramBinding(name) = arg.tpe.dealias match
347393
case _: SingletonType if isIdempotentPath(arg) =>
348394
arg.tpe
349395
case _ =>
350-
paramBindingDef(name, formal, arg, buf).symbol.termRef
396+
paramBindingDef(name, formal, arg, buf, skolem).symbol.termRef
351397
}
352-
computeParamBindings(tp.resultType, targs, argss.tail, formalss.tail, buf)
398+
computeParamBindings(tp.resultType, targs, argss.tail, formalss.tail, skolemss.tail, buf)
353399
case _ =>
354400
assert(targs.isEmpty)
355401
assert(argss.isEmpty)
402+
assert(skolemss.isEmpty)
356403
true
357404

358405
/** The number of enclosing classes of this class, plus one */
@@ -666,6 +713,7 @@ class Inliner(val call: tpd.Tree)(using Context):
666713
if !computeParamBindings(
667714
inlinedMethod.info, callTypeArgs,
668715
mappedCallValueArgss, paramTypess(call, Nil),
716+
callValueSkolemss,
669717
paramBindingsBuf)
670718
then
671719
return (Nil, EmptyTree)

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -598,7 +598,7 @@ object Inlines:
598598

599599
// Take care that only argument bindings go into `bindings`, since positions are
600600
// different for bindings from arguments and bindings from body.
601-
val inlined = tpd.Inlined(call, bindings, expansion)
601+
val inlined0 = tpd.Inlined(call, bindings, expansion)
602602

603603
val hasOpaquesInResultFromCallWithTransparentContext =
604604
val owners = call.symbol.ownersIterator.toSet
@@ -626,6 +626,24 @@ object Inlines:
626626
case _ => mapOver(t)
627627
).typeMap(tpe)
628628

629+
/** Argument proxies may be typed with skolems from the call tree (see
630+
* Inliner#callValueSkolemss). However, TASTy pickles skolems as their underlying
631+
* types, so the expansion's type may change after unpickling, which break the TASTy
632+
* roundtrip checked by `-Ytest-pickler`.
633+
*
634+
* To avoid this, insert the expansion with a no-op cast. This makes the pickled
635+
* underlying type to prevent `TypeOps.avoid` from generating a different
636+
* result type after unpickling.
637+
*/
638+
val inlined =
639+
val proxySkolems = bindings.map(_.symbol.info.widenExpr).collect { case sk: SkolemType => sk }
640+
if proxySkolems.nonEmpty && inlined0.tpe.existsPart(proxySkolems.contains) then
641+
val sealedExpansion =
642+
inContext(ctx.withSource(expansion.source)):
643+
expansion.cast(inlined0.tpe).withSpan(expansion.span)
644+
tpd.Inlined(call, bindings, sealedExpansion)
645+
else inlined0
646+
629647
if !hasOpaqueProxies && !hasOpaquesInResultFromCallWithTransparentContext then inlined
630648
else
631649
val (target, forceCast) =

compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -885,4 +885,3 @@ class PlainPrinter(_ctx: Context) extends Printer {
885885
protected def coloredText(text: Text, color: String): Text =
886886
if (ctx.useColors) color ~ text ~ SyntaxHighlighting.NoColor else text
887887
}
888-

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -616,8 +616,10 @@ object TypeAssigner extends TypeAssigner:
616616

617617
/** An attachment on an application indicating a map from arguments to the skolem types
618618
* that were created in safeSubstParams.
619+
* We use StickyKey so the Inlining phase can reuse the
620+
* same skolems for path-dependent types in the expansion (see #26153).
619621
*/
620-
private[typer] val SkolemizedArgs = new Property.Key[Map[tpd.Tree, SkolemType]]
622+
private[dotc] val SkolemizedArgs = new Property.StickyKey[Map[tpd.Tree, SkolemType]]
621623

622624
def seqLitType(tree: untpd.SeqLiteral, elemType: Type)(using Context) = tree match
623625
case tree: untpd.JavaSeqLiteral => defn.ArrayOf(elemType)

tests/pos/i26031.scala

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
trait Ctx[F[_]]
2+
3+
trait Mon[F[_]]:
4+
type Context <: Ctx[F]
5+
6+
object Mon:
7+
type Aux[F[_], C <: Ctx[F]] = Mon[F] { type Context = C }
8+
9+
class Wrap[F[_], C <: Ctx[F]](using val am: Mon.Aux[F, C]):
10+
transparent inline def apply[T](inline expr: C ?=> T): F[T] =
11+
inner[F, T, C](am, expr)
12+
13+
transparent inline def inner[F[_], T, C <: Ctx[F]](
14+
inline am: Mon.Aux[F, C],
15+
inline expr: C ?=> T
16+
): F[T] = ???
17+
18+
transparent inline def doIt[F[_]](using am: Mon[F]) =
19+
new Wrap(using am)
20+
21+
class Pair[A, B]
22+
class PairCtx[E] extends Ctx[[A] =>> Pair[E, A]]
23+
class PairMon[E] extends Mon[[A] =>> Pair[E, A]]:
24+
type Context = PairCtx[E]
25+
26+
given pm[E]: Mon[[A] =>> Pair[E, A]] = new PairMon[E]
27+
28+
object Test:
29+
val r = doIt[[A] =>> Pair[String, A]] { 42 }

tests/pos/i26153.scala

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
trait Ctx[F[_]]
2+
3+
trait Mon[F[_]]:
4+
type Context <: Ctx[F]
5+
6+
class Wrap[F[_], C <: Ctx[F]]:
7+
transparent inline def apply[T](inline expr: C ?=> T): F[T] =
8+
stage2[F, T, C](expr)
9+
10+
transparent inline def stage2[F[_], T, C <: Ctx[F]](
11+
inline expr: C ?=> T
12+
): F[T] = ???
13+
14+
transparent inline def async[F[_]](using am: Mon[F]) =
15+
new Wrap[F, am.Context]
16+
17+
class TupleMon[E] extends Mon[[A] =>> (E, A)]:
18+
type Context = Ctx[[A] =>> (E, A)]
19+
20+
given pm[E]: Mon[[A] =>> (E, A)] = new TupleMon[E]
21+
22+
object Test:
23+
val r = async[[A] =>> (String, A)] { 42 }
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
trait Ctx[F[_]]
2+
trait TryCtx[F[_]] extends Ctx[F]
3+
4+
trait Monad[F[_]]:
5+
type Context <: Ctx[F]
6+
def pure[T](t: T): F[T]
7+
def apply[T](op: Context => F[T]): F[T]
8+
9+
object Monad:
10+
type Aux[F[_], C <: Ctx[F]] = Monad[F] { type Context = C }
11+
12+
trait TryMonad[F[_]] extends Monad[F]:
13+
type Context <: TryCtx[F]
14+
15+
class TryBody[F[_]] extends TryCtx[F]
16+
17+
trait TryInstance[F[_]] extends TryMonad[F]:
18+
type Context = TryBody[F]
19+
def apply[T](op: Context => F[T]): F[T] = op(TryBody())
20+
21+
class InferAsyncArg[F[_], C <: Ctx[F]](using val am: Monad.Aux[F, C]):
22+
transparent inline def apply[T](inline expr: C ?=> T): F[T] =
23+
am.apply(ctx => am.pure(expr(using ctx)))
24+
25+
transparent inline def async[F[_]](using am: Monad[F]) =
26+
new InferAsyncArg(using am)
27+
28+
case class IO[A](value: A)
29+
30+
implicit def ioTryMonad: TryMonad[IO] = new TryMonad[IO] with TryInstance[IO]:
31+
def pure[T](t: T) = IO(t)
32+
33+
def program: IO[Int] = async[IO] {
34+
1 + 1
35+
}

0 commit comments

Comments
 (0)