Skip to content

Commit 0a95a3d

Browse files
committed
Fix parameter untupling
Make untupled parameters ValDefs instead of DefDefs so that their references are stable paths. Add an additional check that untupled parameters don't come from call-by-name parameters (in that case, ValDefs would be unsound). I was trying hard to allow paramater untupling for cbn parameters, but this seems to be very hard since parameter untupling happens as a desugaring step when the expected parameter type is not yet known. And it seems really hard to change from stable to unstable (or vice versa) later in the compilation pipleline without doing a lot of re-checking. Since untupled cbn parameters are a pretty extreme corner case anyway I think it's OK to just disallow them.
1 parent 20cc92e commit 0a95a3d

File tree

6 files changed

+80
-3
lines changed

6 files changed

+80
-3
lines changed

compiler/src/dotty/tools/dotc/ast/Desugar.scala

+7-1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ object desugar {
3939
*/
4040
val MultiLineInfix: Property.Key[Unit] = Property.StickyKey()
4141

42+
/** An attachment key to indicate that a ValDef originated from parameter untupling.
43+
*/
44+
val UntupledParam: Property.Key[Unit] = Property.StickyKey()
45+
4246
/** What static check should be applied to a Match? */
4347
enum MatchCheck {
4448
case None, Exhaustive, IrrefutablePatDef, IrrefutableGenFrom
@@ -1426,7 +1430,9 @@ object desugar {
14261430
val vdefs =
14271431
params.zipWithIndex.map {
14281432
case (param, idx) =>
1429-
DefDef(param.name, Nil, param.tpt, selector(idx)).withSpan(param.span)
1433+
ValDef(param.name, param.tpt, selector(idx))
1434+
.withSpan(param.span)
1435+
.withAttachment(UntupledParam, ())
14301436
}
14311437
Function(param :: Nil, Block(vdefs, body))
14321438
}

compiler/src/dotty/tools/dotc/transform/PostTyper.scala

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package dotty.tools.dotc
22
package transform
33

4-
import dotty.tools.dotc.ast.{Trees, tpd, untpd}
4+
import dotty.tools.dotc.ast.{Trees, tpd, untpd, desugar}
55
import scala.collection.mutable
66
import core._
77
import dotty.tools.dotc.typer.Checking
@@ -255,6 +255,14 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase
255255
if tree.symbol.is(ConstructorProxy) then
256256
report.error(em"constructor proxy ${tree.symbol} cannot be used as a value", tree.srcPos)
257257

258+
def checkStableSelection(tree: Tree)(using Context): Unit =
259+
def check(qual: Tree) =
260+
if !qual.tpe.isStable then
261+
report.error(em"Parameter untupling cannot be used for call-by-name parameters", tree.srcPos)
262+
tree match
263+
case Select(qual, _) => check(qual) // simple select _n
264+
case Apply(TypeApply(Select(qual, _), _), _) => check(qual) // generic select .apply[T](n)
265+
258266
override def transform(tree: Tree)(using Context): Tree =
259267
try tree match {
260268
// TODO move CaseDef case lower: keep most probable trees first for performance
@@ -356,6 +364,8 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase
356364
case tree: ValDef =>
357365
checkErasedDef(tree)
358366
val tree1 = cpy.ValDef(tree)(rhs = normalizeErasedRhs(tree.rhs, tree.symbol))
367+
if tree1.removeAttachment(desugar.UntupledParam).isDefined then
368+
checkStableSelection(tree.rhs)
359369
processValOrDefDef(super.transform(tree1))
360370
case tree: DefDef =>
361371
checkErasedDef(tree)

tests/neg/function-arity-2.scala

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
object Test {
2+
3+
class T[A] { def foo(f: (=> A) => Int) = f(???) }
4+
5+
def main(args: Array[String]): Unit =
6+
new T[(Int, Int)].foo((x, y) => 0) // error // error Parameter untupling cannot be used for call-by-name parameters (twice)
7+
}

tests/neg/i14783.scala

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
object Test:
2+
def foo(f: (=> (Int, Int)) => Int) = ???
3+
foo((a, b) => a + b) // error // error

tests/pos/i14783.scala

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
object Wart:
2+
def bar(using c: Ctx)(ws: List[Wrap[c.type]]): Unit =
3+
ws.zipWithIndex.foreach { (w, _) => w.x.foo }
4+
5+
trait Wrap[C <: Ctx & Singleton]:
6+
val ctx: C
7+
def x: ctx.inner.X
8+
9+
trait Ctx:
10+
object inner:
11+
type X
12+
extension (self: X) def foo: Int = ???
13+
14+
15+
object WartInspector:
16+
def myWartTraverser: WartTraverser = ???
17+
def inspect(using q: Quotes)(tastys: List[Tasty[q.type]]): Unit = {
18+
val universe: WartUniverse.Aux[q.type] = WartUniverse(q)
19+
val traverser = myWartTraverser.get(universe)
20+
tastys.zipWithIndex.foreach { (tasty, index) =>
21+
val tree = tasty.ast
22+
traverser.traverseTree(tree)(tree.symbol)
23+
}
24+
}
25+
26+
object WartUniverse:
27+
type Aux[X <: Quotes] = WartUniverse { type Q = X }
28+
def apply[Q <: Quotes](quotes: Q): Aux[Q] = ???
29+
30+
31+
abstract class WartUniverse:
32+
type Q <: Quotes
33+
val quotes: Q
34+
abstract class Traverser extends quotes.reflect.TreeTraverser
35+
36+
37+
abstract class WartTraverser:
38+
def get(u: WartUniverse): u.Traverser
39+
40+
trait Tasty[Q <: Quotes & Singleton]:
41+
val quotes: Q
42+
def path: String
43+
def ast: quotes.reflect.Tree
44+
45+
trait Quotes:
46+
object reflect:
47+
type Tree
48+
extension (self: Tree) def symbol: Symbol = ???
49+
type Symbol
50+
trait TreeTraverser:
51+
def traverseTree(tree: Tree)(symbol: Symbol): Unit

tests/run/function-arity.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@ object Test {
33

44
def main(args: Array[String]): Unit = {
55
new T[(Int, Int)].foo((ii) => 0)
6-
new T[(Int, Int)].foo((x, y) => 0) // check that this does not run into ???
6+
//new T[(Int, Int)].foo((x, y) => 0) // not allowed anymore
77
}
88
}

0 commit comments

Comments
 (0)