Skip to content

Handle generic tuples when inlining #14209

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

Closed
wants to merge 3 commits into from
Closed
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
34 changes: 30 additions & 4 deletions compiler/src/dotty/tools/dotc/typer/Inliner.scala
Original file line number Diff line number Diff line change
Expand Up @@ -486,10 +486,18 @@ class Inliner(call: tpd.Tree, rhsToInline: tpd.Tree)(using Context) {
}
val argtpe = arg.tpe.dealiasKeepAnnots.translateFromRepeated(toArray = false)
val argIsBottom = argtpe.isBottomTypeAfterErasure
val bindingType =
val argwtpe =
if argIsBottom then formal
else if isByName then ExprType(argtpe.widen)
else if defn.isTupleNType(formal.widenExpr) && !defn.isTupleNType(argtpe.widen) then
// A generic tuple with know size N is considered as a subtype of TupleN but it does not
// contain the members of TupleN. All these tuples will be erased to TupleN and will have
// those members at runtime. Therefore when inlining it is important to keep the fact that
// this is TupleN and that the members can be accessed when the tree is re-typechecked.
// Also see `newArg` bellow
// See i14182
argtpe.widen & formal.widenExpr
else argtpe.widen
val bindingType = if isByName then ExprType(argwtpe) else argwtpe
var bindingFlags: FlagSet = InlineProxy
if formal.widenExpr.hasAnnotation(defn.InlineParamAnnot) then
bindingFlags |= Inline
Expand All @@ -500,8 +508,12 @@ class Inliner(call: tpd.Tree, rhsToInline: tpd.Tree)(using Context) {
val boundSym = newSym(InlineBinderName.fresh(name.asTermName), bindingFlags, bindingType).asTerm
val binding = {
var newArg = arg.changeOwner(ctx.owner, boundSym)
if bindingFlags.is(Inline) && argIsBottom then
if argIsBottom && bindingFlags.is(Inline) then
newArg = Typed(newArg, TypeTree(formal)) // type ascribe RHS to avoid type errors in expansion. See i8612.scala
else if defn.isTupleNType(formal.widenExpr) && !defn.isTupleNType(argtpe.widen) then
// Also see `argtpe1` above
newArg = Typed(newArg, TypeTree(formal.widenExpr))

if isByName then DefDef(boundSym, newArg)
else ValDef(boundSym, newArg)
}.withSpan(boundSym.span)
Expand Down Expand Up @@ -972,7 +984,21 @@ class Inliner(call: tpd.Tree, rhsToInline: tpd.Tree)(using Context) {
paramProxy.get(tree.tpe) match {
case Some(t) if tree.isTerm && t.isSingleton =>
val inlinedSingleton = singleton(t).withSpan(argSpan)
inlinedFromOutside(inlinedSingleton)(tree.span)
val tree1 = inlinedFromOutside(inlinedSingleton)(tree.span)
if defn.isTupleNType(tree.tpe.widenTermRefExpr) && !defn.isTupleNType(t.widenTermRefExpr) then
// See tests/pos/i14182b.scala
// FIXME: We have a `tree1` that has a term ref `tup.type` with underlying `1 *: 2 *: EmptyTuple` type but the original reference is to a `Tuple2[Int, Int]`
// If we take the `tree1` then it is impossible to select the `_1` field. If we take the parameter type we can lose singletons references that we should keep.
// Ideally we should ascribe it to a `tup.type & Tuple2[Int, Int]` but that is not working because we have the some special sub-typing rule that makes
// `1 *: 2 *: EmptyTuple <:< Tuple2[Int, Int]`. Therefore the glb of the two types ends up being `tup.type` which drops the extra information we need to add.
// None of the following attempts work.
Copy link
Contributor Author

@nicolasstucki nicolasstucki Jan 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An alternative would be to teach member resolution that if we have a generic tuple with a known size that erases to TupleN it has the members of that TupleN class. For example a Int *: EmptyTuple would have the member _1 because it has the members of TupleN.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This member selection issue is not contained in inlining. It was fairly straightforward to reproduce using type parameters in #14215.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@odersky said to look at
Line 3848 Typer: pt match
case pt: SelectionProto =>
insert type ascription and readapt



// tree1.ensureConforms(tree.tpe.widenTermRefExpr)
// Typed(tree1, TypeTree(tree.tpe.widenTermRefExpr & t))
Typed(tree1, TypeTree(AndType(tree.tpe.widenTermRefExpr, t)))
// tree1.asInstance(AndType(tree.tpe.widenTermRefExpr, t))
else tree1
case Some(t) if tree.isType =>
inlinedFromOutside(TypeTree(t).withSpan(argSpan))(tree.span)
case _ => tree
Expand Down
6 changes: 6 additions & 0 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3854,6 +3854,12 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
gadts.println(i"Member selection healed by GADT approximation")
tree.cast(gadtApprox)
else tree
else if tree.tpe.derivesFrom(defn.PairClass) && !defn.isTupleNType(tree.tpe.widenDealias) then
// If this is a generic tuple we need to cast it to make the TupleN/ members accessible.
// This only works for generic tuples of know size up to 22.
defn.tupleTypes(tree.tpe.widenTermRefExpr, Definitions.MaxTupleArity) match
case Some(elems) => tree.cast(defn.tupleType(elems))
case None => tree
else tree // other adaptations for selections are handled in typedSelect
case _ if ctx.mode.is(Mode.ImplicitsEnabled) && tree.tpe.isValueType =>
checkConversionsSpecific(pt, tree.srcPos)
Expand Down
38 changes: 38 additions & 0 deletions tests/neg/genericTupleMembers.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
def Test: Unit =
val tup1 = 1 *: EmptyTuple
val tup2 = 1 *: 2 *: EmptyTuple
val tup3 = 1 *: 2 *: 3 *: EmptyTuple
val tup4 = 1 *: 2 *: 3 *: 4 *: EmptyTuple
val tup5 = 1 *: 2 *: 3 *: 4 *: 5 *: EmptyTuple
val tup22 = 1 *: 2 *: 3 *: 4 *: 5 *: 6 *: 7 *: 8 *: 9 *: 10 *: 11 *: 12 *: 13 *: 14 *: 15 *: 16 *: 17 *: 18 *: 19 *: 20 *: 21 *: 22 *: EmptyTuple
val tup23 = 1 *: 2 *: 3 *: 4 *: 5 *: 6 *: 7 *: 8 *: 9 *: 10 *: 11 *: 12 *: 13 *: 14 *: 15 *: 16 *: 17 *: 18 *: 19 *: 20 *: 21 *: 22 *: 23 *: EmptyTuple

tup1._2 // error

tup2._3 // error

tup22._23 // error

tup23._1 // error
tup23._2 // error
tup23._3 // error
tup23._4 // error
tup23._5 // error
tup23._6 // error
tup23._7 // error
tup23._8 // error
tup23._9 // error
tup23._10 // error
tup23._11 // error
tup23._12 // error
tup23._13 // error
tup23._14 // error
tup23._15 // error
tup23._16 // error
tup23._17 // error
tup23._18 // error
tup23._19 // error
tup23._20 // error
tup23._21 // error
tup23._22 // error
tup23._23 // error
3 changes: 3 additions & 0 deletions tests/pos-macros/i14182/Macro_1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import scala.quoted._
def fooImpl(xs: Expr[(Int, Int)])(using Quotes): Expr[Unit] =
'{ val a: Int = $xs._1; }
3 changes: 3 additions & 0 deletions tests/pos-macros/i14182/Test_2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
inline def foo(inline xs: (Int, Int)): Unit = ${ fooImpl('xs) }
def fail = foo(1 *: 2 *: EmptyTuple)
def ok = foo((1, 2))
3 changes: 3 additions & 0 deletions tests/pos/i14182.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
inline def foo(inline xs: (Int, Int)): Unit = { val a: Int = xs._1; }
def fail = foo(1 *: 2 *: EmptyTuple)
def ok = foo((1, 2))
3 changes: 3 additions & 0 deletions tests/pos/i14182a.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
inline def foo(xs: (Int, Int)): Unit = { val a: Int = xs._1; }
def fail = foo(1 *: 2 *: EmptyTuple)
def ok = foo((1, 2))
4 changes: 4 additions & 0 deletions tests/pos/i14182b.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
inline def foo(inline xs: (Int, Int)): xs.type = { val a: Int = xs._1; xs }
def bar =
val tup: 1 *: 2 *: EmptyTuple = ???
val tup2: tup.type = foo(tup)
3 changes: 3 additions & 0 deletions tests/pos/i14182c.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
inline def foo(xs: => (Int, Int)): Unit = { val a: Int = xs._1; }
def bar =
foo(1 *: 2 *: EmptyTuple)
46 changes: 46 additions & 0 deletions tests/run/genericTupleMembers.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@

@main def Test: Unit =
val tup1 = 1 *: EmptyTuple
val tup2 = 1 *: 2 *: EmptyTuple
val tup3 = 1 *: 2 *: 3 *: EmptyTuple
val tup4 = 1 *: 2 *: 3 *: 4 *: EmptyTuple
val tup5 = 1 *: 2 *: 3 *: 4 *: 5 *: EmptyTuple
val tup22 = 1 *: 2 *: 3 *: 4 *: 5 *: 6 *: 7 *: 8 *: 9 *: 10 *: 11 *: 12 *: 13 *: 14 *: 15 *: 16 *: 17 *: 18 *: 19 *: 20 *: 21 *: 22 *: EmptyTuple

tup1._1

tup2._1
tup2._2
tup2.swap

tup3._1
tup3._2
tup3._3

tup4._1
tup4._2
tup4._3
tup4._4

tup22._1
tup22._2
tup22._3
tup22._4
tup22._5
tup22._6
tup22._7
tup22._8
tup22._9
tup22._10
tup22._11
tup22._12
tup22._13
tup22._14
tup22._15
tup22._16
tup22._17
tup22._18
tup22._19
tup22._20
tup22._21
tup22._22
12 changes: 12 additions & 0 deletions tests/run/i14215.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
def f[T <: Tuple2[Int, Int]](tup: T): T = tup

@main def Test: Unit =
(1, 2)._1
f((1, 2))._1

(1 *: 2 *: EmptyTuple)._1
f(1 *: 2 *: EmptyTuple)._1
f[Int *: Int *: EmptyTuple](1 *: 2 *: EmptyTuple)._1

f[Int *: Int *: EmptyTuple]((1, 2))._1
f[Tuple2[Int, Int]](1 *: 2 *: EmptyTuple)._1