From 3c04b3758c972f97dc1347822bb2538d0e8cc94b Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Tue, 19 Feb 2019 14:06:50 +0100 Subject: [PATCH 01/13] Add alpha and infix annotations --- .../src/dotty/tools/dotc/core/Definitions.scala | 4 ++++ library/src/scala/annotation/alpha.scala | 3 +++ library/src/scala/annotation/infix.scala | 3 +++ tests/run/infix.scala | 15 +++++++++++++++ 4 files changed, 25 insertions(+) create mode 100644 library/src/scala/annotation/alpha.scala create mode 100644 library/src/scala/annotation/infix.scala create mode 100644 tests/run/infix.scala diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index e1e6a0162789..57cef6006113 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -883,6 +883,10 @@ class Definitions { def ShowAsInfixAnnot(implicit ctx: Context): ClassSymbol = ShowAsInfixAnotType.symbol.asClass lazy val FunctionalInterfaceAnnotType = ctx.requiredClassRef("java.lang.FunctionalInterface") def FunctionalInterfaceAnnot(implicit ctx: Context) = FunctionalInterfaceAnnotType.symbol.asClass + lazy val InfixAnnotType = ctx.requiredClassRef("scala.annotation.infix") + def InfixAnnot(implicit ctx: Context) = InfixAnnotType.symbol.asClass + lazy val AlphaAnnotType = ctx.requiredClassRef("scala.annotation.alpha") + def AlphaAnnot(implicit ctx: Context) = AlphaAnnotType.symbol.asClass // convenient one-parameter method types def methOfAny(tp: Type): MethodType = MethodType(List(AnyType), tp) diff --git a/library/src/scala/annotation/alpha.scala b/library/src/scala/annotation/alpha.scala new file mode 100644 index 000000000000..dd5318f7e14d --- /dev/null +++ b/library/src/scala/annotation/alpha.scala @@ -0,0 +1,3 @@ +package scala.annotation + +final class alpha(name: String) extends StaticAnnotation diff --git a/library/src/scala/annotation/infix.scala b/library/src/scala/annotation/infix.scala new file mode 100644 index 000000000000..df1b0ebde1df --- /dev/null +++ b/library/src/scala/annotation/infix.scala @@ -0,0 +1,3 @@ +package scala.annotation + +final class infix extends StaticAnnotation \ No newline at end of file diff --git a/tests/run/infix.scala b/tests/run/infix.scala new file mode 100644 index 000000000000..d8d47ac51609 --- /dev/null +++ b/tests/run/infix.scala @@ -0,0 +1,15 @@ +import annotation.{infix, alpha} +object Test extends App { + + case class Rational(n: Int, d: Int) { + @infix def + (that: Rational) = + Rational(this.n * that.d + that.n * this.d, this.d * that.d) + @infix @alpha("multiply") def * (that: Rational) = + Rational(this.n * that.n, this.d * that.d) + } + + val r1 = Rational(1,2) + val r2 = Rational(2,3) + println(r1 * r2) + println(r1 + r2) +} \ No newline at end of file From 7805293144e03c126183a1bed44d0fbe9d3d27b3 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sat, 23 Feb 2019 11:38:30 +0100 Subject: [PATCH 02/13] Add doc page --- .../reference/changed-features/operators.md | 118 ++++++++++++++++++ docs/sidebar.yml | 2 + library/src/scala/annotation/alpha.scala | 8 +- library/src/scala/annotation/infix.scala | 4 + 4 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 docs/docs/reference/changed-features/operators.md diff --git a/docs/docs/reference/changed-features/operators.md b/docs/docs/reference/changed-features/operators.md new file mode 100644 index 000000000000..ccb6c9b275b6 --- /dev/null +++ b/docs/docs/reference/changed-features/operators.md @@ -0,0 +1,118 @@ +--- +layout: doc-page +title: Rules for Operators +--- + +There are two annotations that regulate operators: `infix` and `alpha`. + +## The @alpha Annotation + +An `@alpha` annotation on a method definition defines an alternate name for the implementation of that method: Example: +```scala +object VecOps { + @alpha("append") def (xs: Vec[T]) ++= [T] (ys: Vec[T]): Vec[T] = ... +} +``` +Here, the `++=` operation is implemented (in Byte code or native code) under the name `append`. The implementation name affects the code that is generated, and is the name under which code from other languages can call the method. For instance, `++=` could be invoked from Java like this: +``` +VecOps.append(vec1, vec2) +``` +The `@alpha` annotation has no bearing on Scala usages. Any application of that method in Scala has to use `++=`, not `append`. + +An `@alpha` annotation will be _mandatory_ if the method name is symbolic. Symbolic methods without `@alpha` annotations are deprecated. + +### Motivation + +The `@alpha` annotation serves a dual purpose: + + - It helps interoperability between Scala and other languages. + - It serves as a documentation tool by providing an alternative regular name + as an alias of a symbolic operator. + +### Details + + 1. `@alpha` is defined in package `scala.annotation`. It takes a single argument + of type `String`. That string is called the _external name_ of the definition + that's annotated. + + 2. An `@alpha` annotation can be given for all kinds of definitions. + + 3. The name given in an `@alpha` annotation must be a legal name + for the defined entities on the host platform. + + 4. Definitions with symbolic names should have an `@alpha` annotation. Lack of such + an annotation will raise a deprecation warning. + + 5. Definitions with names in backticks that are not legal host platform names + should have an `@alpha` annotation. Lack of such an annotation will raise a deprecation warning. + + 6. @alpha annotations must agree: If two definitions are members of an object or class with the same name and matching types, then either none of them has an `@alpha` annotation, or both have `@alpha` annotations with the same name. + + 7. There must be a one-to-one relationship between external and internal names: + If two definitions are members of an object or class with matching types and both have `@alpha` annotations with the same external name, then their internal method names must also be the same. + +## The @infix Annotation + +An `@infix` annotation on a method definition allows using the method as an infix operation. Example: +```scala +trait MultiSet[T] { + + @infix + def union(other: MultiSet[T]): MultiSet[T] + + def difference(other: MultiSet[T]): MultiSet[T] + + @alpha("intersection") + def *(other: MultiSet[T]): MultiSet[T] +} + +val s1, s2: MultiSet[Int] + +s1 union s2 // OK +s1.union(s2) // also OK + +s1.difference(s2) // OK +s1 `difference` s2 // OK +s1 difference s2 // gives a deprecation warning + +s1 * s2 // OK +s1.*(s2) // also OK, but unusual +``` +Infix operations involving alphanumeric operators that do not carry @infix annotations are deprecated. Infix operations involving symbolic operators are always allowed, so `@infix` is redundant for methods with symbolic names. Infix operations are also allowed +if an alphanumeric operator name is given in backticks (as in the third call of `difference` above). + +The @infix annotation can also be given to a type: +``` +@infix type or[X, Y] +val x: String or Int = ... +``` + +### Motivation + +The purpose of the `@infix` annotation is to achieve consistency across a code base in how a method or type is applied. The idea is that the author of a method decides whether that method should be applied as an infix operator or in a regular application. Use sites then implement that decision consistently. + +### Details + + 1. `@infix` is defined in package `scala.annotation`. + + 2. If a method overrides another, their infix annotations must agree. Either both are annotated with `@infix`, or none of them are. + + 3. `@infix` annotations can be given to method definitions. The first non-receiver parameter list of an `@infix` method must define exactly one parameter. Examples: + + ```scala + @infix def op(x: S): R // ok + @infix def op[T](x: T)(y: S): R // ok + @infix def op[T](x: T, y: S): R // error: two parameters + + @infix def (x: A) op (y: B): R // ok + @infix def (x: A) op (y1: B, y2: B): R // error: two parameters + ``` + + 4. @infix annotations can also be given to type, trait or class definitions that have exactly two type parameters. An infix type like + + ```scala + @infix type op[X, Y] + ``` + + can be applied using infix syntax, i.e. `A op B`. + diff --git a/docs/sidebar.yml b/docs/sidebar.yml index 9d6b4a33df06..a25063ae4746 100644 --- a/docs/sidebar.yml +++ b/docs/sidebar.yml @@ -95,6 +95,8 @@ sidebar: url: docs/reference/changed-features/lazy-vals.html - title: Structural Types url: docs/reference/changed-features/structural-types.html + - title: Operators + url: docs/reference/changed-features/operators.html - title: Type Checking url: docs/reference/changed-features/type-checking.html - title: Type Inference diff --git a/library/src/scala/annotation/alpha.scala b/library/src/scala/annotation/alpha.scala index dd5318f7e14d..4d3e9f357c4d 100644 --- a/library/src/scala/annotation/alpha.scala +++ b/library/src/scala/annotation/alpha.scala @@ -1,3 +1,9 @@ package scala.annotation -final class alpha(name: String) extends StaticAnnotation +/** An annotation that defines an external name for a definition. + * If an `alpha(extname)` annotation is given for a method or some other + * definition, its implementation will use the name `extname` instead of + * the regular name. An `alpha` annotation is mandatory for definitions + * with symbolic names. + */ +final class alpha(externalName: String) extends StaticAnnotation diff --git a/library/src/scala/annotation/infix.scala b/library/src/scala/annotation/infix.scala index df1b0ebde1df..3936af243d56 100644 --- a/library/src/scala/annotation/infix.scala +++ b/library/src/scala/annotation/infix.scala @@ -1,3 +1,7 @@ package scala.annotation +/** A method annotation that suggests that the annotated method should + * be used as an infix operator. Infix operations with alphanumeric + * operator names require the operator to be annotated with `@infix`. + */ final class infix extends StaticAnnotation \ No newline at end of file From c880510211cc32932bf7fe8fa0b10a5b9392825a Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sat, 23 Feb 2019 18:02:01 +0100 Subject: [PATCH 03/13] Check validity of infix operations --- .../src/dotty/tools/dotc/typer/Checking.scala | 31 ++++++++++++++++--- .../src/dotty/tools/dotc/typer/Typer.scala | 1 + 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index b07df0230df7..521184676af7 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -20,19 +20,23 @@ import rewrites.Rewrites.patch import util.Spans.Span import util.SourcePosition +import util.Spans.Span +import rewrites.Rewrites.patch import transform.SymUtils._ +import transform.ValueClasses._ import Decorators._ import ErrorReporting.{err, errorType} import config.Printers.{typr, patmatch} import NameKinds.DefaultGetterName +import SymDenotations.{NoCompleter, NoDenotation} import Applications.unapplyArgs import transform.patmat.SpaceEngine.isIrrefutableUnapply + import collection.mutable -import SymDenotations.{NoCompleter, NoDenotation} -import dotty.tools.dotc.reporting.diagnostic.Message -import dotty.tools.dotc.reporting.diagnostic.messages._ -import dotty.tools.dotc.transform.ValueClasses._ +import reporting.diagnostic.Message +import reporting.diagnostic.messages._ +import scala.tasty.util.Chars.isOperatorPart object Checking { import tpd._ @@ -716,6 +720,24 @@ trait Checking { i"Use of implicit conversion ${conv.showLocated}", NoSymbol, posd.sourcePos) } + /** Check that `tree` is a valid infix operation. That is, if the + * operator is alphanumeric, it must be declared `@infix`. + */ + def checkValidInfix(tree: untpd.InfixOp, app: Tree)(implicit ctx: Context): Unit = + tree.op match { + case Ident(name: SimpleName) + if !name.exists(isOperatorPart) && !app.symbol.hasAnnotation(defn.InfixAnnot) && false => + ctx.deprecationWarning( + i"""alphanumeric method $name is not declared @infix; should not be used as infix operator. + |The operation can be rewritten automatically under -migration -rewrite""", + tree.op.sourcePos) + if (ctx.scala2Mode) { + patch(Span(tree.op.span.start, tree.op.span.start), "`") + patch(Span(tree.op.span.end, tree.op.span.end), "`") + } + case _ => + } + /** Issue a feature warning if feature is not enabled */ def checkFeature(name: TermName, description: => String, @@ -1099,5 +1121,6 @@ trait NoChecking extends ReChecking { override def checkNoForwardDependencies(vparams: List[ValDef])(implicit ctx: Context): Unit = () override def checkMembersOK(tp: Type, pos: SourcePosition)(implicit ctx: Context): Type = tp override def checkInInlineContext(what: String, posd: Positioned)(implicit ctx: Context): Unit = () + override def checkValidInfix(tree: untpd.InfixOp, app: Tree)(implicit ctx: Context): Unit = () override def checkFeature(name: TermName, description: => String, featureUseSite: Symbol, pos: SourcePosition)(implicit ctx: Context): Unit = () } diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 5b7d758da246..5c52fa74fa44 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1886,6 +1886,7 @@ class Typer extends Namer def typedInfixOp(tree: untpd.InfixOp, pt: Type)(implicit ctx: Context): Tree = { val untpd.InfixOp(l, op, r) = tree val app = typedApply(desugar.binop(l, op, r), pt) + checkValidInfix(tree, app) if (untpd.isLeftAssoc(op.name)) app else { val defs = new mutable.ListBuffer[Tree] From 06310b33a9be677653246a14a231685c3999e4d5 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sun, 24 Feb 2019 19:20:00 +0100 Subject: [PATCH 04/13] Relax infix criterion - Only checked under -strict - Not checked for symbols coming from Scala-2 - Infix alphanumeric is OK when followed by `{` --- .../src/dotty/tools/dotc/typer/Checking.scala | 17 ++++++++++++++--- .../reference/changed-features/operators.md | 15 +++++++++++++-- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 521184676af7..4ef80659c33a 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -720,18 +720,29 @@ trait Checking { i"Use of implicit conversion ${conv.showLocated}", NoSymbol, posd.sourcePos) } + private def infixOKSinceFollowedBy(tree: untpd.Tree): Boolean = tree match { + case _: untpd.Block | _: untpd.Match => true + case _ => false + } + /** Check that `tree` is a valid infix operation. That is, if the * operator is alphanumeric, it must be declared `@infix`. */ def checkValidInfix(tree: untpd.InfixOp, app: Tree)(implicit ctx: Context): Unit = tree.op match { + case _: untpd.BackquotedIdent => + () case Ident(name: SimpleName) - if !name.exists(isOperatorPart) && !app.symbol.hasAnnotation(defn.InfixAnnot) && false => + if !name.exists(isOperatorPart) && + !app.symbol.hasAnnotation(defn.InfixAnnot) && + !app.symbol.maybeOwner.is(Scala2x) && + !infixOKSinceFollowedBy(tree.right) && + ctx.settings.strict.value => ctx.deprecationWarning( i"""alphanumeric method $name is not declared @infix; should not be used as infix operator. - |The operation can be rewritten automatically under -migration -rewrite""", + |The operation can be rewritten automatically to `$name` under -deprecation -rewrite""", tree.op.sourcePos) - if (ctx.scala2Mode) { + if (ctx.settings.deprecation.value) { patch(Span(tree.op.span.start, tree.op.span.start), "`") patch(Span(tree.op.span.end, tree.op.span.end), "`") } diff --git a/docs/docs/reference/changed-features/operators.md b/docs/docs/reference/changed-features/operators.md index ccb6c9b275b6..0a618e0426a3 100644 --- a/docs/docs/reference/changed-features/operators.md +++ b/docs/docs/reference/changed-features/operators.md @@ -78,8 +78,17 @@ s1 difference s2 // gives a deprecation warning s1 * s2 // OK s1.*(s2) // also OK, but unusual ``` -Infix operations involving alphanumeric operators that do not carry @infix annotations are deprecated. Infix operations involving symbolic operators are always allowed, so `@infix` is redundant for methods with symbolic names. Infix operations are also allowed -if an alphanumeric operator name is given in backticks (as in the third call of `difference` above). +Infix operations involving alphanumeric operators are deprecated, unless +one of the following conditions holds: + + - the operator definition carries an `@infix` annotation, or + - the operator was compiled with Scala 2, or + - the operator is followed by an opening brace. + +An alphanumeric operator is an operator consisting entirely of letters, digits, the `$` and `_` characters, or +any unicode character `c` for which `java.lang.Character.isIdentifierPart(c)` returns `true`. + +Infix operations involving symbolic operators are always allowed, so `@infix` is redundant for methods with symbolic names. The @infix annotation can also be given to a type: ``` @@ -116,3 +125,5 @@ The purpose of the `@infix` annotation is to achieve consistency across a code b can be applied using infix syntax, i.e. `A op B`. + 5. To smooth migration to Scala 3.0, alphanumeric operations will only be deprecated from Scala 3.1 onwards, +or if the `-strict` option is given in Dotty/Scala 3. From bec117437095fcf3af974eab4c879e35bdcf99ea Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Mon, 18 Mar 2019 18:26:14 +0100 Subject: [PATCH 05/13] Make eq/ne infix operators --- compiler/src/dotty/tools/dotc/core/Definitions.scala | 4 ++++ compiler/src/dotty/tools/dotc/typer/Checking.scala | 1 + 2 files changed, 5 insertions(+) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 57cef6006113..4ef3e725eeab 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1287,6 +1287,10 @@ class Definitions { else parents } + /** Is synthesized symbol with alphanumeric name allowed to be used as an infix operator? */ + def isInfix(sym: Symbol)(implicit ctx: Context): Boolean = + (sym eq Object_eq) || (sym eq Object_ne) + // ----- primitive value class machinery ------------------------------------------ /** This class would also be obviated by the implicit function type design */ diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 4ef80659c33a..1283d75465e5 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -735,6 +735,7 @@ trait Checking { case Ident(name: SimpleName) if !name.exists(isOperatorPart) && !app.symbol.hasAnnotation(defn.InfixAnnot) && + !defn.isInfix(app.symbol) && !app.symbol.maybeOwner.is(Scala2x) && !infixOKSinceFollowedBy(tree.right) && ctx.settings.strict.value => From fc69c1d7f336b303ab822e135fb02cc45f4343ba Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Fri, 19 Apr 2019 14:48:41 +0200 Subject: [PATCH 06/13] Add test --- .../src/dotty/tools/dotc/typer/Checking.scala | 5 +++-- tests/neg-custom-args/infix.scala | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 tests/neg-custom-args/infix.scala diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 1283d75465e5..164dc713dd8f 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -740,8 +740,9 @@ trait Checking { !infixOKSinceFollowedBy(tree.right) && ctx.settings.strict.value => ctx.deprecationWarning( - i"""alphanumeric method $name is not declared @infix; should not be used as infix operator. - |The operation can be rewritten automatically to `$name` under -deprecation -rewrite""", + i"""Alphanumeric method $name is not declared @infix; it should not be used as infix operator. + |The operation can be rewritten automatically to `$name` under -deprecation -rewrite. + |Or rewrite to method syntax .$name(...) manually.""", tree.op.sourcePos) if (ctx.settings.deprecation.value) { patch(Span(tree.op.span.start, tree.op.span.start), "`") diff --git a/tests/neg-custom-args/infix.scala b/tests/neg-custom-args/infix.scala new file mode 100644 index 000000000000..2c0cc23c3b4e --- /dev/null +++ b/tests/neg-custom-args/infix.scala @@ -0,0 +1,15 @@ +// Compile with -strict -Xfatal-warnings -deprecation +import scala.annotation.infix +class C { + @infix def op(x: Int): Int = ??? + def meth(x: Int): Int = ??? +} + +val c = C() +def test() = { + c op 2 + c.meth(2) + + c.op(2) + c meth 2 // error: should not be used as infix operator +} From addd807e58b6925dd7928541ea6230322c3411e8 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sat, 20 Apr 2019 10:05:54 +0200 Subject: [PATCH 07/13] Implement @infix for types and patterns --- .../src/dotty/tools/dotc/ast/Desugar.scala | 7 --- compiler/src/dotty/tools/dotc/ast/Trees.scala | 3 +- .../src/dotty/tools/dotc/typer/Checking.scala | 53 +++++++++++++------ .../src/dotty/tools/dotc/typer/Typer.scala | 45 ++++++++++------ tests/neg-custom-args/infix.scala | 44 +++++++++++++++ 5 files changed, 110 insertions(+), 42 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 8f84b8fbf838..96e98b28f44e 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -1440,13 +1440,6 @@ object desugar { } // This is a deliberate departure from scalac, where StringContext is not rooted (See #4732) Apply(Select(Apply(scalaDot(nme.StringContext), strs), id), elems) - case InfixOp(l, op, r) => - if (ctx.mode is Mode.Type) - AppliedTypeTree(op, l :: r :: Nil) // op[l, r] - else { - assert(ctx.mode is Mode.Pattern) // expressions are handled separately by `binop` - Apply(op, l :: r :: Nil) // op(l, r) - } case PostfixOp(t, op) => if ((ctx.mode is Mode.Type) && !op.isBackquoted && op.name == tpnme.raw.STAR) { val seqType = if (ctx.compilationUnit.isJava) defn.ArrayType else defn.SeqType diff --git a/compiler/src/dotty/tools/dotc/ast/Trees.scala b/compiler/src/dotty/tools/dotc/ast/Trees.scala index 2e9ca19b7b19..f1de830774ea 100644 --- a/compiler/src/dotty/tools/dotc/ast/Trees.scala +++ b/compiler/src/dotty/tools/dotc/ast/Trees.scala @@ -719,8 +719,9 @@ object Trees { * if (result.isDefined) "match patterns against result" */ case class UnApply[-T >: Untyped] private[ast] (fun: Tree[T], implicits: List[Tree[T]], patterns: List[Tree[T]])(implicit @constructorOnly src: SourceFile) - extends PatternTree[T] { + extends ProxyTree[T] with PatternTree[T] { type ThisTree[-T >: Untyped] = UnApply[T] + def forwardTo = fun } /** mods val name: tpt = rhs */ diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 164dc713dd8f..65c3cd268b16 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -728,28 +728,47 @@ trait Checking { /** Check that `tree` is a valid infix operation. That is, if the * operator is alphanumeric, it must be declared `@infix`. */ - def checkValidInfix(tree: untpd.InfixOp, app: Tree)(implicit ctx: Context): Unit = + def checkValidInfix(tree: untpd.InfixOp, meth: Symbol)(implicit ctx: Context): Unit = { + + def isInfix(sym: Symbol): Boolean = + sym.hasAnnotation(defn.InfixAnnot) || + defn.isInfix(sym) || + (sym.name == nme.unapply || sym.name == nme.unapplySeq) && + sym.owner.is(Module) && sym.owner.linkedClass.is(Case) && + isInfix(sym.owner.linkedClass) + tree.op match { case _: untpd.BackquotedIdent => () - case Ident(name: SimpleName) - if !name.exists(isOperatorPart) && - !app.symbol.hasAnnotation(defn.InfixAnnot) && - !defn.isInfix(app.symbol) && - !app.symbol.maybeOwner.is(Scala2x) && - !infixOKSinceFollowedBy(tree.right) && - ctx.settings.strict.value => - ctx.deprecationWarning( - i"""Alphanumeric method $name is not declared @infix; it should not be used as infix operator. - |The operation can be rewritten automatically to `$name` under -deprecation -rewrite. - |Or rewrite to method syntax .$name(...) manually.""", - tree.op.sourcePos) - if (ctx.settings.deprecation.value) { - patch(Span(tree.op.span.start, tree.op.span.start), "`") - patch(Span(tree.op.span.end, tree.op.span.end), "`") + case Ident(name: Name) => + name.toTermName match { + case name: SimpleName + if !name.exists(isOperatorPart) && + !isInfix(meth) && + !meth.maybeOwner.is(Scala2x) && + !infixOKSinceFollowedBy(tree.right) && + ctx.settings.strict.value => + val (kind, alternative) = + if (ctx.mode.is(Mode.Type)) + ("type", (n: Name) => s"prefix syntax $n[...]") + else if (ctx.mode.is(Mode.Pattern)) + ("extractor", (n: Name) => s"prefix syntax $n(...)") + else + ("method", (n: Name) => s"method syntax .$n(...)") + ctx.deprecationWarning( + i"""Alphanumeric $kind $name is not declared @infix; it should not be used as infix operator. + |The operation can be rewritten automatically to `$name` under -deprecation -rewrite. + |Or rewrite to ${alternative(name)} manually.""", + tree.op.sourcePos) + if (ctx.settings.deprecation.value) { + patch(Span(tree.op.span.start, tree.op.span.start), "`") + patch(Span(tree.op.span.end, tree.op.span.end), "`") + } + case _ => } case _ => } + } /** Issue a feature warning if feature is not enabled */ def checkFeature(name: TermName, @@ -1134,6 +1153,6 @@ trait NoChecking extends ReChecking { override def checkNoForwardDependencies(vparams: List[ValDef])(implicit ctx: Context): Unit = () override def checkMembersOK(tp: Type, pos: SourcePosition)(implicit ctx: Context): Type = tp override def checkInInlineContext(what: String, posd: Positioned)(implicit ctx: Context): Unit = () - override def checkValidInfix(tree: untpd.InfixOp, app: Tree)(implicit ctx: Context): Unit = () + override def checkValidInfix(tree: untpd.InfixOp, meth: Symbol)(implicit ctx: Context): Unit = () override def checkFeature(name: TermName, description: => String, featureUseSite: Symbol, pos: SourcePosition)(implicit ctx: Context): Unit = () } diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 5c52fa74fa44..cfa4f7bb18f9 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1879,28 +1879,39 @@ class Typer extends Namer /** Translate infix operation expression `l op r` to * - * l.op(r) if `op` is left-associative + * l.op(r) if `op` is left-associative * { val x = l; r.op(l) } if `op` is right-associative call-by-value and `l` is impure * r.op(l) if `op` is right-associative call-by-name or `l` is pure + * + * Translate infix type `l op r` to `op[l, r]` + * Translate infix pattern `l op r` to `op(l, r)` */ def typedInfixOp(tree: untpd.InfixOp, pt: Type)(implicit ctx: Context): Tree = { val untpd.InfixOp(l, op, r) = tree - val app = typedApply(desugar.binop(l, op, r), pt) - checkValidInfix(tree, app) - if (untpd.isLeftAssoc(op.name)) app - else { - val defs = new mutable.ListBuffer[Tree] - def lift(app: Tree): Tree = (app: @unchecked) match { - case Apply(fn, args) => - if (app.tpe.isError) app - else tpd.cpy.Apply(app)(fn, LiftImpure.liftArgs(defs, fn.tpe, args)) - case Assign(lhs, rhs) => - tpd.cpy.Assign(app)(lhs, lift(rhs)) - case Block(stats, expr) => - tpd.cpy.Block(app)(stats, lift(expr)) + val result = + if (ctx.mode.is(Mode.Type)) + typedAppliedTypeTree(cpy.AppliedTypeTree(tree)(op, l :: r :: Nil)) + else if (ctx.mode.is(Mode.Pattern)) + typedUnApply(cpy.Apply(tree)(op, l :: r :: Nil), pt) + else { + val app = typedApply(desugar.binop(l, op, r), pt) + if (untpd.isLeftAssoc(op.name)) app + else { + val defs = new mutable.ListBuffer[Tree] + def lift(app: Tree): Tree = (app: @unchecked) match { + case Apply(fn, args) => + if (app.tpe.isError) app + else tpd.cpy.Apply(app)(fn, LiftImpure.liftArgs(defs, fn.tpe, args)) + case Assign(lhs, rhs) => + tpd.cpy.Assign(app)(lhs, lift(rhs)) + case Block(stats, expr) => + tpd.cpy.Block(app)(stats, lift(expr)) + } + wrapDefs(defs, lift(app)) + } } - wrapDefs(defs, lift(app)) - } + checkValidInfix(tree, result.symbol) + result } /** Translate tuples of all arities */ @@ -2153,7 +2164,7 @@ class Typer extends Namer case tree: untpd.UnApply => typedUnApply(tree, pt) case tree: untpd.Tuple => typedTuple(tree, pt) case tree: untpd.DependentTypeTree => typed(untpd.TypeTree().withSpan(tree.span), pt) - case tree: untpd.InfixOp if ctx.mode.isExpr => typedInfixOp(tree, pt) + case tree: untpd.InfixOp => typedInfixOp(tree, pt) case tree @ untpd.PostfixOp(qual, Ident(nme.WILDCARD)) => typedAsFunction(tree, pt) case untpd.EmptyTree => tpd.EmptyTree case tree: untpd.Quote => typedQuote(tree, pt) diff --git a/tests/neg-custom-args/infix.scala b/tests/neg-custom-args/infix.scala index 2c0cc23c3b4e..1c95a3e489f5 100644 --- a/tests/neg-custom-args/infix.scala +++ b/tests/neg-custom-args/infix.scala @@ -3,6 +3,8 @@ import scala.annotation.infix class C { @infix def op(x: Int): Int = ??? def meth(x: Int): Int = ??? + def matching(x: Int => Int) = ??? + def +(x: Int): Int = ??? } val c = C() @@ -12,4 +14,46 @@ def test() = { c.op(2) c meth 2 // error: should not be used as infix operator + c `meth` 2 // OK, sincd `meth` is backquoted + c + 3 // OK, since `+` is symbolic + 1 to 2 // OK, since `to` is defined by Scala-2 + c meth { // OK, since `meth` is followed by `{...}` + 3 + } + c matching { // OK, since `meth` is followed by `{...}` + case x => x + } + + @infix class Or[X, Y] + class AndC[X, Y] + @infix type And[X, Y] = AndC[X, Y] + @infix type &&[X, Y] = AndC[X, Y] + + class Map[X, Y] + + val x1: Int Map String = ??? // error + val x2: Int Or String = ??? // OK since Or is declared `@infix` + val x3: Int AndC String = ??? // error + val x4: Int `AndC` String = ??? // OK + val x5: Int And String = ??? // OK + val x6: Int && String = ??? + + case class Pair[T](x: T, y: T) + @infix case class Q[T](x: T, y: T) + + object PP { + @infix def unapply[T](x: Pair[T]): Option[(T, T)] = Some((x.x, x.y)) + } + + val p = Pair(1, 2) + val Pair(_, _) = p + val _ Pair _ = p // error + val _ `Pair` _ = p // OK + val _ PP _ = p // OK + + val q = Q(1, 2) + val Q(_, _) = q + val _ Q _ = p // OK + + } From e082e4d76c7bbae90bf318d0ef63a6b8bc746c8e Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sat, 20 Apr 2019 11:13:18 +0200 Subject: [PATCH 08/13] Drop dead code --- compiler/src/dotty/tools/dotc/typer/Checking.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 65c3cd268b16..7e40f2fb2d62 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -766,7 +766,6 @@ trait Checking { } case _ => } - case _ => } } From 9c069c17ecc02ec094486d128e62128fab00e428 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sat, 20 Apr 2019 10:19:32 +0200 Subject: [PATCH 09/13] Treat unapply/unapplySeq more systematically --- compiler/src/dotty/tools/dotc/core/NameOps.scala | 1 + compiler/src/dotty/tools/dotc/typer/Checking.scala | 5 +++-- compiler/src/dotty/tools/dotc/typer/PrepareInlineable.scala | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/NameOps.scala b/compiler/src/dotty/tools/dotc/core/NameOps.scala index b3072d60ccfe..333e982ec8b5 100644 --- a/compiler/src/dotty/tools/dotc/core/NameOps.scala +++ b/compiler/src/dotty/tools/dotc/core/NameOps.scala @@ -71,6 +71,7 @@ object NameOps { def isSelectorName: Boolean = testSimple(n => n.startsWith("_") && n.drop(1).forall(_.isDigit)) def isAnonymousClassName: Boolean = name.startsWith(str.ANON_CLASS) def isAnonymousFunctionName: Boolean = name.startsWith(str.ANON_FUN) + def isUnapplyName: Boolean = name == nme.unapply || name == nme.unapplySeq /** Is name a variable name? */ def isVariableName: Boolean = testSimple { n => diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 7e40f2fb2d62..37d4ce9c912c 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -28,6 +28,7 @@ import Decorators._ import ErrorReporting.{err, errorType} import config.Printers.{typr, patmatch} import NameKinds.DefaultGetterName +import NameOps._ import SymDenotations.{NoCompleter, NoDenotation} import Applications.unapplyArgs import transform.patmat.SpaceEngine.isIrrefutableUnapply @@ -733,7 +734,7 @@ trait Checking { def isInfix(sym: Symbol): Boolean = sym.hasAnnotation(defn.InfixAnnot) || defn.isInfix(sym) || - (sym.name == nme.unapply || sym.name == nme.unapplySeq) && + sym.name.isUnapplyName && sym.owner.is(Module) && sym.owner.linkedClass.is(Case) && isInfix(sym.owner.linkedClass) @@ -1038,7 +1039,7 @@ trait Checking { def checkInInlineContext(what: String, posd: Positioned)(implicit ctx: Context): Unit = if (!ctx.inInlineMethod && !ctx.isInlineContext) { val inInlineUnapply = ctx.owner.ownersIterator.exists(owner => - owner.name == nme.unapply && owner.is(Inline) && owner.is(Method)) + owner.name.isUnapplyName && owner.is(Inline) && owner.is(Method)) val msg = if (inInlineUnapply) "cannot be used in an inline unapply" else "can only be used in an inline method" diff --git a/compiler/src/dotty/tools/dotc/typer/PrepareInlineable.scala b/compiler/src/dotty/tools/dotc/typer/PrepareInlineable.scala index 04c2521c68c2..b4d3c36488f2 100644 --- a/compiler/src/dotty/tools/dotc/typer/PrepareInlineable.scala +++ b/compiler/src/dotty/tools/dotc/typer/PrepareInlineable.scala @@ -14,6 +14,7 @@ import StdNames.nme import Contexts.Context import Names.Name import NameKinds.{InlineAccessorName, UniqueInlineName} +import NameOps._ import Annotations._ import transform.{AccessProxies, PCPCheckAndHeal, Splicer, TreeMapWithStages} import config.Printers.inlining @@ -246,7 +247,7 @@ object PrepareInlineable { def checkInlineMethod(inlined: Symbol, body: Tree)(implicit ctx: Context): Unit = { if (ctx.outer.inInlineMethod) ctx.error(ex"implementation restriction: nested inline methods are not supported", inlined.sourcePos) - if (inlined.name == nme.unapply && tupleArgs(body).isEmpty) + if (inlined.name.isUnapplyName && tupleArgs(body).isEmpty) ctx.warning( em"inline unapply method can be rewritten only if its right hand side is a tuple (e1, ..., eN)", body.sourcePos) From c0daac1711eae8c32f44dda2ffaa7ab02b7583cc Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sat, 20 Apr 2019 16:53:01 +0200 Subject: [PATCH 10/13] More unapply changes --- compiler/src/dotty/tools/dotc/ast/Desugar.scala | 2 +- compiler/src/dotty/tools/dotc/core/SymDenotations.scala | 4 ++-- compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 96e98b28f44e..d9f2e1c9aa05 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -54,7 +54,7 @@ object desugar { * case class method that clashes with a user-defined method? */ def isRetractableCaseClassMethodName(name: Name)(implicit ctx: Context): Boolean = name match { - case nme.apply | nme.unapply | nme.copy => true + case nme.apply | nme.unapply | nme.unapplySeq | nme.copy => true case DefaultGetterName(nme.copy, _) => true case _ => false } diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index 9ffba313ed0f..3dbcdacb422d 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -806,8 +806,8 @@ object SymDenotations { def isSkolem: Boolean = name == nme.SKOLEM def isInlineMethod(implicit ctx: Context): Boolean = - is(InlineMethod, butNot = Accessor) && - name != nme.unapply // unapply methods do not count as inline methods + is(InlineMethod, butNot = AccessorOrSynthetic) && + !name.isUnapplyName // unapply methods do not count as inline methods // we need an inline flag on them only do that // reduceProjection gets access to their rhs diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 1372b2588cbe..1ef1c7a084b6 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -463,7 +463,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { changePrec(OrPrec) { toText(trees, " | ") } case UnApply(fun, implicits, patterns) => val extractor = fun match { - case Select(extractor, nme.unapply) => extractor + case Select(extractor, name) if name.isUnapplyName => extractor case _ => fun } toTextLocal(extractor) ~ From ba04554a144ed25fa64e5ad58d58ee15f96c7158 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Wed, 15 May 2019 18:02:10 +0200 Subject: [PATCH 11/13] Fix test --- .../dotty/tools/dotc/CompilationTests.scala | 3 +- tests/neg-custom-args/infix.scala | 4 +- tests/neg-strict/infix.scala | 59 +++++++++++++++++++ 3 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 tests/neg-strict/infix.scala diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index c7a18e8fb9b5..d2cdf88d1446 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -177,7 +177,8 @@ class CompilationTests extends ParallelTesting { "tests/neg-custom-args/toplevel-samesource/S.scala", "tests/neg-custom-args/toplevel-samesource/nested/S.scala"), defaultOptions), - compileFile("tests/neg-custom-args/i6300.scala", allowDeepSubtypes) + compileFile("tests/neg-custom-args/i6300.scala", allowDeepSubtypes), + compileFile("tests/neg-custom-args/infix.scala", defaultOptions.and("-strict", "-deprecation", "-Xfatal-warnings")) ).checkExpectedErrors() } diff --git a/tests/neg-custom-args/infix.scala b/tests/neg-custom-args/infix.scala index 1c95a3e489f5..e8db3532d07c 100644 --- a/tests/neg-custom-args/infix.scala +++ b/tests/neg-custom-args/infix.scala @@ -49,11 +49,11 @@ def test() = { val Pair(_, _) = p val _ Pair _ = p // error val _ `Pair` _ = p // OK - val _ PP _ = p // OK + val (_ PP _): @unchecked = p // OK val q = Q(1, 2) val Q(_, _) = q - val _ Q _ = p // OK + val _ Q _ = q // OK } diff --git a/tests/neg-strict/infix.scala b/tests/neg-strict/infix.scala new file mode 100644 index 000000000000..e8db3532d07c --- /dev/null +++ b/tests/neg-strict/infix.scala @@ -0,0 +1,59 @@ +// Compile with -strict -Xfatal-warnings -deprecation +import scala.annotation.infix +class C { + @infix def op(x: Int): Int = ??? + def meth(x: Int): Int = ??? + def matching(x: Int => Int) = ??? + def +(x: Int): Int = ??? +} + +val c = C() +def test() = { + c op 2 + c.meth(2) + + c.op(2) + c meth 2 // error: should not be used as infix operator + c `meth` 2 // OK, sincd `meth` is backquoted + c + 3 // OK, since `+` is symbolic + 1 to 2 // OK, since `to` is defined by Scala-2 + c meth { // OK, since `meth` is followed by `{...}` + 3 + } + c matching { // OK, since `meth` is followed by `{...}` + case x => x + } + + @infix class Or[X, Y] + class AndC[X, Y] + @infix type And[X, Y] = AndC[X, Y] + @infix type &&[X, Y] = AndC[X, Y] + + class Map[X, Y] + + val x1: Int Map String = ??? // error + val x2: Int Or String = ??? // OK since Or is declared `@infix` + val x3: Int AndC String = ??? // error + val x4: Int `AndC` String = ??? // OK + val x5: Int And String = ??? // OK + val x6: Int && String = ??? + + case class Pair[T](x: T, y: T) + @infix case class Q[T](x: T, y: T) + + object PP { + @infix def unapply[T](x: Pair[T]): Option[(T, T)] = Some((x.x, x.y)) + } + + val p = Pair(1, 2) + val Pair(_, _) = p + val _ Pair _ = p // error + val _ `Pair` _ = p // OK + val (_ PP _): @unchecked = p // OK + + val q = Q(1, 2) + val Q(_, _) = q + val _ Q _ = q // OK + + +} From c1a3aceabd18f412e96b1ad77553c872f67e9b10 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Wed, 15 May 2019 18:35:12 +0200 Subject: [PATCH 12/13] Fix rebase breakage --- compiler/src/dotty/tools/dotc/core/SymDenotations.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index 3dbcdacb422d..21e28b6baa06 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -806,7 +806,7 @@ object SymDenotations { def isSkolem: Boolean = name == nme.SKOLEM def isInlineMethod(implicit ctx: Context): Boolean = - is(InlineMethod, butNot = AccessorOrSynthetic) && + is(InlineMethod, butNot = Accessor) && !name.isUnapplyName // unapply methods do not count as inline methods // we need an inline flag on them only do that // reduceProjection gets access to their rhs From 774e9a76009e1a6a7aff7b86993b52918b362979 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Wed, 15 May 2019 20:33:01 +0200 Subject: [PATCH 13/13] Remove duplicate test --- tests/neg-strict/infix.scala | 59 ------------------------------------ 1 file changed, 59 deletions(-) delete mode 100644 tests/neg-strict/infix.scala diff --git a/tests/neg-strict/infix.scala b/tests/neg-strict/infix.scala deleted file mode 100644 index e8db3532d07c..000000000000 --- a/tests/neg-strict/infix.scala +++ /dev/null @@ -1,59 +0,0 @@ -// Compile with -strict -Xfatal-warnings -deprecation -import scala.annotation.infix -class C { - @infix def op(x: Int): Int = ??? - def meth(x: Int): Int = ??? - def matching(x: Int => Int) = ??? - def +(x: Int): Int = ??? -} - -val c = C() -def test() = { - c op 2 - c.meth(2) - - c.op(2) - c meth 2 // error: should not be used as infix operator - c `meth` 2 // OK, sincd `meth` is backquoted - c + 3 // OK, since `+` is symbolic - 1 to 2 // OK, since `to` is defined by Scala-2 - c meth { // OK, since `meth` is followed by `{...}` - 3 - } - c matching { // OK, since `meth` is followed by `{...}` - case x => x - } - - @infix class Or[X, Y] - class AndC[X, Y] - @infix type And[X, Y] = AndC[X, Y] - @infix type &&[X, Y] = AndC[X, Y] - - class Map[X, Y] - - val x1: Int Map String = ??? // error - val x2: Int Or String = ??? // OK since Or is declared `@infix` - val x3: Int AndC String = ??? // error - val x4: Int `AndC` String = ??? // OK - val x5: Int And String = ??? // OK - val x6: Int && String = ??? - - case class Pair[T](x: T, y: T) - @infix case class Q[T](x: T, y: T) - - object PP { - @infix def unapply[T](x: Pair[T]): Option[(T, T)] = Some((x.x, x.y)) - } - - val p = Pair(1, 2) - val Pair(_, _) = p - val _ Pair _ = p // error - val _ `Pair` _ = p // OK - val (_ PP _): @unchecked = p // OK - - val q = Q(1, 2) - val Q(_, _) = q - val _ Q _ = q // OK - - -}