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))) diff --git a/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala b/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala index e3613e3f783a..0ed627c983e4 100644 --- a/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala +++ b/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala @@ -214,6 +214,9 @@ 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 InvalidMultipleAssignmentTargetID // errorNumber: 202 + case MultipleAssignmentShapeMismatchID // errorNumber: 203 def errorNumber = ordinal - 1 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) } diff --git a/compiler/src/dotty/tools/dotc/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala index 1d906130d4e4..fdb694ea4da3 100644 --- a/compiler/src/dotty/tools/dotc/reporting/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala @@ -3288,3 +3288,28 @@ 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 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) = + i"""Source and target of multiple assignment have different sizes. + |Source: $found + |Target: $required""" + def explain(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..7c2abdc4b71d --- /dev/null +++ b/compiler/src/dotty/tools/dotc/typer/PartialAssignment.scala @@ -0,0 +1,156 @@ +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.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. + * + * @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` to `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 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] final class PossiblyHoistedValue private (representation: tpd.Tree): + + /** Returns a tree representing the value of `self`. */ + def value(using Context): tpd.Tree = + 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] = + 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]) = + definition match + case Some(d) => (tpd.Ident(d.namedType).withSpan(representation.span), Some(d)) + case _ => (representation, None) + +object PossiblyHoistedValue: + + /** Creates a value representing the `e`'s evaluation. */ + 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).withSpan(e.span)) + +/** 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 a tree computing the assignment of `rhs` to this lvalue. */ + def formAssignment(rhs: untpd.Tree)(using Context): untpd.Tree + +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 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. + */ +private[typer] final case class ApplyLValue( + function: ApplyLValue.Callee, + arguments: List[PossiblyHoistedValue] +) extends LValue: + + val locals: List[tpd.ValDef] = + function.locals ++ (arguments.flatMap { (v) => v.definition }) + + def formAssignment(rhs: untpd.Tree)(using Context): untpd.Tree = + val s = function.expanded + val t = arguments.map((a) => untpd.TypedSplice(a.value)) :+ rhs + untpd.Apply(s, t) + +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. */ + def locals: List[tpd.ValDef] + + object Callee: + + /** 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: + + 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 f2a7124f9fa4..594f7811143c 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1297,115 +1297,240 @@ 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.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 a builder for making trees representing assignments to `lhs`. */ + 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), 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((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((a) => PossiblyHoistedValue(a, isSingleAssignment)) + val callee = ApplyLValue.Callee(typed(t), isSingleAssignment) + val lvalue = ApplyLValue(callee, arguments) + PartialAssignment(lvalue) { (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, isSingleAssignment) - 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 making trees representing assignments to `lhs`, which isn't a term or + * type application. + */ + 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 + 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(core, isSingleAssignment) + .getOrElse(reassignmentToVal()) + + case _ => core.tpe match + case r: TermRef if isSingleAssignment || !mustFormSetter(adapted) => + 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) => + // QUESTION: Why do we need the `typedUnadapted(s, WildcardType, locked)`? + val s = untpd.Apply(untpd.TypedSplice(l.expression), List(r)) + untpd.TypedSplice(typedUnadapted(s, WildcardType, locked)) } - case TryDynamicCallType => - typedDynamicAssign(tree, pt) - case tpe => - reassignmentToVal - } - } + 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) + + case _ => + reassignmentToVal() + + /** Returns a builder for making trees representing assignments to `lhs`, which denotes a setter + * defined in an extension. + */ + def formPartialAssignmentToExtensionApply( + lhs: Tree, isSingleAssignment: Boolean + )(using Context): Option[PartialAssignment[LValue]] = + 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 + // 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, _) => + recurse(ref(q).select(f.symbol).withSpan(f.span), locals) + case TermRef(q: ThisType, _) => + 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, isSingleAssignment).valueAndDefinition + (untpd.cpy.Select(f)(untpd.TypedSplice(v), name.setterName), locals ++ d) + + case f @ TypeApply(g, ts) => + 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) = recurse(g, locals) + var arguments = List[untpd.Tree]() + for a <- as do + val (v, d) = PossiblyHoistedValue(a, isSingleAssignment).valueAndDefinition + arguments = untpd.TypedSplice(v, isExtensionReceiver) +: arguments + newLocals = newLocals ++ 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. + (setter.setApplyKind(ApplyKind.Using), newLocals) + case _ => + (setter, newLocals) + + case _ => + (EmptyTree, List()) + + recurse(lhs, List()) + + /** Returns whether `t` should be desugared as a setter to form a partial assignment. */ + 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 + case untpd.Tuple(lhs) => + // Multiple assignment. + typedMultipleAssign(lhs, tree.rhs) + case _ => + // Simple assignment. + 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)) + 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, false) + 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/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/multiple-assignment.check b/tests/neg/multiple-assignment.check new file mode 100644 index 000000000000..f06e5a4d7ea4 --- /dev/null +++ b/tests/neg/multiple-assignment.check @@ -0,0 +1,23 @@ +-- [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 +-- [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 new file mode 100644 index 000000000000..06fc731fe5e8 --- /dev/null +++ b/tests/neg/multiple-assignment.scala @@ -0,0 +1,12 @@ + +def tu(): (Int, Boolean) = (1, true) + +@main def ma(): Unit = + var x = 0 + var y = false + var z = "a" + + (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/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 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()) diff --git a/tests/pos/multiple-assignment.scala b/tests/pos/multiple-assignment.scala new file mode 100644 index 000000000000..960f0c25ad8a --- /dev/null +++ b/tests/pos/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 = 99 + (x, y) = tu() + (x, z) = (2, "b") 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})")