diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 0df47f91c254..6d2a0b80f312 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -1195,10 +1195,10 @@ object desugar { arg match case Parens(arg) => Apply(sel, assignToNamedArg(arg) :: Nil) - case Tuple(Nil) => - Apply(sel, arg :: Nil).setApplyKind(ApplyKind.InfixUnit) - case Tuple(args) if args.nonEmpty => // this case should be dropped if auto-tupling is removed + case Tuple(args) if args.exists(_.isInstanceOf[Assign]) => Apply(sel, args.mapConserve(assignToNamedArg)) + case Tuple(args) => + Apply(sel, arg :: Nil).setApplyKind(ApplyKind.InfixTuple) case _ => Apply(sel, arg :: Nil) diff --git a/compiler/src/dotty/tools/dotc/ast/Trees.scala b/compiler/src/dotty/tools/dotc/ast/Trees.scala index 6f162bf5d973..8801f2a92159 100644 --- a/compiler/src/dotty/tools/dotc/ast/Trees.scala +++ b/compiler/src/dotty/tools/dotc/ast/Trees.scala @@ -448,7 +448,7 @@ object Trees { enum ApplyKind: case Regular // r.f(x) case Using // r.f(using x) - case InfixUnit // r f (), needs to be treated specially for an error message in typedApply + case InfixTuple // r f (x1, ..., xN) where N != 1; needs to be treated specially for an error message in typedApply /** fun(args) */ case class Apply[-T >: Untyped] private[ast] (fun: Tree[T], args: List[Tree[T]])(implicit @constructorOnly src: SourceFile) diff --git a/compiler/src/dotty/tools/dotc/transform/SymUtils.scala b/compiler/src/dotty/tools/dotc/transform/SymUtils.scala index bae0cdb4cceb..a3b55131865b 100644 --- a/compiler/src/dotty/tools/dotc/transform/SymUtils.scala +++ b/compiler/src/dotty/tools/dotc/transform/SymUtils.scala @@ -218,4 +218,12 @@ class SymUtils(val self: Symbol) extends AnyVal { def isScalaStatic(using Context): Boolean = self.hasAnnotation(ctx.definitions.ScalaStaticAnnot) + /** Is symbol assumed or declared as an infix symbol? */ + def isDeclaredInfix(using Context): Boolean = + self.hasAnnotation(defn.InfixAnnot) + || defn.isInfix(self) + || self.name.isUnapplyName + && self.owner.is(Module) + && self.owner.linkedClass.is(Case) + && self.owner.linkedClass.isDeclaredInfix } diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 788f0acd3e99..6cafff446e8e 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -615,7 +615,7 @@ trait Applications extends Compatibility { case arg :: args1 => val msg = arg match case untpd.Tuple(Nil) - if applyKind == ApplyKind.InfixUnit && funType.widen.isNullaryMethod => + if applyKind == ApplyKind.InfixTuple && funType.widen.isNullaryMethod => i"can't supply unit value with infix notation because nullary $methString takes no arguments; use dotted invocation instead: (...).${methRef.name}()" case _ => i"too many arguments for $methString" @@ -862,14 +862,15 @@ trait Applications extends Compatibility { record("typedApply") val fun1 = typedFunPart(tree.fun, originalProto) - // Warning: The following lines are dirty and fragile. We record that auto-tupling was demanded as - // a side effect in adapt. If it was, we assume the tupled proto-type in the rest of the application, + // Warning: The following lines are dirty and fragile. + // We record that auto-tupling or untupling was demanded as a side effect in adapt. + // If it was, we assume the tupled-dual proto-type in the rest of the application, // until, possibly, we have to fall back to insert an implicit on the qualifier. // This crucially relies on he fact that `proto` is used only in a single call of `adapt`, // otherwise we would get possible cross-talk between different `adapt` calls using the same // prototype. A cleaner alternative would be to return a modified prototype from `adapt` together with // a modified tree but this would be more convoluted and less efficient. - val proto = if (originalProto.isTupled) originalProto.tupled else originalProto + val proto = if (originalProto.hasTupledDual) originalProto.tupledDual else originalProto // If some of the application's arguments are function literals without explicitly declared // parameter types, relate the normalized result type of the application with the diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index ae424aee4319..835a57068c06 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -810,21 +810,13 @@ trait Checking { * operator is alphanumeric, it must be declared `@infix`. */ def checkValidInfix(tree: untpd.InfixOp, meth: Symbol)(using Context): Unit = { - - def isInfix(sym: Symbol): Boolean = - sym.hasAnnotation(defn.InfixAnnot) || - defn.isInfix(sym) || - sym.name.isUnapplyName && - sym.owner.is(Module) && sym.owner.linkedClass.is(Case) && - isInfix(sym.owner.linkedClass) - tree.op match { case id @ Ident(name: Name) => name.toTermName match { case name: SimpleName if !untpd.isBackquoted(id) && !name.isOperatorName && - !isInfix(meth) && + !meth.isDeclaredInfix && !meth.maybeOwner.is(Scala2x) && !infixOKSinceFollowedBy(tree.right) && sourceVersion.isAtLeast(`3.1`) => diff --git a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala index ca49908949b7..1aa1e7353724 100644 --- a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala +++ b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala @@ -236,8 +236,8 @@ object ProtoTypes { /** A map in which typed arguments can be stored to be later integrated in `typedArgs`. */ var typedArg: SimpleIdentityMap[untpd.Tree, Tree] = SimpleIdentityMap.Empty - /** The tupled version of this prototype, if it has been computed */ - var tupled: Type = NoType + /** The tupled or untupled version of this prototype, if it has been computed */ + var tupledDual: Type = NoType /** If true, the application of this prototype was canceled. */ var toDrop: Boolean = false @@ -348,16 +348,19 @@ object ProtoTypes { } /** The same proto-type but with all arguments combined in a single tuple */ - def tupled: FunProto = state.tupled match { + def tupledDual: FunProto = state.tupledDual match { case pt: FunProto => pt case _ => - state.tupled = new FunProto(untpd.Tuple(args) :: Nil, resultType)(typer, applyKind) - tupled + val dualArgs = args match + case untpd.Tuple(elems) :: Nil => elems + case _ => untpd.Tuple(args) :: Nil + state.tupledDual = new FunProto(dualArgs, resultType)(typer, applyKind) + tupledDual } - /** Somebody called the `tupled` method of this prototype */ - def isTupled: Boolean = state.tupled.isInstanceOf[FunProto] + /** Somebody called the `tupledDual` method of this prototype */ + def hasTupledDual: Boolean = state.tupledDual.isInstanceOf[FunProto] /** Cancel the application of this prototype. This can happen for a nullary * application `f()` if `f` refers to a symbol that exists both in parameterless diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index aabd00731e82..a2d6d9ac1f84 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -2930,14 +2930,31 @@ class Typer extends Namer false } + /** Should we tuple or untuple the argument before application? + * If auto-tupling is enabled then + * + * - we tuple n-ary arguments where n > 0 if the function consists + * only of unary alternatives + * - we untuple tuple arguments of infix operations if the function + * does not consist only of unary alternatives. + */ + def needsTupledDual(funType: Type, pt: FunProto): Boolean = + pt.args match + case untpd.Tuple(elems) :: Nil => + elems.length > 1 + && pt.applyKind == ApplyKind.InfixTuple + && !isUnary(funType) + case args => + args.lengthCompare(1) > 0 + && isUnary(funType) + && autoTuplingEnabled + def adaptToArgs(wtp: Type, pt: FunProto): Tree = wtp match { case wtp: MethodOrPoly => def methodStr = methPart(tree).symbol.showLocated if (matchingApply(wtp, pt)) - if (pt.args.lengthCompare(1) > 0 && isUnary(wtp) && autoTuplingEnabled) - adapt(tree, pt.tupled, locked) - else - tree + if needsTupledDual(wtp, pt) then adapt(tree, pt.tupledDual, locked) + else tree else if (wtp.isContextualMethod) def isContextBoundParams = wtp.stripPoly match case MethodType(EvidenceParamName(_) :: _) => true @@ -3465,8 +3482,8 @@ class Typer extends Namer case ref: TermRef => pt match { case pt: FunProto - if pt.args.lengthCompare(1) > 0 && isUnary(ref) && autoTuplingEnabled => - adapt(tree, pt.tupled, locked) + if needsTupledDual(ref, pt) && autoTuplingEnabled => + adapt(tree, pt.tupledDual, locked) case _ => adaptOverloaded(ref) }