From 584c05bd80be0bb048365470be9fe73e15b49796 Mon Sep 17 00:00:00 2001 From: Nicolas Stucki Date: Mon, 10 Jan 2022 17:25:04 +0100 Subject: [PATCH 1/3] Adapt generic tuples to be able to access members Fixes #14215 --- .../src/dotty/tools/dotc/typer/Typer.scala | 6 +++ tests/neg/genericTupleMembers.scala | 38 +++++++++++++++ tests/run/genericTupleMembers.scala | 46 +++++++++++++++++++ tests/run/i14215.scala | 12 +++++ 4 files changed, 102 insertions(+) create mode 100644 tests/neg/genericTupleMembers.scala create mode 100644 tests/run/genericTupleMembers.scala create mode 100644 tests/run/i14215.scala diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 957a51532886..5f1a45fbd2d8 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -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) diff --git a/tests/neg/genericTupleMembers.scala b/tests/neg/genericTupleMembers.scala new file mode 100644 index 000000000000..13c70fa73b6f --- /dev/null +++ b/tests/neg/genericTupleMembers.scala @@ -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 diff --git a/tests/run/genericTupleMembers.scala b/tests/run/genericTupleMembers.scala new file mode 100644 index 000000000000..1b6de1486a05 --- /dev/null +++ b/tests/run/genericTupleMembers.scala @@ -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 diff --git a/tests/run/i14215.scala b/tests/run/i14215.scala new file mode 100644 index 000000000000..d732337ce0f2 --- /dev/null +++ b/tests/run/i14215.scala @@ -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 From 28187d96a573860b08db8955f6746ec4e34e75ba Mon Sep 17 00:00:00 2001 From: Nicolas Stucki Date: Thu, 13 Jan 2022 09:22:45 +0100 Subject: [PATCH 2/3] Add tests for #14182 --- tests/pos-macros/i14182/Macro_1.scala | 3 +++ tests/pos-macros/i14182/Test_2.scala | 3 +++ tests/pos/i14182.scala | 3 +++ tests/pos/i14182a.scala | 3 +++ tests/pos/i14182b.scala | 4 ++++ tests/pos/i14182c.scala | 3 +++ 6 files changed, 19 insertions(+) create mode 100644 tests/pos-macros/i14182/Macro_1.scala create mode 100644 tests/pos-macros/i14182/Test_2.scala create mode 100644 tests/pos/i14182.scala create mode 100644 tests/pos/i14182a.scala create mode 100644 tests/pos/i14182b.scala create mode 100644 tests/pos/i14182c.scala diff --git a/tests/pos-macros/i14182/Macro_1.scala b/tests/pos-macros/i14182/Macro_1.scala new file mode 100644 index 000000000000..ba4935b790b5 --- /dev/null +++ b/tests/pos-macros/i14182/Macro_1.scala @@ -0,0 +1,3 @@ +import scala.quoted._ +def fooImpl(xs: Expr[(Int, Int)])(using Quotes): Expr[Unit] = + '{ val a: Int = $xs._1; } diff --git a/tests/pos-macros/i14182/Test_2.scala b/tests/pos-macros/i14182/Test_2.scala new file mode 100644 index 000000000000..6ff328da4e06 --- /dev/null +++ b/tests/pos-macros/i14182/Test_2.scala @@ -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)) diff --git a/tests/pos/i14182.scala b/tests/pos/i14182.scala new file mode 100644 index 000000000000..e434ccef0603 --- /dev/null +++ b/tests/pos/i14182.scala @@ -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)) diff --git a/tests/pos/i14182a.scala b/tests/pos/i14182a.scala new file mode 100644 index 000000000000..fde41dd7a1ab --- /dev/null +++ b/tests/pos/i14182a.scala @@ -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)) diff --git a/tests/pos/i14182b.scala b/tests/pos/i14182b.scala new file mode 100644 index 000000000000..b1f2d002bfe7 --- /dev/null +++ b/tests/pos/i14182b.scala @@ -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) diff --git a/tests/pos/i14182c.scala b/tests/pos/i14182c.scala new file mode 100644 index 000000000000..5497ab16d055 --- /dev/null +++ b/tests/pos/i14182c.scala @@ -0,0 +1,3 @@ +inline def foo(xs: => (Int, Int)): Unit = { val a: Int = xs._1; } +def bar = + foo(1 *: 2 *: EmptyTuple) From da814bf85987f1724f967eb0cfb0a83a2f22aa34 Mon Sep 17 00:00:00 2001 From: Nicolas Stucki Date: Thu, 13 Jan 2022 09:23:36 +0100 Subject: [PATCH 3/3] Handle generic tuples when inlining Fixes #14182 --- .../src/dotty/tools/dotc/typer/Inliner.scala | 34 ++++++++++++++++--- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Inliner.scala b/compiler/src/dotty/tools/dotc/typer/Inliner.scala index 722e3abfec85..ae4a839a2c79 100644 --- a/compiler/src/dotty/tools/dotc/typer/Inliner.scala +++ b/compiler/src/dotty/tools/dotc/typer/Inliner.scala @@ -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 @@ -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) @@ -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. + + + // 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