From 5ec25bddc5e69aef8743ac7e21e81f0c278c93c5 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sun, 14 Jun 2020 14:54:17 +0200 Subject: [PATCH 1/2] Leave arguments of infix operations tupled. Leave arguments of infix operations tupled in desugaring. Untuple them in typer unless the function type is unary. This allows us to simply omit the operation in typer when auto tupling is disabled. --- .../src/dotty/tools/dotc/ast/Desugar.scala | 6 ++-- compiler/src/dotty/tools/dotc/ast/Trees.scala | 2 +- .../dotty/tools/dotc/transform/SymUtils.scala | 8 +++++ .../dotty/tools/dotc/typer/Applications.scala | 9 +++--- .../src/dotty/tools/dotc/typer/Checking.scala | 10 +------ .../dotty/tools/dotc/typer/ProtoTypes.scala | 17 ++++++----- .../src/dotty/tools/dotc/typer/Typer.scala | 29 +++++++++++++++---- 7 files changed, 51 insertions(+), 30 deletions(-) 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..17cd2dc3fc9b 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) } From 2acf7765e8a7347ef707650b6835775e1da5db49 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Thu, 11 Jun 2020 11:21:59 +0200 Subject: [PATCH 2/2] Change autoTuplingEnabled to only apply to tupling Untupling of infix operators is no longer under the noAutoTupling language feature. Instead, we always untuple if the infix operator is not unary. The idea is that, going forward we will gradually remove infix operators that are not unary. Once that is done completely, and there are no more backwards compatibility concerbs, the code to untuple arguments of infix operations will become dead code and can be removed. --- 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 17cd2dc3fc9b..a2d6d9ac1f84 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -2938,7 +2938,7 @@ class Typer extends Namer * - we untuple tuple arguments of infix operations if the function * does not consist only of unary alternatives. */ - def needsTupledDual(funType: Type, pt: FunProto): Boolean = { + def needsTupledDual(funType: Type, pt: FunProto): Boolean = pt.args match case untpd.Tuple(elems) :: Nil => elems.length > 1 @@ -2947,7 +2947,7 @@ class Typer extends Namer case args => args.lengthCompare(1) > 0 && isUnary(funType) - } && autoTuplingEnabled + && autoTuplingEnabled def adaptToArgs(wtp: Type, pt: FunProto): Tree = wtp match { case wtp: MethodOrPoly =>