From e4682ce50173a1a42c83095fcb5d2e592b610fec Mon Sep 17 00:00:00 2001 From: Dimi Racordon Date: Tue, 30 Jan 2024 11:01:05 +0000 Subject: [PATCH 01/24] Add a test for multiple assignments --- tests/pos/multiple-assignment.scala | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 tests/pos/multiple-assignment.scala diff --git a/tests/pos/multiple-assignment.scala b/tests/pos/multiple-assignment.scala new file mode 100644 index 000000000000..75c7dd04c92b --- /dev/null +++ b/tests/pos/multiple-assignment.scala @@ -0,0 +1,4 @@ +def ma(): Unit = + var x = 0 + var y = false + (x, y) = (1, true) From af187a57bda6ae10cda43e3b5aabfa94c25e39b5 Mon Sep 17 00:00:00 2001 From: Dimi Racordon Date: Tue, 30 Jan 2024 11:01:38 +0000 Subject: [PATCH 02/24] Allow tuple expressions on the LHS of assignments --- compiler/src/dotty/tools/dotc/parsing/Parsers.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index acb7b869b269..914c549b32cb 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -2479,7 +2479,7 @@ object Parsers { def expr1Rest(t: Tree, location: Location): Tree = if in.token == EQUALS then t match - case Ident(_) | Select(_, _) | Apply(_, _) | PrefixOp(_, _) => + case Ident(_) | Select(_, _) | Apply(_, _) | PrefixOp(_, _) | Tuple(_) => atSpan(startOffset(t), in.skipToken()) { val loc = if location.inArgs then location else Location.ElseWhere Assign(t, subPart(() => expr(loc))) From 99bcde340e593ba299ed86c3c5716148ae02d217 Mon Sep 17 00:00:00 2001 From: Dimi Racordon Date: Fri, 2 Feb 2024 16:53:23 +0100 Subject: [PATCH 03/24] Strengthen tests for multiple assignments --- tests/neg/multiple-assignment.check | 11 +++++++++++ tests/neg/multiple-assignment.scala | 10 ++++++++++ tests/pos/multiple-assignment.scala | 9 +++++++-- 3 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 tests/neg/multiple-assignment.check create mode 100644 tests/neg/multiple-assignment.scala diff --git a/tests/neg/multiple-assignment.check b/tests/neg/multiple-assignment.check new file mode 100644 index 000000000000..1c43f2eca809 --- /dev/null +++ b/tests/neg/multiple-assignment.check @@ -0,0 +1,11 @@ +-- [E193] Type Error: tests/neg/multiple-assignment.scala:9:16 ----------------- +9 | ((x, y), z) = 1 + | ^ + | invalid source of multiple assignment. + | The right hand side must be a tuple but (1 : Int) was found. +-- [E194] Type Error: tests/neg/multiple-assignment.scala:10:11 ---------------- +10 | (x, y) = (1, 1, 1) + | ^^^^^^^^^ + | Source and target of multiple assignment have different sizes. + | Source: 3 + | Target: 2 diff --git a/tests/neg/multiple-assignment.scala b/tests/neg/multiple-assignment.scala new file mode 100644 index 000000000000..17eb2d7945d1 --- /dev/null +++ b/tests/neg/multiple-assignment.scala @@ -0,0 +1,10 @@ + +def tu(): (Int, Boolean) = (1, true) + +@main def ma(): Unit = + var x = 0 + var y = false + var z = "a" + + ((x, y), z) = 1 + (x, y) = (1, 1, 1) diff --git a/tests/pos/multiple-assignment.scala b/tests/pos/multiple-assignment.scala index 75c7dd04c92b..a073deccb77f 100644 --- a/tests/pos/multiple-assignment.scala +++ b/tests/pos/multiple-assignment.scala @@ -1,4 +1,9 @@ -def ma(): Unit = +def tu(): (Int, Boolean) = (1, true) + +@main def ma(): Unit = var x = 0 var y = false - (x, y) = (1, true) + var z = "a" + + ((x, y), z) = (tu(), "b") + From d0e46b55a23f14d876708ad66168d94a3a013c62 Mon Sep 17 00:00:00 2001 From: Dimi Racordon Date: Fri, 2 Feb 2024 16:51:53 +0100 Subject: [PATCH 04/24] Add error messages for type errors in multiple assignment --- .../tools/dotc/reporting/ErrorMessageID.scala | 2 ++ .../dotty/tools/dotc/reporting/messages.scala | 17 +++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala b/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala index e3613e3f783a..0af64da23fc0 100644 --- a/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala +++ b/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala @@ -214,6 +214,8 @@ enum ErrorMessageID(val isActive: Boolean = true) extends java.lang.Enum[ErrorMe case UnusedSymbolID // errorNumber: 198 case TailrecNestedCallID //errorNumber: 199 case FinalLocalDefID // errorNumber: 200 + case InvalidMultipleAssignmentSourceID // errorNumber: 201 + case MultipleAssignmentShapeMismatchID // errorNumber: 202 def errorNumber = ordinal - 1 diff --git a/compiler/src/dotty/tools/dotc/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala index 1d906130d4e4..47f6b63467f3 100644 --- a/compiler/src/dotty/tools/dotc/reporting/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala @@ -3288,3 +3288,20 @@ object UnusedSymbol { def privateMembers(using Context): UnusedSymbol = new UnusedSymbol(i"unused private member") def patVars(using Context): UnusedSymbol = new UnusedSymbol(i"unused pattern variable") } + +class InvalidMultipleAssignmentSource(found: Type)(using Context) + extends TypeMsg(InvalidMultipleAssignmentSourceID) { + def msg(using Context) = + i"""invalid source of multiple assignment. + |The right hand side must be a tuple but $found was found.""" + def explain(using Context) = "" +} + +class MultipleAssignmentShapeMismatch(found: Int, required: Int)(using Context) + extends TypeMsg(MultipleAssignmentShapeMismatchID) { + def msg(using Context) = + i"""Source and target of multiple assignment have different sizes. + |Source: $found + |Target: $required""" + def explain(using Context) = "" +} From 716a3238b205bd56722b0fd37f64fc2172c90be0 Mon Sep 17 00:00:00 2001 From: Dimi Racordon Date: Fri, 2 Feb 2024 16:52:25 +0100 Subject: [PATCH 05/24] Handle multiple assignments in the typer --- .../src/dotty/tools/dotc/typer/Typer.scala | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index f2a7124f9fa4..b0e65360e378 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1301,6 +1301,10 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer tree.lhs match { case lhs @ Apply(fn, args) => typed(untpd.Apply(untpd.Select(fn, nme.update), args :+ tree.rhs), pt) + case untpd.Tuple(lhs) => + val locked = ctx.typerState.ownedVars + val rhs = typed(tree.rhs, WildcardType) + typedPairwiseAssignments(lhs, rhs) case untpd.TypedSplice(Apply(MaybePoly(Select(fn, app), targs), args)) if app == nme.apply => val rawUpdate: untpd.Tree = untpd.Select(untpd.TypedSplice(fn), nme.update) val wrappedUpdate = @@ -1407,6 +1411,74 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer } } + /** Returns a block assigning to `targets` the corresponding values in `rhs`. + * + * @param targets A sequence of untyped trees on the LHS of a multiple assignment. + * @param rhs The RHS of a multuple assignment. + */ + private def typedPairwiseAssignments( + targets: List[untpd.Tree], rhs: Tree + )(using Context): Tree = + val s = mutable.ListBuffer[untpd.Tree]() + val u = formPairwiseAssignments( + s, targets, untpd.TypedSplice(rhs), rhs.tpe, + EmptyTermName) + u.map((b) => typed(b)).getOrElse(unitLiteral) + + /** Appends to `statements` the assignments of `targets` to corresponding values in `rhs`. + * + * @param statements A partially constructed sequence representing the desugaring of a + * multiple assignment. + * @param targets A sequence of expression representing variables to assign. + * @param rhs The expression of the values to assign.. + * @param rhsType The type of `rhs`. + * @param prefix A prefix for the name of syntactic vals created by the methods. + */ + private def formPairwiseAssignments( + statements: mutable.ListBuffer[untpd.Tree], + targets: List[untpd.Tree], + rhs: untpd.Tree, + rhsType: Type, + prefix: TermName + )(using Context): Option[untpd.Block] = + val source = UniqueName.fresh(prefix) + val d = untpd.ValDef(source, untpd.TypeTree(rhsType), rhs) + .withSpan(rhs.span) + .withFlags(Synthetic) + statements.append(d) + + rhsType.tupleElementTypes match { + case None => + report.error(InvalidMultipleAssignmentSource(rhsType), rhs.srcPos) + None + + case Some(e) if targets.length != e.length => + report.error(MultipleAssignmentShapeMismatch(e.length, targets.length), rhs.srcPos) + None + + case Some(sourceTypes) => + /** The value to assign to the `i`-th source. */ + def rhsElement(i: Int): untpd.Tree = + untpd.Select(untpd.Ident(source), nme.productAccessorName(i)) + .withSpan(rhs.span) + + /** Forms the assignments of the targets in the range [`i`, `targets.length`). */ + @tailrec def loop(i: Int): Option[untpd.Block] = + if (i == targets.length) then + Some(untpd.Block(statements.toList, untpd.TypedSplice(unitLiteral))) + else targets(i) match { + case untpd.Tuple(lhs) => + formPairwiseAssignments(statements, lhs, rhsElement(i + 1), sourceTypes(i), source) + loop(i + 1) + case lhs => + val u = untpd.Assign(lhs, rhsElement(i + 1)) + statements.append(u) + loop(i + 1) + } + + loop(0) + } + def typedBlockStats(stats: List[untpd.Tree])(using Context): (List[tpd.Tree], Context) = index(stats) typedStats(stats, ctx.owner) From 16646201a52bcab94b3d3f3d21edb7e12c6d90d7 Mon Sep 17 00:00:00 2001 From: Dimi Racordon Date: Fri, 2 Feb 2024 17:02:40 +0100 Subject: [PATCH 06/24] Add the Fibonacci sequence example from SIP-59 to the tests --- tests/pos/fibonacci.scala | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 tests/pos/fibonacci.scala diff --git a/tests/pos/fibonacci.scala b/tests/pos/fibonacci.scala new file mode 100644 index 000000000000..954ea6e375b5 --- /dev/null +++ b/tests/pos/fibonacci.scala @@ -0,0 +1,15 @@ +class FibonacciIterator() extends Iterator[Int]: + + private var a: Int = 0 + private var b: Int = 1 + + def hasNext = true + def next() = + val r = a + (a, b) = (b, a + b) + r + +@main def fib() = + val i = FibonacciIterator() + for _ <- 0 until 10 do + println(i.next()) From ed92d95653dd91a8ee53f3f8fb26ddf8f38f8b48 Mon Sep 17 00:00:00 2001 From: Dimi Racordon Date: Mon, 12 Feb 2024 13:04:49 +0100 Subject: [PATCH 07/24] Use errorTree rather than pruning ill-typed multiple assignments --- compiler/src/dotty/tools/dotc/typer/Typer.scala | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index b0e65360e378..12da27dde1d1 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1423,7 +1423,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val u = formPairwiseAssignments( s, targets, untpd.TypedSplice(rhs), rhs.tpe, EmptyTermName) - u.map((b) => typed(b)).getOrElse(unitLiteral) + typed(u) /** Appends to `statements` the assignments of `targets` to corresponding values in `rhs`. * @@ -1440,7 +1440,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer rhs: untpd.Tree, rhsType: Type, prefix: TermName - )(using Context): Option[untpd.Block] = + )(using Context): untpd.Tree = val source = UniqueName.fresh(prefix) val d = untpd.ValDef(source, untpd.TypeTree(rhsType), rhs) .withSpan(rhs.span) @@ -1449,12 +1449,10 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer rhsType.tupleElementTypes match { case None => - report.error(InvalidMultipleAssignmentSource(rhsType), rhs.srcPos) - None + errorTree(rhs, InvalidMultipleAssignmentSource(rhsType)) case Some(e) if targets.length != e.length => - report.error(MultipleAssignmentShapeMismatch(e.length, targets.length), rhs.srcPos) - None + errorTree(rhs, MultipleAssignmentShapeMismatch(e.length, targets.length)) case Some(sourceTypes) => /** The value to assign to the `i`-th source. */ @@ -1463,9 +1461,9 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer .withSpan(rhs.span) /** Forms the assignments of the targets in the range [`i`, `targets.length`). */ - @tailrec def loop(i: Int): Option[untpd.Block] = + @tailrec def loop(i: Int): untpd.Block = if (i == targets.length) then - Some(untpd.Block(statements.toList, untpd.TypedSplice(unitLiteral))) + untpd.Block(statements.toList, untpd.TypedSplice(unitLiteral)) else targets(i) match { case untpd.Tuple(lhs) => formPairwiseAssignments(statements, lhs, rhsElement(i + 1), sourceTypes(i), source) From 6b4b3f09d1b7c1dbb10a1cc9cd8434a55da24f37 Mon Sep 17 00:00:00 2001 From: Dimi Racordon Date: Mon, 12 Feb 2024 13:34:07 +0100 Subject: [PATCH 08/24] Form a single block with all assignments --- compiler/src/dotty/tools/dotc/typer/Typer.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 12da27dde1d1..f5c5ff682198 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1420,10 +1420,10 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer targets: List[untpd.Tree], rhs: Tree )(using Context): Tree = val s = mutable.ListBuffer[untpd.Tree]() - val u = formPairwiseAssignments( + formPairwiseAssignments( s, targets, untpd.TypedSplice(rhs), rhs.tpe, EmptyTermName) - typed(u) + typed(untpd.Block(s.toList, untpd.TypedSplice(unitLiteral))) /** Appends to `statements` the assignments of `targets` to corresponding values in `rhs`. * From a9d11fd72232cd4a43a8774f3ce4006a774c6d52 Mon Sep 17 00:00:00 2001 From: Dimi Racordon Date: Mon, 12 Feb 2024 13:34:37 +0100 Subject: [PATCH 09/24] Replace tailrec def with a for loop --- compiler/src/dotty/tools/dotc/typer/Typer.scala | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index f5c5ff682198..c76dac1d0c3c 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1440,7 +1440,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer rhs: untpd.Tree, rhsType: Type, prefix: TermName - )(using Context): untpd.Tree = + )(using Context): Unit = val source = UniqueName.fresh(prefix) val d = untpd.ValDef(source, untpd.TypeTree(rhsType), rhs) .withSpan(rhs.span) @@ -1461,20 +1461,15 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer .withSpan(rhs.span) /** Forms the assignments of the targets in the range [`i`, `targets.length`). */ - @tailrec def loop(i: Int): untpd.Block = - if (i == targets.length) then - untpd.Block(statements.toList, untpd.TypedSplice(unitLiteral)) - else targets(i) match { + for i <- 0 until targets.length do { + targets(i) match { case untpd.Tuple(lhs) => formPairwiseAssignments(statements, lhs, rhsElement(i + 1), sourceTypes(i), source) - loop(i + 1) case lhs => val u = untpd.Assign(lhs, rhsElement(i + 1)) statements.append(u) - loop(i + 1) } - - loop(0) + } } def typedBlockStats(stats: List[untpd.Tree])(using Context): (List[tpd.Tree], Context) = From f414f67852350c6d321bc823352dc1d8d2054b80 Mon Sep 17 00:00:00 2001 From: Dimi Racordon Date: Wed, 5 Jun 2024 14:55:50 +0200 Subject: [PATCH 10/24] Refactor 'typedAssign' to ensure the right evaluation order --- .../tools/dotc/reporting/ErrorMessageID.scala | 3 +- .../dotty/tools/dotc/reporting/messages.scala | 8 + .../src/dotty/tools/dotc/typer/Dynamic.scala | 27 ++ .../tools/dotc/typer/PartialAssignment.scala | 123 ++++++ .../src/dotty/tools/dotc/typer/Typer.scala | 372 ++++++++++-------- tests/neg/multiple-assignment.check | 32 +- tests/neg/multiple-assignment.scala | 6 +- tests/pos/multiple-assignment.scala | 5 +- 8 files changed, 395 insertions(+), 181 deletions(-) create mode 100644 compiler/src/dotty/tools/dotc/typer/PartialAssignment.scala diff --git a/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala b/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala index 0af64da23fc0..0ed627c983e4 100644 --- a/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala +++ b/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala @@ -215,7 +215,8 @@ enum ErrorMessageID(val isActive: Boolean = true) extends java.lang.Enum[ErrorMe case TailrecNestedCallID //errorNumber: 199 case FinalLocalDefID // errorNumber: 200 case InvalidMultipleAssignmentSourceID // errorNumber: 201 - case MultipleAssignmentShapeMismatchID // errorNumber: 202 + case InvalidMultipleAssignmentTargetID // errorNumber: 202 + case MultipleAssignmentShapeMismatchID // errorNumber: 203 def errorNumber = ordinal - 1 diff --git a/compiler/src/dotty/tools/dotc/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala index 47f6b63467f3..fdb694ea4da3 100644 --- a/compiler/src/dotty/tools/dotc/reporting/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala @@ -3297,6 +3297,14 @@ class InvalidMultipleAssignmentSource(found: Type)(using Context) def explain(using Context) = "" } +class InvalidMultipleAssignmentTarget()(using Context) + extends TypeMsg(InvalidMultipleAssignmentTargetID) { + def msg(using Context) = + i"""invalid target of multiple assignment. + |Multiple assignments admit only one level of nesting.""" + def explain(using Context) = "" +} + class MultipleAssignmentShapeMismatch(found: Int, required: Int)(using Context) extends TypeMsg(MultipleAssignmentShapeMismatchID) { def msg(using Context) = diff --git a/compiler/src/dotty/tools/dotc/typer/Dynamic.scala b/compiler/src/dotty/tools/dotc/typer/Dynamic.scala index 14cc7bf963a6..5f037cc61c09 100644 --- a/compiler/src/dotty/tools/dotc/typer/Dynamic.scala +++ b/compiler/src/dotty/tools/dotc/typer/Dynamic.scala @@ -141,6 +141,33 @@ trait Dynamic { } } + /** Returns the partial application of a dynamic assignment translating selection that does not + * typecheck according to the normal rules into a `updateDynamic`. + * + * For example: `foo.bar = baz ~~> foo.updateDynamic(bar)(baz)` + * + * @param lhs The target of the assignment. + */ + def formPartialDynamicAssignment( + lhs: untpd.Tree + )(using Context): PartialAssignment[SimpleLValue] = + lhs match + case s @ Select(q, n) if !isDynamicMethod(n) => + formPartialDynamicAssignment(q, n, s.span, Nil) + case TypeApply(s @ Select(q, n), targs) if !isDynamicMethod(n) => + formPartialDynamicAssignment(q, n, s.span, Nil) + case _ => + val e = errorTree(lhs, ReassignmentToVal(lhs.symbol.name)) + PartialAssignment(SimpleLValue(e)) { (l, _) => l.expression } + + def formPartialDynamicAssignment( + q: untpd.Tree, n: Name, s: Span, targs: List[untpd.Tree] + )(using Context): PartialAssignment[SimpleLValue] = + val v = typed(coreDynamic(q, nme.updateDynamic, n, s, targs)) + PartialAssignment(SimpleLValue(v)) { (l, r) => + untpd.Apply(untpd.TypedSplice(l.expression), List(r)) + } + private def coreDynamic(qual: untpd.Tree, dynName: Name, name: Name, selSpan: Span, targs: List[untpd.Tree])(using Context): untpd.Apply = { val select = untpd.Select(qual, dynName).withSpan(selSpan) val selectWithTypes = diff --git a/compiler/src/dotty/tools/dotc/typer/PartialAssignment.scala b/compiler/src/dotty/tools/dotc/typer/PartialAssignment.scala new file mode 100644 index 000000000000..fd72acbf995b --- /dev/null +++ b/compiler/src/dotty/tools/dotc/typer/PartialAssignment.scala @@ -0,0 +1,123 @@ +package dotty.tools +package dotc +package typer + +import dotty.tools.dotc.ast.Trees.ApplyKind +import dotty.tools.dotc.ast.tpd +import dotty.tools.dotc.ast.untpd +import dotty.tools.dotc.core.Contexts.Context +import dotty.tools.dotc.core.Names.Name +import core.Symbols.defn + +/** A function computing the assignment of a lvalue. + * + * @param lhs The target of the assignment. + * @param perform: A closure that accepts `lhs` and an untyped tree `rhs`, and returns a tree + * representing the assignment of `rhs` ro `lhs`. + */ +private[typer] final class PartialAssignment[+T <: LValue](val lhs: T)( + perform: (T, untpd.Tree) => untpd.Tree +): + + /** Returns a tree computing the assignment of `rhs` to `lhs`. */ + def apply(rhs: untpd.Tree): untpd.Tree = + perform(lhs, rhs) + +end PartialAssignment + +/** The left-hand side of an assignment. */ +private[typer] sealed abstract class LValue: + + /** Returns the local `val` definitions composing this lvalue. */ + def locals: List[tpd.ValDef] + + /** Returns this lvalue converted to a rvalue. */ + def toRValue(using Context): untpd.Tree + + /** Returns a tree computing the assignment of `rhs` to this lvalue. */ + def formAssignment(rhs: untpd.Tree)(using Context): untpd.Tree + + /** Returns the value of `t`, which may be an expression or a local `val` definition. */ + protected final def read(t: tpd.Tree)(using Context): tpd.Tree = + t match + case d: tpd.ValDef => tpd.Ident(d.namedType) + case e => e + +end LValue + +/** A simple expression, typically valid on left-hand side of an `Assign` tree. + * + * Use this class to represent an assignment that translates to an `Assign` tree or to wrap an + * error whose diagnostic can be delayed until the right-hand side is known. + * + * @param expression The expression of the lvalue. + */ +private[typer] final case class SimpleLValue(expression: tpd.Tree) extends LValue: + + def locals: List[tpd.ValDef] = + List() + + def toRValue(using Context): untpd.Tree = + untpd.TypedSplice(expression) + + def formAssignment(rhs: untpd.Tree)(using Context): untpd.Tree = + val s = untpd.Assign(untpd.TypedSplice(expression), rhs) + untpd.TypedSplice(s.withType(defn.UnitType)) + +end SimpleLValue + +/** A lvalue represeted by the partial application a function. + * + * @param function The partially applied function. + * @param arguments The arguments of the partial application. + * @param kind The way in which the function is applied. + */ +private[typer] final case class ApplyLValue( + function: tpd.Tree, + arguments: List[tpd.Tree], + kind: ApplyKind = ApplyKind.Regular +) extends LValue: + + val locals: List[tpd.ValDef] = + (function +: arguments).collect { case d: tpd.ValDef => d } + + def toRValue(using Context): untpd.Tree = + untpd.Apply(function, arguments) + + def formAssignment(rhs: untpd.Tree)(using Context): untpd.Tree = + val s = untpd.TypedSplice(read(function)) + val t = arguments.map((a) => untpd.TypedSplice(read(a))) :+ rhs + untpd.Apply(s, t) + +end ApplyLValue + +/** A lvalue represeted by the application of a partially applied method. + * + * @param receiver The receiver of the partially applied method. + * @param member The name of the partially applied method. + * @param arguments The arguments of the partial application. + */ +private[typer] final case class SelectLValue( + receiver: tpd.Tree, + member: Name, + arguments: List[tpd.Tree] = List() +) extends LValue: + + def expandReceiver()(using Context): tpd.Tree = + receiver match + case d: tpd.ValDef => d.rhs + case r => r + + val locals: List[tpd.ValDef] = + (receiver +: arguments).collect { case d: tpd.ValDef => d } + + def toRValue(using Context): untpd.Tree = + require(arguments.isEmpty) + untpd.Select(untpd.TypedSplice(expandReceiver()), member) + + def formAssignment(rhs: untpd.Tree)(using Context): untpd.Tree = + val s = untpd.Select(untpd.TypedSplice(read(receiver)), member) + val t = arguments.map((a) => untpd.TypedSplice(read(a))) :+ rhs + untpd.Apply(s, t) + +end SelectLValue diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index c76dac1d0c3c..84feb16ea152 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1297,180 +1297,220 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer assignType(cpy.NamedArg(tree)(tree.name, arg1), arg1) } - def typedAssign(tree: untpd.Assign, pt: Type)(using Context): Tree = - tree.lhs match { - case lhs @ Apply(fn, args) => - typed(untpd.Apply(untpd.Select(fn, nme.update), args :+ tree.rhs), pt) - case untpd.Tuple(lhs) => - val locked = ctx.typerState.ownedVars - val rhs = typed(tree.rhs, WildcardType) - typedPairwiseAssignments(lhs, rhs) - case untpd.TypedSplice(Apply(MaybePoly(Select(fn, app), targs), args)) if app == nme.apply => - val rawUpdate: untpd.Tree = untpd.Select(untpd.TypedSplice(fn), nme.update) - val wrappedUpdate = - if (targs.isEmpty) rawUpdate - else untpd.TypeApply(rawUpdate, targs map (untpd.TypedSplice(_))) - val appliedUpdate = - untpd.Apply(wrappedUpdate, (args map (untpd.TypedSplice(_))) :+ tree.rhs) - typed(appliedUpdate, pt) - case lhs => - val locked = ctx.typerState.ownedVars - val lhsCore = typedUnadapted(lhs, LhsProto, locked) - def lhs1 = adapt(lhsCore, LhsProto, locked) - - def reassignmentToVal = - report.error(ReassignmentToVal(lhsCore.symbol.name), tree.srcPos) - cpy.Assign(tree)(lhsCore, typed(tree.rhs, lhs1.tpe.widen)).withType(defn.UnitType) - - def canAssign(sym: Symbol) = - sym.is(Mutable, butNot = Accessor) || - ctx.owner.isPrimaryConstructor && !sym.is(Method) && sym.maybeOwner == ctx.owner.owner || - // allow assignments from the primary constructor to class fields - ctx.owner.name.is(TraitSetterName) || ctx.owner.isStaticConstructor - - /** Mark private variables that are assigned with a prefix other than - * the `this` type of their owner with a `annotation.internal.AssignedNonLocally` - * annotation. The annotation influences the variance check for these - * variables, which is done at PostTyper. It will be removed after the - * variance check. - */ - def rememberNonLocalAssignToPrivate(sym: Symbol) = lhs1 match - case Select(qual, _) - if sym.is(Private, butNot = Local) && !sym.isAccessPrivilegedThisType(qual.tpe) => - sym.addAnnotation(Annotation(defn.AssignedNonLocallyAnnot, lhs1.span)) - case _ => + /** Returns `e` if evaluating `e` doesn't cause any side effect. Otherwise, returns a synthethic + * val definition binding the result of `e`. + */ + def temporary(e: tpd.Tree)(using Context): tpd.Tree = + if exprPurity(e) >= TreeInfo.Pure then + e + else + tpd.SyntheticValDef(TempResultName.fresh(), e) + + /** Returns a builder for computing trees representing assignments to `lhs`. */ + def formPartialAssignmentTo(lhs: untpd.Tree)(using Context): PartialAssignment[LValue] = + lhs match + case lhs @ Apply(f, as) => + // LHS is an application `f(a1, ..., an)` that desugars to `f.update(a1, ..., an, rhs)`. + val v = SelectLValue(temporary(typed(f)), nme.update, as.map((a) => temporary(typed(a)))) + PartialAssignment(v) { (l, r) => l.formAssignment(r) } + + case untpd.TypedSplice(Apply(MaybePoly(Select(fn, app), tas), as)) if app == nme.apply => + if tas.isEmpty then + // No type arguments: fall back to a regular update. + val v = SelectLValue(temporary(fn), nme.update, as.map((a) => temporary(typed(a)))) + PartialAssignment(v) { (l, r) => l.formAssignment(r) } + else + // Type arguments are present; the LHS requires a type application. + val s: untpd.Tree = untpd.Select(untpd.TypedSplice(fn), nme.update) + val t = untpd.TypeApply(s, tas.map((ta) => untpd.TypedSplice(ta))) + val v = ApplyLValue(temporary(typed(t)), as.map((a) => temporary(typed(a)))) + PartialAssignment(v) { (l, r) => l.formAssignment(r) } - lhsCore match - case Apply(fn, _) if fn.symbol.is(ExtensionMethod) => - def toSetter(fn: Tree): untpd.Tree = fn match - case fn @ Ident(name: TermName) => - // We need to make sure that the prefix of this extension getter is - // retained when we transform it into a setter. Otherwise, we could - // end up resolving an unrelated setter from another extension. We - // transform the `Ident` into a `Select` to ensure that the prefix - // is retained with a `TypedSplice` (see `case Select` bellow). - // See tests/pos/i18713.scala for an example. - fn.tpe match - case TermRef(qual: TermRef, _) => - toSetter(ref(qual).select(fn.symbol).withSpan(fn.span)) - case TermRef(qual: ThisType, _) => - toSetter(This(qual.cls).select(fn.symbol).withSpan(fn.span)) - case TermRef(NoPrefix, _) => - untpd.cpy.Ident(fn)(name.setterName) - case fn @ Select(qual, name: TermName) => - untpd.cpy.Select(fn)(untpd.TypedSplice(qual), name.setterName) - case fn @ TypeApply(fn1, targs) => - untpd.cpy.TypeApply(fn)(toSetter(fn1), targs.map(untpd.TypedSplice(_))) - case fn @ Apply(fn1, args) => - val result = untpd.cpy.Apply(fn)( - toSetter(fn1), - args.map(untpd.TypedSplice(_, isExtensionReceiver = true))) - fn1 match - case Apply(_, _) => // current apply is to implicit arguments - result.setApplyKind(ApplyKind.Using) - // Note that we cannot copy the apply kind of `fn` since `fn` is a typed - // tree and applyKinds are not preserved for those. - case _ => result - case _ => - EmptyTree + case _ => + formPartialAssignmentToNonApply(lhs) - val setter = toSetter(lhsCore) - if setter.isEmpty then reassignmentToVal - else - val assign = untpd.Apply(setter, tree.rhs :: Nil) - typed(assign, IgnoredProto(pt)) - case _ => lhsCore.tpe match { - case ref: TermRef => - val lhsVal = lhsCore.denot.suchThat(!_.is(Method)) - val lhsSym = lhsVal.symbol - if canAssign(lhsSym) then - rememberNonLocalAssignToPrivate(lhsSym) - // lhsBounds: (T .. Any) as seen from lhs prefix, where T is the type of lhsSym - // This ensures we do the as-seen-from on T with variance -1. Test case neg/i2928.scala - val lhsBounds = - TypeBounds.lower(lhsSym.info).asSeenFrom(ref.prefix, lhsSym.owner) - assignType(cpy.Assign(tree)(lhs1, typed(tree.rhs, lhsBounds.loBound))) - .computeAssignNullable() - else - val pre = ref.prefix - val setterName = ref.name.setterName - val setter = pre.member(setterName) - lhsCore match { - case lhsCore: RefTree if setter.exists => - val setterTypeRaw = pre.select(setterName, setter) - val setterType = ensureAccessible(setterTypeRaw, isSuperSelection(lhsCore), tree.srcPos) - val lhs2 = untpd.rename(lhsCore, setterName).withType(setterType) - typedUnadapted(untpd.Apply(untpd.TypedSplice(lhs2), tree.rhs :: Nil), WildcardType, locked) - case _ => - reassignmentToVal + /** Returns a builder for computing trees representing assignments to `lhs`, which isn't a term + * or type application. + */ + def formPartialAssignmentToNonApply(lhs: untpd.Tree)(using Context): PartialAssignment[LValue] = + val locked = ctx.typerState.ownedVars + val core = typedUnadapted(lhs, LhsProto, locked) + def adapted = adapt(core, LhsProto, locked) + + def reassignmentToVal(): PartialAssignment[SimpleLValue] = + PartialAssignment(SimpleLValue(core)) { (l, r) => + val target = l.expression + report.error(ReassignmentToVal(target.symbol.name), target.srcPos) + untpd.TypedSplice(tpd.Assign(target, typed(r, adapted.tpe.widen))) + } + + /** Returns `true` if `s` is assignable. */ + def canAssign(s: Symbol) = + s.is(Mutable, butNot = Accessor) || + ctx.owner.isPrimaryConstructor && !s.is(Method) && s.maybeOwner == ctx.owner.owner || + // allow assignments from the primary constructor to class fields + ctx.owner.name.is(TraitSetterName) || ctx.owner.isStaticConstructor + + /** Marks private variables that are assigned with a prefix other than the `this` type of their + * owner with a `annotation.internal.AssignedNonLocally` annotation. The annotation influences + * the variance check for these variables, which is done at PostTyper. It will be removed + * after the variance check. + */ + def rememberNonLocalAssignToPrivate(s: Symbol) = adapted match + case Select(q, _) if s.is(Private, butNot = Local) && !s.isAccessPrivilegedThisType(q.tpe) => + s.addAnnotation(Annotation(defn.AssignedNonLocallyAnnot, adapted.span)) + case _ => () + + core match + case Apply(f, _) if f.symbol.is(ExtensionMethod) => + formPartialAssignmentToExtensionApply(f, reassignmentToVal).getOrElse(reassignmentToVal()) + + case _ => core.tpe match + case r: TermRef => + val v = core.denot.suchThat(!_.is(Method)) + if canAssign(v.symbol) then + rememberNonLocalAssignToPrivate(v.symbol) + // bounds: (T .. Any) as seen from lhs prefix, where T is the type of v.symbol + // This ensures we do the as-seen-from on T with variance -1. Test case neg/i2928.scala + val bounds = TypeBounds.lower(v.symbol.info).asSeenFrom(r.prefix, v.symbol.owner) + PartialAssignment(SimpleLValue(adapted)) { (l, r) => + val s = tpd.Assign(l.expression, typed(r, bounds.loBound)) + untpd.TypedSplice(s.computeAssignNullable()) + } + else + val setterName = r.name.setterName + val setter = r.prefix.member(setterName) + core match + case core: RefTree if setter.exists => + val t = r.prefix.select(setterName, setter) + val u = ensureAccessible(t, isSuperSelection(core), lhs.srcPos) + val v = untpd.rename(core, setterName).withType(u) + PartialAssignment(SimpleLValue(v)) { (l, r) => + untpd.Apply(untpd.TypedSplice(l.expression), List(r)) + // QUESTION: Why do we need the `typedUnadapted(s, WildcardType, locked)`? + // val s = untpd.Apply(untpd.TypedSplice(lvalue.expression), List(rhs)) + // typedUnadapted(s, WildcardType, locked) } - case TryDynamicCallType => - typedDynamicAssign(tree, pt) - case tpe => - reassignmentToVal - } - } + case _ => + reassignmentToVal() - /** Returns a block assigning to `targets` the corresponding values in `rhs`. - * - * @param targets A sequence of untyped trees on the LHS of a multiple assignment. - * @param rhs The RHS of a multuple assignment. - */ - private def typedPairwiseAssignments( - targets: List[untpd.Tree], rhs: Tree - )(using Context): Tree = - val s = mutable.ListBuffer[untpd.Tree]() - formPairwiseAssignments( - s, targets, untpd.TypedSplice(rhs), rhs.tpe, - EmptyTermName) - typed(untpd.Block(s.toList, untpd.TypedSplice(unitLiteral))) - - /** Appends to `statements` the assignments of `targets` to corresponding values in `rhs`. - * - * @param statements A partially constructed sequence representing the desugaring of a - * multiple assignment. - * @param targets A sequence of expression representing variables to assign. - * @param rhs The expression of the values to assign.. - * @param rhsType The type of `rhs`. - * @param prefix A prefix for the name of syntactic vals created by the methods. - */ - private def formPairwiseAssignments( - statements: mutable.ListBuffer[untpd.Tree], - targets: List[untpd.Tree], - rhs: untpd.Tree, - rhsType: Type, - prefix: TermName - )(using Context): Unit = - val source = UniqueName.fresh(prefix) - val d = untpd.ValDef(source, untpd.TypeTree(rhsType), rhs) - .withSpan(rhs.span) - .withFlags(Synthetic) - statements.append(d) - - rhsType.tupleElementTypes match { - case None => - errorTree(rhs, InvalidMultipleAssignmentSource(rhsType)) + case TryDynamicCallType => + formPartialDynamicAssignment(lhs) - case Some(e) if targets.length != e.length => - errorTree(rhs, MultipleAssignmentShapeMismatch(e.length, targets.length)) + case _ => + reassignmentToVal() + + /** Returns a builder for computing trees representing assignments to `lhs`, which denotes the + * use of a setter defined in an extension. + */ + def formPartialAssignmentToExtensionApply( + lhs: Tree, reassignmentToVal: () => PartialAssignment[LValue] + )(using Context): Option[PartialAssignment[LValue]] = + def formAssignment(v: LValue): PartialAssignment[LValue] = + PartialAssignment(v) { (l, r) => + // QUESTION: Why do we need `IgnoredProto(pt)`= + // typed(l.formAssignment(r), IgnoredProto(e)) + l.formAssignment(r) + } - case Some(sourceTypes) => - /** The value to assign to the `i`-th source. */ - def rhsElement(i: Int): untpd.Tree = - untpd.Select(untpd.Ident(source), nme.productAccessorName(i)) - .withSpan(rhs.span) - - /** Forms the assignments of the targets in the range [`i`, `targets.length`). */ - for i <- 0 until targets.length do { - targets(i) match { - case untpd.Tuple(lhs) => - formPairwiseAssignments(statements, lhs, rhsElement(i + 1), sourceTypes(i), source) - case lhs => - val u = untpd.Assign(lhs, rhsElement(i + 1)) - statements.append(u) + lhs match + case f @ Ident(name: TermName) => + // We need to make sure that the prefix of this extension getter is retained when we + // transform it into a setter. Otherwise, we could end up resolving an unrelated setter + // from another extension. See tests/pos/i18713.scala for an example. + val v: SelectLValue = f.tpe match + case TermRef(q: TermRef, _) => + SelectLValue(temporary(ref(q)), f.symbol.name) + case TermRef(q: ThisType, _) => + SelectLValue(temporary(This(q.cls)), f.symbol.name) + case TermRef(NoPrefix, _) => + SelectLValue(f, name.setterName) + Some(formAssignment(v)) + + case f @ Select(q, name: TermName) => + Some(formAssignment(SelectLValue(temporary(q), name.setterName))) + + case f @ TypeApply(f1, tas) => + formPartialAssignmentToExtensionApply(f1, reassignmentToVal).map { (partialAssignment) => + val s = partialAssignment.lhs.toRValue + val t = untpd.cpy.TypeApply(f)(s, tas.map((ta) => untpd.TypedSplice(ta))) + formAssignment(ApplyLValue(temporary(typed(t)), List())) + } + + case f @ Apply(f1, as) => + formPartialAssignmentToExtensionApply(f1, reassignmentToVal).map { (partialAssignment) => + val applyKind = f1 match + case _: Apply => + // Current apply is to implicit arguments. Note that we cannot copy the apply kind + // of `f` since `f` is a typed tree and apply kinds are not preserved for those. + ApplyKind.Using + case _ => + ApplyKind.Regular + + val arguments = as.map { (a) => + val x = untpd.TypedSplice(a, isExtensionReceiver = true) + temporary(typed(x)) } + val s = partialAssignment.lhs.toRValue + formAssignment(ApplyLValue(temporary(typed(s)), arguments, applyKind)) } - } + + case _ => + None + + def typedAssign(tree: untpd.Assign, pt: Type)(using Context): Tree = + tree.lhs match + case untpd.Tuple(lhs) => + // Multiple assignment. + typedMultipleAssign(lhs, tree.rhs) + case _ => + // Simple assignment. + val assignmentBuilder = formPartialAssignmentTo(tree.lhs) + val locals = assignmentBuilder.lhs.locals.map((d) => untpd.TypedSplice(d)) + if locals.isEmpty then + typed(assignmentBuilder(tree.rhs)) + else + typed(untpd.Block(locals, assignmentBuilder(tree.rhs))) + + def typedMultipleAssign(targets: List[untpd.Tree], source: untpd.Tree)(using Context): Tree = + val rhs = typed(source, WildcardType) + rhs.tpe.tupleElementTypes match + case None => + errorTree(rhs, InvalidMultipleAssignmentSource(rhs.tpe)) + case Some(e) if targets.length != e.length => + errorTree(rhs, MultipleAssignmentShapeMismatch(e.length, targets.length)) + case _ => + val statements = mutable.ListBuffer[untpd.Tree]() + val assignmentBuilders = mutable.ListBuffer[PartialAssignment[LValue]]() + + // Compute the targets of each assignment, hoisting impure intermediate steps. + for l <- targets do + l match + case _: untpd.Tuple => + val e = errorTree(l, InvalidMultipleAssignmentTarget()) + val s = PartialAssignment(SimpleLValue(e)) { (l, _) => l.expression } + assignmentBuilders.append(s) + case _ => + val s = formPartialAssignmentTo(l) + statements.appendAll(s.lhs.locals.map((d) => untpd.TypedSplice(d))) + assignmentBuilders.append(s) + + // Compute the right-hand side value. + // QUESTION: `withSpan(rhs.span)` is necessary or the pos test will fail pickling; why? + // It seems like the symbol gets a different unpickled position if its span is synthetic. + val d = tpd.SyntheticValDef(TempResultName.fresh(), rhs).withSpan(rhs.span) + statements.append(untpd.TypedSplice(d)) + + // Append the assignments. + var i = 0 + for l <- targets do + val r = untpd.Select( + untpd.TypedSplice(tpd.Ident(d.namedType)), + nme.productAccessorName(i + 1) + ).withSpan(rhs.span) + statements.append(assignmentBuilders(i)(r)) + i += 1 + typed(untpd.Block(statements.toList, untpd.TypedSplice(unitLiteral))) def typedBlockStats(stats: List[untpd.Tree])(using Context): (List[tpd.Tree], Context) = index(stats) diff --git a/tests/neg/multiple-assignment.check b/tests/neg/multiple-assignment.check index 1c43f2eca809..f06e5a4d7ea4 100644 --- a/tests/neg/multiple-assignment.check +++ b/tests/neg/multiple-assignment.check @@ -1,11 +1,23 @@ --- [E193] Type Error: tests/neg/multiple-assignment.scala:9:16 ----------------- -9 | ((x, y), z) = 1 - | ^ - | invalid source of multiple assignment. - | The right hand side must be a tuple but (1 : Int) was found. --- [E194] Type Error: tests/neg/multiple-assignment.scala:10:11 ---------------- -10 | (x, y) = (1, 1, 1) +-- [E201] Type Error: tests/neg/multiple-assignment.scala:9:11 --------------------------------------------------------- +9 | (x, y) = 1 // error + | ^ + | invalid source of multiple assignment. + | The right hand side must be a tuple but (1 : Int) was found. +-- [E203] Type Error: tests/neg/multiple-assignment.scala:10:11 -------------------------------------------------------- +10 | (x, y) = (1, 1, 1) // error | ^^^^^^^^^ - | Source and target of multiple assignment have different sizes. - | Source: 3 - | Target: 2 + | Source and target of multiple assignment have different sizes. + | Source: 3 + | Target: 2 +-- [E007] Type Mismatch Error: tests/neg/multiple-assignment.scala:11:11 ----------------------------------------------- +11 | (x, y) = (1, 2) // error + | ^^^^^^ + | Found: (ev$1._2 : Int) + | Required: Boolean + | + | longer explanation available when compiling with `-explain` +-- [E202] Type Error: tests/neg/multiple-assignment.scala:12:6 --------------------------------------------------------- +12 | (x, (y, z)) = (1, (true, "b")) // error + | ^^^^^^ + | invalid target of multiple assignment. + | Multiple assignments admit only one level of nesting. diff --git a/tests/neg/multiple-assignment.scala b/tests/neg/multiple-assignment.scala index 17eb2d7945d1..06fc731fe5e8 100644 --- a/tests/neg/multiple-assignment.scala +++ b/tests/neg/multiple-assignment.scala @@ -6,5 +6,7 @@ def tu(): (Int, Boolean) = (1, true) var y = false var z = "a" - ((x, y), z) = 1 - (x, y) = (1, 1, 1) + (x, y) = 1 // error + (x, y) = (1, 1, 1) // error + (x, y) = (1, 2) // error + (x, (y, z)) = (1, (true, "b")) // error diff --git a/tests/pos/multiple-assignment.scala b/tests/pos/multiple-assignment.scala index a073deccb77f..960f0c25ad8a 100644 --- a/tests/pos/multiple-assignment.scala +++ b/tests/pos/multiple-assignment.scala @@ -5,5 +5,6 @@ def tu(): (Int, Boolean) = (1, true) var y = false var z = "a" - ((x, y), z) = (tu(), "b") - + x = 99 + (x, y) = tu() + (x, z) = (2, "b") From 431cffd124630bbf9dfafadb62fd8c03201e102d Mon Sep 17 00:00:00 2001 From: Dimi Racordon Date: Wed, 5 Jun 2024 16:14:15 +0200 Subject: [PATCH 11/24] Update compiler/src/dotty/tools/dotc/typer/PartialAssignment.scala Co-authored-by: Matt Bovel --- compiler/src/dotty/tools/dotc/typer/PartialAssignment.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/typer/PartialAssignment.scala b/compiler/src/dotty/tools/dotc/typer/PartialAssignment.scala index fd72acbf995b..376a14f42fd0 100644 --- a/compiler/src/dotty/tools/dotc/typer/PartialAssignment.scala +++ b/compiler/src/dotty/tools/dotc/typer/PartialAssignment.scala @@ -13,7 +13,7 @@ import core.Symbols.defn * * @param lhs The target of the assignment. * @param perform: A closure that accepts `lhs` and an untyped tree `rhs`, and returns a tree - * representing the assignment of `rhs` ro `lhs`. + * representing the assignment of `rhs` to `lhs`. */ private[typer] final class PartialAssignment[+T <: LValue](val lhs: T)( perform: (T, untpd.Tree) => untpd.Tree From 4566328e86d998f99809fdac97e7db80aab0cbfc Mon Sep 17 00:00:00 2001 From: Dimi Racordon Date: Tue, 18 Jun 2024 08:27:19 +0200 Subject: [PATCH 12/24] Use unadapted apply in desugaring of setter --- compiler/src/dotty/tools/dotc/typer/Typer.scala | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 84feb16ea152..bdd755df9b31 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1386,10 +1386,9 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val u = ensureAccessible(t, isSuperSelection(core), lhs.srcPos) val v = untpd.rename(core, setterName).withType(u) PartialAssignment(SimpleLValue(v)) { (l, r) => - untpd.Apply(untpd.TypedSplice(l.expression), List(r)) // QUESTION: Why do we need the `typedUnadapted(s, WildcardType, locked)`? - // val s = untpd.Apply(untpd.TypedSplice(lvalue.expression), List(rhs)) - // typedUnadapted(s, WildcardType, locked) + val s = untpd.Apply(untpd.TypedSplice(l.expression), List(r)) + untpd.TypedSplice(typedUnadapted(s, WildcardType, locked)) } case _ => reassignmentToVal() From 1b98ba425fcdf866eb12708c6f9e53f44ac88cd6 Mon Sep 17 00:00:00 2001 From: Dimi Racordon Date: Thu, 25 Jul 2024 16:14:35 +0200 Subject: [PATCH 13/24] Fix the computation of partial assignments to setter in extensions --- .../tools/dotc/typer/PartialAssignment.scala | 22 ++-- .../src/dotty/tools/dotc/typer/Typer.scala | 107 ++++++++++-------- 2 files changed, 66 insertions(+), 63 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/PartialAssignment.scala b/compiler/src/dotty/tools/dotc/typer/PartialAssignment.scala index 376a14f42fd0..05d07909826b 100644 --- a/compiler/src/dotty/tools/dotc/typer/PartialAssignment.scala +++ b/compiler/src/dotty/tools/dotc/typer/PartialAssignment.scala @@ -31,9 +31,6 @@ private[typer] sealed abstract class LValue: /** Returns the local `val` definitions composing this lvalue. */ def locals: List[tpd.ValDef] - /** Returns this lvalue converted to a rvalue. */ - def toRValue(using Context): untpd.Tree - /** Returns a tree computing the assignment of `rhs` to this lvalue. */ def formAssignment(rhs: untpd.Tree)(using Context): untpd.Tree @@ -45,6 +42,15 @@ private[typer] sealed abstract class LValue: end LValue +private[typer] final case class UnappliedSetter( + expression: untpd.Tree, locals: List[tpd.ValDef] +) extends LValue: + + def formAssignment(rhs: untpd.Tree)(using Context): untpd.Tree = + untpd.Apply(expression, List(rhs)) + +end UnappliedSetter + /** A simple expression, typically valid on left-hand side of an `Assign` tree. * * Use this class to represent an assignment that translates to an `Assign` tree or to wrap an @@ -57,9 +63,6 @@ private[typer] final case class SimpleLValue(expression: tpd.Tree) extends LValu def locals: List[tpd.ValDef] = List() - def toRValue(using Context): untpd.Tree = - untpd.TypedSplice(expression) - def formAssignment(rhs: untpd.Tree)(using Context): untpd.Tree = val s = untpd.Assign(untpd.TypedSplice(expression), rhs) untpd.TypedSplice(s.withType(defn.UnitType)) @@ -81,9 +84,6 @@ private[typer] final case class ApplyLValue( val locals: List[tpd.ValDef] = (function +: arguments).collect { case d: tpd.ValDef => d } - def toRValue(using Context): untpd.Tree = - untpd.Apply(function, arguments) - def formAssignment(rhs: untpd.Tree)(using Context): untpd.Tree = val s = untpd.TypedSplice(read(function)) val t = arguments.map((a) => untpd.TypedSplice(read(a))) :+ rhs @@ -111,10 +111,6 @@ private[typer] final case class SelectLValue( val locals: List[tpd.ValDef] = (receiver +: arguments).collect { case d: tpd.ValDef => d } - def toRValue(using Context): untpd.Tree = - require(arguments.isEmpty) - untpd.Select(untpd.TypedSplice(expandReceiver()), member) - def formAssignment(rhs: untpd.Tree)(using Context): untpd.Tree = val s = untpd.Select(untpd.TypedSplice(read(receiver)), member) val t = arguments.map((a) => untpd.TypedSplice(read(a))) :+ rhs diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index bdd755df9b31..e24fce8e2733 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1306,6 +1306,16 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer else tpd.SyntheticValDef(TempResultName.fresh(), e) + /** Returns `(n, Some(d))` where `n` is the name of a synthetic val `d` that binds the result of + * `e` if evaluating `e` is impure. Otherwise, returns `(e, None)`. + */ + def hoisted(e: tpd.Tree)(using Context): (tpd.Tree, Option[tpd.ValDef]) = + if exprPurity(e) >= TreeInfo.Pure then + (e, None) + else + val d = tpd.SyntheticValDef(TempResultName.fresh(), e) + (tpd.Ident(d.namedType), Some(d)) + /** Returns a builder for computing trees representing assignments to `lhs`. */ def formPartialAssignmentTo(lhs: untpd.Tree)(using Context): PartialAssignment[LValue] = lhs match @@ -1363,7 +1373,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer core match case Apply(f, _) if f.symbol.is(ExtensionMethod) => - formPartialAssignmentToExtensionApply(f, reassignmentToVal).getOrElse(reassignmentToVal()) + formPartialAssignmentToExtensionApply(core).getOrElse(reassignmentToVal()) case _ => core.tpe match case r: TermRef => @@ -1399,63 +1409,60 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer case _ => reassignmentToVal() - /** Returns a builder for computing trees representing assignments to `lhs`, which denotes the - * use of a setter defined in an extension. - */ + /** Returns a builder for making trees representing assignments to `lhs`, which denotes a setter + * defined in an extension. + */ def formPartialAssignmentToExtensionApply( - lhs: Tree, reassignmentToVal: () => PartialAssignment[LValue] + lhs: Tree )(using Context): Option[PartialAssignment[LValue]] = - def formAssignment(v: LValue): PartialAssignment[LValue] = - PartialAssignment(v) { (l, r) => - // QUESTION: Why do we need `IgnoredProto(pt)`= - // typed(l.formAssignment(r), IgnoredProto(e)) - l.formAssignment(r) - } - - lhs match - case f @ Ident(name: TermName) => - // We need to make sure that the prefix of this extension getter is retained when we - // transform it into a setter. Otherwise, we could end up resolving an unrelated setter - // from another extension. See tests/pos/i18713.scala for an example. - val v: SelectLValue = f.tpe match - case TermRef(q: TermRef, _) => - SelectLValue(temporary(ref(q)), f.symbol.name) - case TermRef(q: ThisType, _) => - SelectLValue(temporary(This(q.cls)), f.symbol.name) - case TermRef(NoPrefix, _) => - SelectLValue(f, name.setterName) - Some(formAssignment(v)) - - case f @ Select(q, name: TermName) => - Some(formAssignment(SelectLValue(temporary(q), name.setterName))) - - case f @ TypeApply(f1, tas) => - formPartialAssignmentToExtensionApply(f1, reassignmentToVal).map { (partialAssignment) => - val s = partialAssignment.lhs.toRValue - val t = untpd.cpy.TypeApply(f)(s, tas.map((ta) => untpd.TypedSplice(ta))) - formAssignment(ApplyLValue(temporary(typed(t)), List())) - } - - case f @ Apply(f1, as) => - formPartialAssignmentToExtensionApply(f1, reassignmentToVal).map { (partialAssignment) => - val applyKind = f1 match + /** Returns the setter corresponding to `lhs`, which is a getter, along hoisted definitions. */ + def formSetter(lhs: Tree, captures: List[Tree]): (untpd.Tree, List[Tree]) = + lhs match + case f @ Ident(name: TermName) => + // We need to make sure that the prefix of this extension getter is retained when we + // transform it into a setter. Otherwise, we could end up resolving an unrelated setter + // from another extension. See tests/pos/i18713.scala for an example. + f.tpe match + case TermRef(q: TermRef, _) => + formSetter(ref(q).select(f.symbol).withSpan(f.span), captures) + case TermRef(q: ThisType, _) => + formSetter(This(q.cls).select(f.symbol).withSpan(f.span), captures) + case TermRef(NoPrefix, _) => + (untpd.cpy.Ident(f)(name.setterName), captures) + + case f @ Select(q, name: TermName) => + val (v, d) = hoisted(q) + (untpd.cpy.Select(f)(untpd.TypedSplice(v), name.setterName), captures ++ d) + + case f @ TypeApply(g, ts) => + val (s, cs) = formSetter(g, captures) + (untpd.cpy.TypeApply(f)(s, ts.map((t) => untpd.TypedSplice(t))), cs) + + case f @ Apply(g, as) => + var (s, newCaptures) = formSetter(g, captures) + var arguments = List[untpd.Tree]() + for a <- as do + val (v, d) = hoisted(a) + arguments = untpd.TypedSplice(v, isExtensionReceiver = true) +: arguments + newCaptures = newCaptures ++ d + + val setter = untpd.cpy.Apply(f)(s, arguments) + + g match case _: Apply => // Current apply is to implicit arguments. Note that we cannot copy the apply kind // of `f` since `f` is a typed tree and apply kinds are not preserved for those. - ApplyKind.Using + (setter.setApplyKind(ApplyKind.Using), newCaptures) case _ => - ApplyKind.Regular + (setter, newCaptures) - val arguments = as.map { (a) => - val x = untpd.TypedSplice(a, isExtensionReceiver = true) - temporary(typed(x)) - } - val s = partialAssignment.lhs.toRValue - formAssignment(ApplyLValue(temporary(typed(s)), arguments, applyKind)) - } + case _ => + (EmptyTree, List()) - case _ => - None + val (setter, captures) = formSetter(lhs, List()) + if setter.isEmpty then None else + val cs = captures.collect { case d: tpd.ValDef => d } + Some(PartialAssignment(UnappliedSetter(setter, cs)) { (l, r) => l.formAssignment(r) }) def typedAssign(tree: untpd.Assign, pt: Type)(using Context): Tree = tree.lhs match From 71fbbd08089444ea3f813d4d6a1ebcb7daa4234c Mon Sep 17 00:00:00 2001 From: Dimi Racordon Date: Thu, 25 Jul 2024 16:16:51 +0200 Subject: [PATCH 14/24] Remove needless parameter --- compiler/src/dotty/tools/dotc/typer/PartialAssignment.scala | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/PartialAssignment.scala b/compiler/src/dotty/tools/dotc/typer/PartialAssignment.scala index 05d07909826b..c5242da56e4e 100644 --- a/compiler/src/dotty/tools/dotc/typer/PartialAssignment.scala +++ b/compiler/src/dotty/tools/dotc/typer/PartialAssignment.scala @@ -73,12 +73,10 @@ end SimpleLValue * * @param function The partially applied function. * @param arguments The arguments of the partial application. - * @param kind The way in which the function is applied. */ private[typer] final case class ApplyLValue( function: tpd.Tree, - arguments: List[tpd.Tree], - kind: ApplyKind = ApplyKind.Regular + arguments: List[tpd.Tree] ) extends LValue: val locals: List[tpd.ValDef] = From 9a2d8a6be6949da735d0853829e5f9468a44afa5 Mon Sep 17 00:00:00 2001 From: Dimi Racordon Date: Thu, 25 Jul 2024 19:04:05 +0200 Subject: [PATCH 15/24] Improve choice of words in comments --- .../tools/dotc/typer/PartialAssignment.scala | 116 +++++++++++------- .../src/dotty/tools/dotc/typer/Typer.scala | 44 +++---- 2 files changed, 87 insertions(+), 73 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/PartialAssignment.scala b/compiler/src/dotty/tools/dotc/typer/PartialAssignment.scala index c5242da56e4e..98fb7ee933a9 100644 --- a/compiler/src/dotty/tools/dotc/typer/PartialAssignment.scala +++ b/compiler/src/dotty/tools/dotc/typer/PartialAssignment.scala @@ -5,8 +5,11 @@ package typer import dotty.tools.dotc.ast.Trees.ApplyKind import dotty.tools.dotc.ast.tpd import dotty.tools.dotc.ast.untpd +import dotty.tools.dotc.ast.TreeInfo import dotty.tools.dotc.core.Contexts.Context import dotty.tools.dotc.core.Names.Name +import dotty.tools.dotc.core.NameKinds.TempResultName + import core.Symbols.defn /** A function computing the assignment of a lvalue. @@ -25,6 +28,39 @@ private[typer] final class PartialAssignment[+T <: LValue](val lhs: T)( end PartialAssignment +/** The expression of a pure value or a synthetic val definition binding a value whose evaluation + * must be hoisted. + * + * Use this type to represent a part of a lvalue that must be evaluated before the lvalue gets + * used for updating a value. + */ +private[typer] opaque type PossiblyHoistedValue = tpd.Tree + +extension (self: PossiblyHoistedValue) + + /** Returns a tree representing the value of `self`. */ + def value(using Context): tpd.Tree = + self.definition.map((d) => tpd.Ident(d.namedType)).getOrElse(self) + + /** Returns the synthetic val defining `self` if it is hoisted. */ + def definition: Option[tpd.ValDef] = + self match + case d: tpd.ValDef => Some(d) + case _ => None + + /** Returns a tree representing the value of `self` along with its hoisted definition, if any. */ + def valueAndDefinition(using Context): (tpd.Tree, Option[tpd.ValDef]) = + self.definition + .map((d) => (tpd.Ident(d.namedType), Some(d))) + .getOrElse((self, None)) + +object PossiblyHoistedValue: + + /** Creates a value representing the `e`'s evaluation. */ + def apply(e: tpd.Tree)(using Context): PossiblyHoistedValue = + if tpd.exprPurity(e) >= TreeInfo.Pure then e else + tpd.SyntheticValDef(TempResultName.fresh(), e) + /** The left-hand side of an assignment. */ private[typer] sealed abstract class LValue: @@ -34,23 +70,8 @@ private[typer] sealed abstract class LValue: /** Returns a tree computing the assignment of `rhs` to this lvalue. */ def formAssignment(rhs: untpd.Tree)(using Context): untpd.Tree - /** Returns the value of `t`, which may be an expression or a local `val` definition. */ - protected final def read(t: tpd.Tree)(using Context): tpd.Tree = - t match - case d: tpd.ValDef => tpd.Ident(d.namedType) - case e => e - end LValue -private[typer] final case class UnappliedSetter( - expression: untpd.Tree, locals: List[tpd.ValDef] -) extends LValue: - - def formAssignment(rhs: untpd.Tree)(using Context): untpd.Tree = - untpd.Apply(expression, List(rhs)) - -end UnappliedSetter - /** A simple expression, typically valid on left-hand side of an `Assign` tree. * * Use this class to represent an assignment that translates to an `Assign` tree or to wrap an @@ -75,43 +96,52 @@ end SimpleLValue * @param arguments The arguments of the partial application. */ private[typer] final case class ApplyLValue( - function: tpd.Tree, - arguments: List[tpd.Tree] + function: ApplyLValue.Callee, + arguments: List[PossiblyHoistedValue] ) extends LValue: val locals: List[tpd.ValDef] = - (function +: arguments).collect { case d: tpd.ValDef => d } + function.locals ++ (arguments.collect { case d: tpd.ValDef => d }) def formAssignment(rhs: untpd.Tree)(using Context): untpd.Tree = - val s = untpd.TypedSplice(read(function)) - val t = arguments.map((a) => untpd.TypedSplice(read(a))) :+ rhs + val s = function.expanded + val t = arguments.map((a) => untpd.TypedSplice(a.value)) :+ rhs untpd.Apply(s, t) -end ApplyLValue +object ApplyLValue: -/** A lvalue represeted by the application of a partially applied method. - * - * @param receiver The receiver of the partially applied method. - * @param member The name of the partially applied method. - * @param arguments The arguments of the partial application. - */ -private[typer] final case class SelectLValue( - receiver: tpd.Tree, - member: Name, - arguments: List[tpd.Tree] = List() -) extends LValue: + /** The callee of a lvalue represented by a partial application. */ + sealed abstract class Callee: - def expandReceiver()(using Context): tpd.Tree = - receiver match - case d: tpd.ValDef => d.rhs - case r => r + def expanded(using Context): untpd.Tree - val locals: List[tpd.ValDef] = - (receiver +: arguments).collect { case d: tpd.ValDef => d } + /** Returns the local `val` definitions composing this lvalue. */ + def locals: List[tpd.ValDef] - def formAssignment(rhs: untpd.Tree)(using Context): untpd.Tree = - val s = untpd.Select(untpd.TypedSplice(read(receiver)), member) - val t = arguments.map((a) => untpd.TypedSplice(read(a))) :+ rhs - untpd.Apply(s, t) + object Callee: + + def apply(receiver: tpd.Tree)(using Context): Typed = + Typed(PossiblyHoistedValue(receiver), None) + + def apply(receiver: tpd.Tree, member: Name)(using Context): Typed = + Typed(PossiblyHoistedValue(receiver), Some(member)) -end SelectLValue + /** A function representing a lvalue. */ + final case class Typed(receiver: PossiblyHoistedValue, member: Option[Name]) extends Callee: + + def expanded(using Context): untpd.Tree = + val s = untpd.TypedSplice(receiver.value) + member.map((m) => untpd.Select(s, m)).getOrElse(s) + + def locals: List[tpd.ValDef] = + receiver.definition.toList + + /** The untyped expression of a function representing a lvalue along with its captures. */ + final case class Untyped(value: untpd.Tree, locals: List[tpd.ValDef]) extends Callee: + + def expanded(using Context): untpd.Tree = + value + + end Callee + +end ApplyLValue diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index e24fce8e2733..bb6fbbd5d4e4 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1297,50 +1297,34 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer assignType(cpy.NamedArg(tree)(tree.name, arg1), arg1) } - /** Returns `e` if evaluating `e` doesn't cause any side effect. Otherwise, returns a synthethic - * val definition binding the result of `e`. - */ - def temporary(e: tpd.Tree)(using Context): tpd.Tree = - if exprPurity(e) >= TreeInfo.Pure then - e - else - tpd.SyntheticValDef(TempResultName.fresh(), e) - - /** Returns `(n, Some(d))` where `n` is the name of a synthetic val `d` that binds the result of - * `e` if evaluating `e` is impure. Otherwise, returns `(e, None)`. - */ - def hoisted(e: tpd.Tree)(using Context): (tpd.Tree, Option[tpd.ValDef]) = - if exprPurity(e) >= TreeInfo.Pure then - (e, None) - else - val d = tpd.SyntheticValDef(TempResultName.fresh(), e) - (tpd.Ident(d.namedType), Some(d)) - - /** Returns a builder for computing trees representing assignments to `lhs`. */ + /** Returns a builder for making trees representing assignments to `lhs`. */ def formPartialAssignmentTo(lhs: untpd.Tree)(using Context): PartialAssignment[LValue] = lhs match case lhs @ Apply(f, as) => // LHS is an application `f(a1, ..., an)` that desugars to `f.update(a1, ..., an, rhs)`. - val v = SelectLValue(temporary(typed(f)), nme.update, as.map((a) => temporary(typed(a)))) - PartialAssignment(v) { (l, r) => l.formAssignment(r) } + val arguments = as.map((a) => PossiblyHoistedValue(typed(a))) + val lvalue = ApplyLValue(ApplyLValue.Callee(typed(f), nme.update), arguments) + PartialAssignment(lvalue) { (l, r) => l.formAssignment(r) } case untpd.TypedSplice(Apply(MaybePoly(Select(fn, app), tas), as)) if app == nme.apply => if tas.isEmpty then // No type arguments: fall back to a regular update. - val v = SelectLValue(temporary(fn), nme.update, as.map((a) => temporary(typed(a)))) - PartialAssignment(v) { (l, r) => l.formAssignment(r) } + val arguments = as.map(PossiblyHoistedValue.apply) + val lvalue = ApplyLValue(ApplyLValue.Callee(fn, nme.update), arguments) + PartialAssignment(lvalue) { (l, r) => l.formAssignment(r) } else // Type arguments are present; the LHS requires a type application. val s: untpd.Tree = untpd.Select(untpd.TypedSplice(fn), nme.update) val t = untpd.TypeApply(s, tas.map((ta) => untpd.TypedSplice(ta))) - val v = ApplyLValue(temporary(typed(t)), as.map((a) => temporary(typed(a)))) - PartialAssignment(v) { (l, r) => l.formAssignment(r) } + val arguments = as.map(PossiblyHoistedValue.apply) + val lvalue = ApplyLValue(ApplyLValue.Callee(typed(t)), arguments) + PartialAssignment(lvalue) { (l, r) => l.formAssignment(r) } case _ => formPartialAssignmentToNonApply(lhs) - /** Returns a builder for computing trees representing assignments to `lhs`, which isn't a term - * or type application. + /** Returns a builder for making trees representing assignments to `lhs`, which isn't a term or + * type application. */ def formPartialAssignmentToNonApply(lhs: untpd.Tree)(using Context): PartialAssignment[LValue] = val locked = ctx.typerState.ownedVars @@ -1431,7 +1415,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer (untpd.cpy.Ident(f)(name.setterName), captures) case f @ Select(q, name: TermName) => - val (v, d) = hoisted(q) + val (v, d) = PossiblyHoistedValue(q).valueAndDefinition (untpd.cpy.Select(f)(untpd.TypedSplice(v), name.setterName), captures ++ d) case f @ TypeApply(g, ts) => @@ -1442,7 +1426,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer var (s, newCaptures) = formSetter(g, captures) var arguments = List[untpd.Tree]() for a <- as do - val (v, d) = hoisted(a) + val (v, d) = PossiblyHoistedValue(a).valueAndDefinition arguments = untpd.TypedSplice(v, isExtensionReceiver = true) +: arguments newCaptures = newCaptures ++ d From 2841e53e475a2a7d853777de645eaad2a3d7e580 Mon Sep 17 00:00:00 2001 From: Dimi Racordon Date: Thu, 25 Jul 2024 19:12:49 +0200 Subject: [PATCH 16/24] Remove needless filter --- .../src/dotty/tools/dotc/typer/Typer.scala | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index bb6fbbd5d4e4..7e68bf07a9fb 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1400,7 +1400,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer lhs: Tree )(using Context): Option[PartialAssignment[LValue]] = /** Returns the setter corresponding to `lhs`, which is a getter, along hoisted definitions. */ - def formSetter(lhs: Tree, captures: List[Tree]): (untpd.Tree, List[Tree]) = + def formSetter(lhs: Tree, locals: List[ValDef]): (untpd.Tree, List[ValDef]) = lhs match case f @ Ident(name: TermName) => // We need to make sure that the prefix of this extension getter is retained when we @@ -1408,27 +1408,27 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer // from another extension. See tests/pos/i18713.scala for an example. f.tpe match case TermRef(q: TermRef, _) => - formSetter(ref(q).select(f.symbol).withSpan(f.span), captures) + formSetter(ref(q).select(f.symbol).withSpan(f.span), locals) case TermRef(q: ThisType, _) => - formSetter(This(q.cls).select(f.symbol).withSpan(f.span), captures) + formSetter(This(q.cls).select(f.symbol).withSpan(f.span), locals) case TermRef(NoPrefix, _) => - (untpd.cpy.Ident(f)(name.setterName), captures) + (untpd.cpy.Ident(f)(name.setterName), locals) case f @ Select(q, name: TermName) => val (v, d) = PossiblyHoistedValue(q).valueAndDefinition - (untpd.cpy.Select(f)(untpd.TypedSplice(v), name.setterName), captures ++ d) + (untpd.cpy.Select(f)(untpd.TypedSplice(v), name.setterName), locals ++ d) case f @ TypeApply(g, ts) => - val (s, cs) = formSetter(g, captures) + val (s, cs) = formSetter(g, locals) (untpd.cpy.TypeApply(f)(s, ts.map((t) => untpd.TypedSplice(t))), cs) case f @ Apply(g, as) => - var (s, newCaptures) = formSetter(g, captures) + var (s, newLocals) = formSetter(g, locals) var arguments = List[untpd.Tree]() for a <- as do val (v, d) = PossiblyHoistedValue(a).valueAndDefinition arguments = untpd.TypedSplice(v, isExtensionReceiver = true) +: arguments - newCaptures = newCaptures ++ d + newLocals = newLocals ++ d val setter = untpd.cpy.Apply(f)(s, arguments) @@ -1436,17 +1436,17 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer case _: Apply => // Current apply is to implicit arguments. Note that we cannot copy the apply kind // of `f` since `f` is a typed tree and apply kinds are not preserved for those. - (setter.setApplyKind(ApplyKind.Using), newCaptures) + (setter.setApplyKind(ApplyKind.Using), newLocals) case _ => - (setter, newCaptures) + (setter, newLocals) case _ => (EmptyTree, List()) - val (setter, captures) = formSetter(lhs, List()) + val (setter, locals) = formSetter(lhs, List()) if setter.isEmpty then None else - val cs = captures.collect { case d: tpd.ValDef => d } - Some(PartialAssignment(UnappliedSetter(setter, cs)) { (l, r) => l.formAssignment(r) }) + val lvalue = ApplyLValue(ApplyLValue.Callee.Untyped(setter, locals), List()) + Some(PartialAssignment(lvalue) { (l, r) => l.formAssignment(r) }) def typedAssign(tree: untpd.Assign, pt: Type)(using Context): Tree = tree.lhs match From 3b83559fdea107ea1e1f3135d9bed3b81ea1fb7e Mon Sep 17 00:00:00 2001 From: Dimi Racordon Date: Thu, 25 Jul 2024 19:14:57 +0200 Subject: [PATCH 17/24] Refactor expression to work around an inference regression --- .../src/dotty/tools/dotc/reporting/UniqueMessagePositions.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/reporting/UniqueMessagePositions.scala b/compiler/src/dotty/tools/dotc/reporting/UniqueMessagePositions.scala index d8426aa8781e..37649e095ed6 100644 --- a/compiler/src/dotty/tools/dotc/reporting/UniqueMessagePositions.scala +++ b/compiler/src/dotty/tools/dotc/reporting/UniqueMessagePositions.scala @@ -33,6 +33,6 @@ trait UniqueMessagePositions extends Reporter { for offset <- dia.pos.start to dia.pos.end do positions.get((ctx.source, offset)) match case Some(dia1) if dia1.hides(dia) => - case _ => positions((ctx.source, offset)) = dia + case _ => positions((ctx.source, Integer.valueOf(offset).nn)) = dia super.markReported(dia) } From c2a37054d24943047aff774258d1c064ab00a35a Mon Sep 17 00:00:00 2001 From: Dimi Racordon Date: Thu, 25 Jul 2024 19:15:32 +0200 Subject: [PATCH 18/24] Update test expectations --- tests/init-global/warn/global-irrelevance2.check | 2 +- tests/neg/assignments.scala | 2 +- tests/neg/i11561.check | 12 +++++++++--- tests/neg/i11561.scala | 2 +- tests/neg/i16655.check | 13 ++++++++++--- tests/neg/i16655.scala | 2 +- tests/neg/i20338a.scala | 4 ++-- tests/neg/i20338c.check | 4 ++-- tests/neg/parser-stability-16.scala | 2 +- 9 files changed, 28 insertions(+), 15 deletions(-) diff --git a/tests/init-global/warn/global-irrelevance2.check b/tests/init-global/warn/global-irrelevance2.check index 156b34fa6aa5..ad3500143114 100644 --- a/tests/init-global/warn/global-irrelevance2.check +++ b/tests/init-global/warn/global-irrelevance2.check @@ -1,4 +1,4 @@ --- Warning: tests/init-global/warn/global-irrelevance2.scala:5:6 ------------------------------------------------------- +-- Warning: tests/init-global/warn/global-irrelevance2.scala:5:2 ------------------------------------------------------- 5 | A.x = b * 2 // warn | ^^^^^^^^^^^^ | Mutating object A during initialization of object B. diff --git a/tests/neg/assignments.scala b/tests/neg/assignments.scala index 273419cb50ba..f96e2f0cc963 100644 --- a/tests/neg/assignments.scala +++ b/tests/neg/assignments.scala @@ -13,7 +13,7 @@ object assignments { x = x + 1 x *= 2 - x_= = 2 // error should give missing arguments + x_= = 2 // error should give missing arguments // error } var c = new C diff --git a/tests/neg/i11561.check b/tests/neg/i11561.check index 28d7e355c499..7a3eb932bd38 100644 --- a/tests/neg/i11561.check +++ b/tests/neg/i11561.check @@ -1,5 +1,5 @@ -- [E081] Type Error: tests/neg/i11561.scala:2:32 ---------------------------------------------------------------------- -2 | val updateText1 = copy(text = _) // error +2 | val updateText1 = copy(text = _) // error // error | ^ | Missing parameter type | @@ -8,9 +8,15 @@ | _$1 => State.this.text = _$1 | Expected type for the whole anonymous function: | String --- [E052] Type Error: tests/neg/i11561.scala:3:30 ---------------------------------------------------------------------- +-- [E052] Type Error: tests/neg/i11561.scala:2:25 ---------------------------------------------------------------------- +2 | val updateText1 = copy(text = _) // error // error + | ^^^^ + | Reassignment to val text + | + | longer explanation available when compiling with `-explain` +-- [E052] Type Error: tests/neg/i11561.scala:3:25 ---------------------------------------------------------------------- 3 | val updateText2 = copy(text = (_: String)) // error - | ^^^^^^^^^^^^^^^^^^ + | ^^^^ | Reassignment to val text | | longer explanation available when compiling with `-explain` diff --git a/tests/neg/i11561.scala b/tests/neg/i11561.scala index c8b3280d2768..2493e41e4181 100644 --- a/tests/neg/i11561.scala +++ b/tests/neg/i11561.scala @@ -1,3 +1,3 @@ case class State(text: String): - val updateText1 = copy(text = _) // error + val updateText1 = copy(text = _) // error // error val updateText2 = copy(text = (_: String)) // error diff --git a/tests/neg/i16655.check b/tests/neg/i16655.check index e1335b624244..cd46488d3af4 100644 --- a/tests/neg/i16655.check +++ b/tests/neg/i16655.check @@ -1,6 +1,13 @@ --- [E052] Type Error: tests/neg/i16655.scala:3:4 ----------------------------------------------------------------------- -3 | x = 5 // error - | ^^^^^ +-- [E052] Type Error: tests/neg/i16655.scala:3:2 ----------------------------------------------------------------------- +3 | x = 5 // error // error + | ^ | Reassignment to val x | | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg/i16655.scala:3:6 -------------------------------------------------------------- +3 | x = 5 // error // error + | ^ + | Found: (5 : Int) + | Required: String + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg/i16655.scala b/tests/neg/i16655.scala index c758678d9896..882d3929e2b9 100644 --- a/tests/neg/i16655.scala +++ b/tests/neg/i16655.scala @@ -1,3 +1,3 @@ object Test: val x = "MyString" - x = 5 // error + x = 5 // error // error diff --git a/tests/neg/i20338a.scala b/tests/neg/i20338a.scala index b91982297d78..968955474495 100644 --- a/tests/neg/i20338a.scala +++ b/tests/neg/i20338a.scala @@ -1,10 +1,10 @@ object types: opaque type Struct = Int val test: Struct = 25 - extension (s: Struct) + extension (s: Struct) def field: Int = s def field_=(other: Int) = () -@main def hello = +@main def hello = import types.* test.field = "hello" // error \ No newline at end of file diff --git a/tests/neg/i20338c.check b/tests/neg/i20338c.check index 1d19ec0b3042..261e94f1151a 100644 --- a/tests/neg/i20338c.check +++ b/tests/neg/i20338c.check @@ -1,6 +1,6 @@ --- [E052] Type Error: tests/neg/i20338c.scala:9:6 ---------------------------------------------------------------------- +-- [E052] Type Error: tests/neg/i20338c.scala:9:4 ---------------------------------------------------------------------- 9 | f.x = 42 // error - | ^^^^^^^^ + | ^^^ | Reassignment to val x | | longer explanation available when compiling with `-explain` diff --git a/tests/neg/parser-stability-16.scala b/tests/neg/parser-stability-16.scala index 25fb38374c45..a0b4e2eb474e 100644 --- a/tests/neg/parser-stability-16.scala +++ b/tests/neg/parser-stability-16.scala @@ -2,4 +2,4 @@ class x0[x0] { val x1 : x0 } trait x3 extends x0 { // error -x1 = 0 object // error // error +x1 = 0 object // error // error // error From 6cf4e153e777c34d2187941d8f69ab0aaf61a4d7 Mon Sep 17 00:00:00 2001 From: Dimi Racordon Date: Fri, 26 Jul 2024 10:47:04 +0200 Subject: [PATCH 19/24] Remember the span of hoisted values --- .../tools/dotc/typer/PartialAssignment.scala | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/PartialAssignment.scala b/compiler/src/dotty/tools/dotc/typer/PartialAssignment.scala index 98fb7ee933a9..0aebfadf5403 100644 --- a/compiler/src/dotty/tools/dotc/typer/PartialAssignment.scala +++ b/compiler/src/dotty/tools/dotc/typer/PartialAssignment.scala @@ -34,32 +34,34 @@ end PartialAssignment * Use this type to represent a part of a lvalue that must be evaluated before the lvalue gets * used for updating a value. */ -private[typer] opaque type PossiblyHoistedValue = tpd.Tree - -extension (self: PossiblyHoistedValue) +private[typer] final class PossiblyHoistedValue private (representation: tpd.Tree): /** Returns a tree representing the value of `self`. */ def value(using Context): tpd.Tree = - self.definition.map((d) => tpd.Ident(d.namedType)).getOrElse(self) + definition match + case Some(d) => tpd.Ident(d.namedType).withSpan(representation.span) + case _ => representation /** Returns the synthetic val defining `self` if it is hoisted. */ def definition: Option[tpd.ValDef] = - self match + representation match case d: tpd.ValDef => Some(d) case _ => None /** Returns a tree representing the value of `self` along with its hoisted definition, if any. */ def valueAndDefinition(using Context): (tpd.Tree, Option[tpd.ValDef]) = - self.definition + definition .map((d) => (tpd.Ident(d.namedType), Some(d))) - .getOrElse((self, None)) + .getOrElse((representation, None)) object PossiblyHoistedValue: /** Creates a value representing the `e`'s evaluation. */ def apply(e: tpd.Tree)(using Context): PossiblyHoistedValue = - if tpd.exprPurity(e) >= TreeInfo.Pure then e else - tpd.SyntheticValDef(TempResultName.fresh(), e) + if tpd.exprPurity(e) >= TreeInfo.Pure then + new PossiblyHoistedValue(e) + else + new PossiblyHoistedValue(tpd.SyntheticValDef(TempResultName.fresh(), e)) /** The left-hand side of an assignment. */ private[typer] sealed abstract class LValue: @@ -101,7 +103,7 @@ private[typer] final case class ApplyLValue( ) extends LValue: val locals: List[tpd.ValDef] = - function.locals ++ (arguments.collect { case d: tpd.ValDef => d }) + function.locals ++ (arguments.flatMap { (v) => v.definition }) def formAssignment(rhs: untpd.Tree)(using Context): untpd.Tree = val s = function.expanded From e16dcdec8f38cbd09bbc2722042384e43ef516a9 Mon Sep 17 00:00:00 2001 From: Dimi Racordon Date: Fri, 26 Jul 2024 13:24:10 +0200 Subject: [PATCH 20/24] Fix the hoisting of multiple-assignment targets --- .../tools/dotc/typer/PartialAssignment.scala | 20 +++-- .../src/dotty/tools/dotc/typer/Typer.scala | 86 +++++++++++++------ 2 files changed, 72 insertions(+), 34 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/PartialAssignment.scala b/compiler/src/dotty/tools/dotc/typer/PartialAssignment.scala index 0aebfadf5403..cc586299be69 100644 --- a/compiler/src/dotty/tools/dotc/typer/PartialAssignment.scala +++ b/compiler/src/dotty/tools/dotc/typer/PartialAssignment.scala @@ -57,8 +57,8 @@ private[typer] final class PossiblyHoistedValue private (representation: tpd.Tre object PossiblyHoistedValue: /** Creates a value representing the `e`'s evaluation. */ - def apply(e: tpd.Tree)(using Context): PossiblyHoistedValue = - if tpd.exprPurity(e) >= TreeInfo.Pure then + def apply(e: tpd.Tree, isSingleAssignment: Boolean)(using Context): PossiblyHoistedValue = + if isSingleAssignment || (tpd.exprPurity(e) >= TreeInfo.Pure) then new PossiblyHoistedValue(e) else new PossiblyHoistedValue(tpd.SyntheticValDef(TempResultName.fresh(), e)) @@ -122,11 +122,17 @@ object ApplyLValue: object Callee: - def apply(receiver: tpd.Tree)(using Context): Typed = - Typed(PossiblyHoistedValue(receiver), None) - - def apply(receiver: tpd.Tree, member: Name)(using Context): Typed = - Typed(PossiblyHoistedValue(receiver), Some(member)) + /** Creates an instance from a function represented as a typed tree. */ + def apply( + receiver: tpd.Tree, isSingleAssignment: Boolean + )(using Context): Typed = + Typed(PossiblyHoistedValue(receiver, isSingleAssignment), None) + + /** Creates an instance denoting a selection on a receiver represented as a typed tree. */ + def apply( + receiver: tpd.Tree, member: Name, isSingleAssignment: Boolean + )(using Context): Typed = + Typed(PossiblyHoistedValue(receiver, isSingleAssignment), Some(member)) /** A function representing a lvalue. */ final case class Typed(receiver: PossiblyHoistedValue, member: Option[Name]) extends Callee: diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 7e68bf07a9fb..21722f16d0a6 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1298,39 +1298,47 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer } /** Returns a builder for making trees representing assignments to `lhs`. */ - def formPartialAssignmentTo(lhs: untpd.Tree)(using Context): PartialAssignment[LValue] = + def formPartialAssignmentTo( + lhs: untpd.Tree, isSingleAssignment: Boolean + )(using Context): PartialAssignment[LValue] = lhs match case lhs @ Apply(f, as) => // LHS is an application `f(a1, ..., an)` that desugars to `f.update(a1, ..., an, rhs)`. - val arguments = as.map((a) => PossiblyHoistedValue(typed(a))) - val lvalue = ApplyLValue(ApplyLValue.Callee(typed(f), nme.update), arguments) + val arguments = as.map((a) => PossiblyHoistedValue(typed(a), isSingleAssignment)) + val callee = ApplyLValue.Callee(typed(f), nme.update, isSingleAssignment) + val lvalue = ApplyLValue(callee, arguments) PartialAssignment(lvalue) { (l, r) => l.formAssignment(r) } case untpd.TypedSplice(Apply(MaybePoly(Select(fn, app), tas), as)) if app == nme.apply => if tas.isEmpty then // No type arguments: fall back to a regular update. - val arguments = as.map(PossiblyHoistedValue.apply) - val lvalue = ApplyLValue(ApplyLValue.Callee(fn, nme.update), arguments) + val arguments = as.map((a) => PossiblyHoistedValue(a, isSingleAssignment)) + val callee = ApplyLValue.Callee(fn, nme.update, isSingleAssignment) + val lvalue = ApplyLValue(callee, arguments) PartialAssignment(lvalue) { (l, r) => l.formAssignment(r) } else // Type arguments are present; the LHS requires a type application. val s: untpd.Tree = untpd.Select(untpd.TypedSplice(fn), nme.update) val t = untpd.TypeApply(s, tas.map((ta) => untpd.TypedSplice(ta))) - val arguments = as.map(PossiblyHoistedValue.apply) - val lvalue = ApplyLValue(ApplyLValue.Callee(typed(t)), arguments) + val arguments = as.map((a) => PossiblyHoistedValue(a, isSingleAssignment)) + val callee = ApplyLValue.Callee(typed(t), isSingleAssignment) + val lvalue = ApplyLValue(callee, arguments) PartialAssignment(lvalue) { (l, r) => l.formAssignment(r) } case _ => - formPartialAssignmentToNonApply(lhs) + formPartialAssignmentToNonApply(lhs, isSingleAssignment) /** Returns a builder for making trees representing assignments to `lhs`, which isn't a term or * type application. */ - def formPartialAssignmentToNonApply(lhs: untpd.Tree)(using Context): PartialAssignment[LValue] = + def formPartialAssignmentToNonApply( + lhs: untpd.Tree, isSingleAssignment: Boolean + )(using Context): PartialAssignment[LValue] = val locked = ctx.typerState.ownedVars val core = typedUnadapted(lhs, LhsProto, locked) def adapted = adapt(core, LhsProto, locked) + /** Returns a builder reporting that the left-hand side is not reassignable. */ def reassignmentToVal(): PartialAssignment[SimpleLValue] = PartialAssignment(SimpleLValue(core)) { (l, r) => val target = l.expression @@ -1357,10 +1365,11 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer core match case Apply(f, _) if f.symbol.is(ExtensionMethod) => - formPartialAssignmentToExtensionApply(core).getOrElse(reassignmentToVal()) + formPartialAssignmentToExtensionApply(core, isSingleAssignment) + .getOrElse(reassignmentToVal()) case _ => core.tpe match - case r: TermRef => + case r: TermRef if !mustFormSetter(adapted, isSingleAssignment) => val v = core.denot.suchThat(!_.is(Method)) if canAssign(v.symbol) then rememberNonLocalAssignToPrivate(v.symbol) @@ -1387,6 +1396,11 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer case _ => reassignmentToVal() + case r: TermRef => + val (setter, locals) = formSetter(adapted, isExtensionReceiver=false, isSingleAssignment) + val lvalue = ApplyLValue(ApplyLValue.Callee.Untyped(setter, locals), List()) + PartialAssignment(lvalue) { (l, r) => l.formAssignment(r) } + case TryDynamicCallType => formPartialDynamicAssignment(lhs) @@ -1397,10 +1411,18 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer * defined in an extension. */ def formPartialAssignmentToExtensionApply( - lhs: Tree + lhs: Tree, isSingleAssignment: Boolean )(using Context): Option[PartialAssignment[LValue]] = - /** Returns the setter corresponding to `lhs`, which is a getter, along hoisted definitions. */ - def formSetter(lhs: Tree, locals: List[ValDef]): (untpd.Tree, List[ValDef]) = + val (setter, locals) = formSetter(lhs, isExtensionReceiver=true, isSingleAssignment) + if setter.isEmpty then None else + val lvalue = ApplyLValue(ApplyLValue.Callee.Untyped(setter, locals), List()) + Some(PartialAssignment(lvalue) { (l, r) => l.formAssignment(r) }) + + /** Returns the setter corresponding to `lhs`, which is a getter, along hoisted definitions. */ + def formSetter( + lhs: Tree, isExtensionReceiver: Boolean, isSingleAssignment: Boolean + )(using Context): (untpd.Tree, List[ValDef]) = + def recurse(lhs: Tree, locals: List[ValDef]): (untpd.Tree, List[ValDef]) = lhs match case f @ Ident(name: TermName) => // We need to make sure that the prefix of this extension getter is retained when we @@ -1408,26 +1430,26 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer // from another extension. See tests/pos/i18713.scala for an example. f.tpe match case TermRef(q: TermRef, _) => - formSetter(ref(q).select(f.symbol).withSpan(f.span), locals) + recurse(ref(q).select(f.symbol).withSpan(f.span), locals) case TermRef(q: ThisType, _) => - formSetter(This(q.cls).select(f.symbol).withSpan(f.span), locals) + recurse(This(q.cls).select(f.symbol).withSpan(f.span), locals) case TermRef(NoPrefix, _) => (untpd.cpy.Ident(f)(name.setterName), locals) case f @ Select(q, name: TermName) => - val (v, d) = PossiblyHoistedValue(q).valueAndDefinition + val (v, d) = PossiblyHoistedValue(q, isSingleAssignment).valueAndDefinition (untpd.cpy.Select(f)(untpd.TypedSplice(v), name.setterName), locals ++ d) case f @ TypeApply(g, ts) => - val (s, cs) = formSetter(g, locals) + val (s, cs) = recurse(g, locals) (untpd.cpy.TypeApply(f)(s, ts.map((t) => untpd.TypedSplice(t))), cs) case f @ Apply(g, as) => - var (s, newLocals) = formSetter(g, locals) + var (s, newLocals) = recurse(g, locals) var arguments = List[untpd.Tree]() for a <- as do - val (v, d) = PossiblyHoistedValue(a).valueAndDefinition - arguments = untpd.TypedSplice(v, isExtensionReceiver = true) +: arguments + val (v, d) = PossiblyHoistedValue(a, isSingleAssignment).valueAndDefinition + arguments = untpd.TypedSplice(v, isExtensionReceiver) +: arguments newLocals = newLocals ++ d val setter = untpd.cpy.Apply(f)(s, arguments) @@ -1443,10 +1465,20 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer case _ => (EmptyTree, List()) - val (setter, locals) = formSetter(lhs, List()) - if setter.isEmpty then None else - val lvalue = ApplyLValue(ApplyLValue.Callee.Untyped(setter, locals), List()) - Some(PartialAssignment(lvalue) { (l, r) => l.formAssignment(r) }) + recurse(lhs, List()) + + /** Returns whether `t` should be desugared as a setter to form a partial assignment. */ + def mustFormSetter(t: tpd.Tree, isSingleAssignment: Boolean)(using Context) = + !isSingleAssignment && ( + t match + case f @ Ident(_) => f.tpe match + case TermRef(NoPrefix, _) => false + case _ => true + case f @ Select(q, _) => + !(exprPurity(q) >= TreeInfo.Pure) + case _ => + true + ) def typedAssign(tree: untpd.Assign, pt: Type)(using Context): Tree = tree.lhs match @@ -1455,7 +1487,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer typedMultipleAssign(lhs, tree.rhs) case _ => // Simple assignment. - val assignmentBuilder = formPartialAssignmentTo(tree.lhs) + val assignmentBuilder = formPartialAssignmentTo(tree.lhs, isSingleAssignment=true) val locals = assignmentBuilder.lhs.locals.map((d) => untpd.TypedSplice(d)) if locals.isEmpty then typed(assignmentBuilder(tree.rhs)) @@ -1481,7 +1513,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val s = PartialAssignment(SimpleLValue(e)) { (l, _) => l.expression } assignmentBuilders.append(s) case _ => - val s = formPartialAssignmentTo(l) + val s = formPartialAssignmentTo(l, false) statements.appendAll(s.lhs.locals.map((d) => untpd.TypedSplice(d))) assignmentBuilders.append(s) From 833d329d37ea4745c4ab8328e1cfcdcdf4ca3117 Mon Sep 17 00:00:00 2001 From: Dimi Racordon Date: Fri, 26 Jul 2024 14:21:39 +0200 Subject: [PATCH 21/24] Add missing spans --- .../src/dotty/tools/dotc/typer/PartialAssignment.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/PartialAssignment.scala b/compiler/src/dotty/tools/dotc/typer/PartialAssignment.scala index cc586299be69..92acb7076107 100644 --- a/compiler/src/dotty/tools/dotc/typer/PartialAssignment.scala +++ b/compiler/src/dotty/tools/dotc/typer/PartialAssignment.scala @@ -50,9 +50,9 @@ private[typer] final class PossiblyHoistedValue private (representation: tpd.Tre /** Returns a tree representing the value of `self` along with its hoisted definition, if any. */ def valueAndDefinition(using Context): (tpd.Tree, Option[tpd.ValDef]) = - definition - .map((d) => (tpd.Ident(d.namedType), Some(d))) - .getOrElse((representation, None)) + definition match + case Some(d) => (tpd.Ident(d.namedType).withSpan(representation.span), Some(d)) + case _ => (representation, None) object PossiblyHoistedValue: @@ -61,7 +61,7 @@ object PossiblyHoistedValue: if isSingleAssignment || (tpd.exprPurity(e) >= TreeInfo.Pure) then new PossiblyHoistedValue(e) else - new PossiblyHoistedValue(tpd.SyntheticValDef(TempResultName.fresh(), e)) + new PossiblyHoistedValue(tpd.SyntheticValDef(TempResultName.fresh(), e).withSpan(e.span)) /** The left-hand side of an assignment. */ private[typer] sealed abstract class LValue: From 65c428b6e66495837de2df787064445478763d8b Mon Sep 17 00:00:00 2001 From: Dimi Racordon Date: Fri, 26 Jul 2024 14:21:50 +0200 Subject: [PATCH 22/24] Add missing comment --- compiler/src/dotty/tools/dotc/typer/PartialAssignment.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/compiler/src/dotty/tools/dotc/typer/PartialAssignment.scala b/compiler/src/dotty/tools/dotc/typer/PartialAssignment.scala index 92acb7076107..7c2abdc4b71d 100644 --- a/compiler/src/dotty/tools/dotc/typer/PartialAssignment.scala +++ b/compiler/src/dotty/tools/dotc/typer/PartialAssignment.scala @@ -115,6 +115,7 @@ object ApplyLValue: /** The callee of a lvalue represented by a partial application. */ sealed abstract class Callee: + /** Returns the tree representing this callee. */ def expanded(using Context): untpd.Tree /** Returns the local `val` definitions composing this lvalue. */ From 5dab0f6a37a95d7b306f3af6a9174423eecc8d05 Mon Sep 17 00:00:00 2001 From: Dimi Racordon Date: Fri, 26 Jul 2024 14:22:13 +0200 Subject: [PATCH 23/24] Add evaluation tests for multiple assignments --- tests/run/multiple-assignment.check | 12 +++++++++ tests/run/multiple-assignment.scala | 38 +++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 tests/run/multiple-assignment.check create mode 100644 tests/run/multiple-assignment.scala diff --git a/tests/run/multiple-assignment.check b/tests/run/multiple-assignment.check new file mode 100644 index 000000000000..039f0ae2b071 --- /dev/null +++ b/tests/run/multiple-assignment.check @@ -0,0 +1,12 @@ +after swap: (2, 4) +after swap: (2, 4) +after swap: (2, 4) +load 1 +load 2 +load 2 +load 4 from 2 +load 1 +load 2 from 1 +store 4 to 1 +store 2 to 2 +after swap: (4, 2) diff --git a/tests/run/multiple-assignment.scala b/tests/run/multiple-assignment.scala new file mode 100644 index 000000000000..e2108812bba0 --- /dev/null +++ b/tests/run/multiple-assignment.scala @@ -0,0 +1,38 @@ +object Test: + + class Container[T](val id: Int, var x: T): + + def y: T = + println(s"load ${x} from ${id}") + x + + def y_=(newValue: T): Unit = + println(s"store ${newValue} to ${id}") + this.x_=(newValue) + + def main(args: Array[String]): Unit = + // simple swap + var x1 = 4 + var x2 = 2 + (x1, x2) = (x2, x1) + println(s"after swap: (${x1}, ${x2})") + + // swap in a container + val a = Array(4, 2) + (a(0), a(1)) = (a(1), a(0)) + println(s"after swap: (${a(0)}, ${a(1)})") + + // swap fields with effectless left-hand sides + var c1 = Container(1, 4) + var c2 = Container(2, 2) + (c1.x, c2.x) = (c2.x, c1.x) + println(s"after swap: (${c1.x}, ${c2.x})") + + // swap fields with side effectful left-hand sides + def f(n: Int): Container[Int] = + println(s"load ${n}") + n match + case 1 => c1 + case 2 => c2 + (f(1).y, f(2).y) = (f(2).y, f(1).y) + println(s"after swap: (${c1.x}, ${c2.x})") From 51e863f50056635f6a8c098cbb6ce3e670d1047a Mon Sep 17 00:00:00 2001 From: Dimi Racordon Date: Fri, 26 Jul 2024 18:15:20 +0200 Subject: [PATCH 24/24] Fix setters desugaring --- .../src/dotty/tools/dotc/typer/Typer.scala | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 21722f16d0a6..594f7811143c 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1369,7 +1369,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer .getOrElse(reassignmentToVal()) case _ => core.tpe match - case r: TermRef if !mustFormSetter(adapted, isSingleAssignment) => + case r: TermRef if isSingleAssignment || !mustFormSetter(adapted) => val v = core.denot.suchThat(!_.is(Method)) if canAssign(v.symbol) then rememberNonLocalAssignToPrivate(v.symbol) @@ -1468,17 +1468,15 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer recurse(lhs, List()) /** Returns whether `t` should be desugared as a setter to form a partial assignment. */ - def mustFormSetter(t: tpd.Tree, isSingleAssignment: Boolean)(using Context) = - !isSingleAssignment && ( - t match - case f @ Ident(_) => f.tpe match - case TermRef(NoPrefix, _) => false - case _ => true - case f @ Select(q, _) => - !(exprPurity(q) >= TreeInfo.Pure) - case _ => - true - ) + def mustFormSetter(t: tpd.Tree)(using Context) = + t match + case f @ Ident(_) => f.tpe match + case TermRef(NoPrefix, _) => false + case _ => true + case f @ Select(q, _) => + !(exprPurity(q) >= TreeInfo.Pure) + case _ => + true def typedAssign(tree: untpd.Assign, pt: Type)(using Context): Tree = tree.lhs match