Skip to content

Commit 206fa7c

Browse files
authored
Merge pull request #5975 from dotty-staging/add-infix
Add @infix annotation
2 parents d3a9a91 + 774e9a7 commit 206fa7c

File tree

16 files changed

+327
-34
lines changed

16 files changed

+327
-34
lines changed

compiler/src/dotty/tools/dotc/ast/Desugar.scala

+1-8
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ object desugar {
5454
* case class method that clashes with a user-defined method?
5555
*/
5656
def isRetractableCaseClassMethodName(name: Name)(implicit ctx: Context): Boolean = name match {
57-
case nme.apply | nme.unapply | nme.copy => true
57+
case nme.apply | nme.unapply | nme.unapplySeq | nme.copy => true
5858
case DefaultGetterName(nme.copy, _) => true
5959
case _ => false
6060
}
@@ -1440,13 +1440,6 @@ object desugar {
14401440
}
14411441
// This is a deliberate departure from scalac, where StringContext is not rooted (See #4732)
14421442
Apply(Select(Apply(scalaDot(nme.StringContext), strs), id), elems)
1443-
case InfixOp(l, op, r) =>
1444-
if (ctx.mode is Mode.Type)
1445-
AppliedTypeTree(op, l :: r :: Nil) // op[l, r]
1446-
else {
1447-
assert(ctx.mode is Mode.Pattern) // expressions are handled separately by `binop`
1448-
Apply(op, l :: r :: Nil) // op(l, r)
1449-
}
14501443
case PostfixOp(t, op) =>
14511444
if ((ctx.mode is Mode.Type) && !op.isBackquoted && op.name == tpnme.raw.STAR) {
14521445
val seqType = if (ctx.compilationUnit.isJava) defn.ArrayType else defn.SeqType

compiler/src/dotty/tools/dotc/ast/Trees.scala

+2-1
Original file line numberDiff line numberDiff line change
@@ -719,8 +719,9 @@ object Trees {
719719
* if (result.isDefined) "match patterns against result"
720720
*/
721721
case class UnApply[-T >: Untyped] private[ast] (fun: Tree[T], implicits: List[Tree[T]], patterns: List[Tree[T]])(implicit @constructorOnly src: SourceFile)
722-
extends PatternTree[T] {
722+
extends ProxyTree[T] with PatternTree[T] {
723723
type ThisTree[-T >: Untyped] = UnApply[T]
724+
def forwardTo = fun
724725
}
725726

726727
/** mods val name: tpt = rhs */

compiler/src/dotty/tools/dotc/core/Definitions.scala

+8
Original file line numberDiff line numberDiff line change
@@ -883,6 +883,10 @@ class Definitions {
883883
def ShowAsInfixAnnot(implicit ctx: Context): ClassSymbol = ShowAsInfixAnotType.symbol.asClass
884884
lazy val FunctionalInterfaceAnnotType = ctx.requiredClassRef("java.lang.FunctionalInterface")
885885
def FunctionalInterfaceAnnot(implicit ctx: Context) = FunctionalInterfaceAnnotType.symbol.asClass
886+
lazy val InfixAnnotType = ctx.requiredClassRef("scala.annotation.infix")
887+
def InfixAnnot(implicit ctx: Context) = InfixAnnotType.symbol.asClass
888+
lazy val AlphaAnnotType = ctx.requiredClassRef("scala.annotation.alpha")
889+
def AlphaAnnot(implicit ctx: Context) = AlphaAnnotType.symbol.asClass
886890

887891
// convenient one-parameter method types
888892
def methOfAny(tp: Type): MethodType = MethodType(List(AnyType), tp)
@@ -1283,6 +1287,10 @@ class Definitions {
12831287
else parents
12841288
}
12851289

1290+
/** Is synthesized symbol with alphanumeric name allowed to be used as an infix operator? */
1291+
def isInfix(sym: Symbol)(implicit ctx: Context): Boolean =
1292+
(sym eq Object_eq) || (sym eq Object_ne)
1293+
12861294
// ----- primitive value class machinery ------------------------------------------
12871295

12881296
/** This class would also be obviated by the implicit function type design */

compiler/src/dotty/tools/dotc/core/NameOps.scala

+1
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ object NameOps {
7171
def isSelectorName: Boolean = testSimple(n => n.startsWith("_") && n.drop(1).forall(_.isDigit))
7272
def isAnonymousClassName: Boolean = name.startsWith(str.ANON_CLASS)
7373
def isAnonymousFunctionName: Boolean = name.startsWith(str.ANON_FUN)
74+
def isUnapplyName: Boolean = name == nme.unapply || name == nme.unapplySeq
7475

7576
/** Is name a variable name? */
7677
def isVariableName: Boolean = testSimple { n =>

compiler/src/dotty/tools/dotc/core/SymDenotations.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -807,7 +807,7 @@ object SymDenotations {
807807

808808
def isInlineMethod(implicit ctx: Context): Boolean =
809809
is(InlineMethod, butNot = Accessor) &&
810-
name != nme.unapply // unapply methods do not count as inline methods
810+
!name.isUnapplyName // unapply methods do not count as inline methods
811811
// we need an inline flag on them only do that
812812
// reduceProjection gets access to their rhs
813813

compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) {
463463
changePrec(OrPrec) { toText(trees, " | ") }
464464
case UnApply(fun, implicits, patterns) =>
465465
val extractor = fun match {
466-
case Select(extractor, nme.unapply) => extractor
466+
case Select(extractor, name) if name.isUnapplyName => extractor
467467
case _ => fun
468468
}
469469
toTextLocal(extractor) ~

compiler/src/dotty/tools/dotc/typer/Checking.scala

+60-5
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,24 @@ import rewrites.Rewrites.patch
2020
import util.Spans.Span
2121

2222
import util.SourcePosition
23+
import util.Spans.Span
24+
import rewrites.Rewrites.patch
2325
import transform.SymUtils._
26+
import transform.ValueClasses._
2427
import Decorators._
2528
import ErrorReporting.{err, errorType}
2629
import config.Printers.{typr, patmatch}
2730
import NameKinds.DefaultGetterName
31+
import NameOps._
32+
import SymDenotations.{NoCompleter, NoDenotation}
2833
import Applications.unapplyArgs
2934
import transform.patmat.SpaceEngine.isIrrefutableUnapply
3035

36+
3137
import collection.mutable
32-
import SymDenotations.{NoCompleter, NoDenotation}
33-
import dotty.tools.dotc.reporting.diagnostic.Message
34-
import dotty.tools.dotc.reporting.diagnostic.messages._
35-
import dotty.tools.dotc.transform.ValueClasses._
38+
import reporting.diagnostic.Message
39+
import reporting.diagnostic.messages._
40+
import scala.tasty.util.Chars.isOperatorPart
3641

3742
object Checking {
3843
import tpd._
@@ -716,6 +721,55 @@ trait Checking {
716721
i"Use of implicit conversion ${conv.showLocated}", NoSymbol, posd.sourcePos)
717722
}
718723

724+
private def infixOKSinceFollowedBy(tree: untpd.Tree): Boolean = tree match {
725+
case _: untpd.Block | _: untpd.Match => true
726+
case _ => false
727+
}
728+
729+
/** Check that `tree` is a valid infix operation. That is, if the
730+
* operator is alphanumeric, it must be declared `@infix`.
731+
*/
732+
def checkValidInfix(tree: untpd.InfixOp, meth: Symbol)(implicit ctx: Context): Unit = {
733+
734+
def isInfix(sym: Symbol): Boolean =
735+
sym.hasAnnotation(defn.InfixAnnot) ||
736+
defn.isInfix(sym) ||
737+
sym.name.isUnapplyName &&
738+
sym.owner.is(Module) && sym.owner.linkedClass.is(Case) &&
739+
isInfix(sym.owner.linkedClass)
740+
741+
tree.op match {
742+
case _: untpd.BackquotedIdent =>
743+
()
744+
case Ident(name: Name) =>
745+
name.toTermName match {
746+
case name: SimpleName
747+
if !name.exists(isOperatorPart) &&
748+
!isInfix(meth) &&
749+
!meth.maybeOwner.is(Scala2x) &&
750+
!infixOKSinceFollowedBy(tree.right) &&
751+
ctx.settings.strict.value =>
752+
val (kind, alternative) =
753+
if (ctx.mode.is(Mode.Type))
754+
("type", (n: Name) => s"prefix syntax $n[...]")
755+
else if (ctx.mode.is(Mode.Pattern))
756+
("extractor", (n: Name) => s"prefix syntax $n(...)")
757+
else
758+
("method", (n: Name) => s"method syntax .$n(...)")
759+
ctx.deprecationWarning(
760+
i"""Alphanumeric $kind $name is not declared @infix; it should not be used as infix operator.
761+
|The operation can be rewritten automatically to `$name` under -deprecation -rewrite.
762+
|Or rewrite to ${alternative(name)} manually.""",
763+
tree.op.sourcePos)
764+
if (ctx.settings.deprecation.value) {
765+
patch(Span(tree.op.span.start, tree.op.span.start), "`")
766+
patch(Span(tree.op.span.end, tree.op.span.end), "`")
767+
}
768+
case _ =>
769+
}
770+
}
771+
}
772+
719773
/** Issue a feature warning if feature is not enabled */
720774
def checkFeature(name: TermName,
721775
description: => String,
@@ -985,7 +1039,7 @@ trait Checking {
9851039
def checkInInlineContext(what: String, posd: Positioned)(implicit ctx: Context): Unit =
9861040
if (!ctx.inInlineMethod && !ctx.isInlineContext) {
9871041
val inInlineUnapply = ctx.owner.ownersIterator.exists(owner =>
988-
owner.name == nme.unapply && owner.is(Inline) && owner.is(Method))
1042+
owner.name.isUnapplyName && owner.is(Inline) && owner.is(Method))
9891043
val msg =
9901044
if (inInlineUnapply) "cannot be used in an inline unapply"
9911045
else "can only be used in an inline method"
@@ -1099,5 +1153,6 @@ trait NoChecking extends ReChecking {
10991153
override def checkNoForwardDependencies(vparams: List[ValDef])(implicit ctx: Context): Unit = ()
11001154
override def checkMembersOK(tp: Type, pos: SourcePosition)(implicit ctx: Context): Type = tp
11011155
override def checkInInlineContext(what: String, posd: Positioned)(implicit ctx: Context): Unit = ()
1156+
override def checkValidInfix(tree: untpd.InfixOp, meth: Symbol)(implicit ctx: Context): Unit = ()
11021157
override def checkFeature(name: TermName, description: => String, featureUseSite: Symbol, pos: SourcePosition)(implicit ctx: Context): Unit = ()
11031158
}

compiler/src/dotty/tools/dotc/typer/PrepareInlineable.scala

+2-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import StdNames.nme
1414
import Contexts.Context
1515
import Names.Name
1616
import NameKinds.{InlineAccessorName, UniqueInlineName}
17+
import NameOps._
1718
import Annotations._
1819
import transform.{AccessProxies, PCPCheckAndHeal, Splicer, TreeMapWithStages}
1920
import config.Printers.inlining
@@ -246,7 +247,7 @@ object PrepareInlineable {
246247
def checkInlineMethod(inlined: Symbol, body: Tree)(implicit ctx: Context): Unit = {
247248
if (ctx.outer.inInlineMethod)
248249
ctx.error(ex"implementation restriction: nested inline methods are not supported", inlined.sourcePos)
249-
if (inlined.name == nme.unapply && tupleArgs(body).isEmpty)
250+
if (inlined.name.isUnapplyName && tupleArgs(body).isEmpty)
250251
ctx.warning(
251252
em"inline unapply method can be rewritten only if its right hand side is a tuple (e1, ..., eN)",
252253
body.sourcePos)

compiler/src/dotty/tools/dotc/typer/Typer.scala

+28-16
Original file line numberDiff line numberDiff line change
@@ -1879,27 +1879,39 @@ class Typer extends Namer
18791879

18801880
/** Translate infix operation expression `l op r` to
18811881
*
1882-
* l.op(r) if `op` is left-associative
1882+
* l.op(r) if `op` is left-associative
18831883
* { val x = l; r.op(l) } if `op` is right-associative call-by-value and `l` is impure
18841884
* r.op(l) if `op` is right-associative call-by-name or `l` is pure
1885+
*
1886+
* Translate infix type `l op r` to `op[l, r]`
1887+
* Translate infix pattern `l op r` to `op(l, r)`
18851888
*/
18861889
def typedInfixOp(tree: untpd.InfixOp, pt: Type)(implicit ctx: Context): Tree = {
18871890
val untpd.InfixOp(l, op, r) = tree
1888-
val app = typedApply(desugar.binop(l, op, r), pt)
1889-
if (untpd.isLeftAssoc(op.name)) app
1890-
else {
1891-
val defs = new mutable.ListBuffer[Tree]
1892-
def lift(app: Tree): Tree = (app: @unchecked) match {
1893-
case Apply(fn, args) =>
1894-
if (app.tpe.isError) app
1895-
else tpd.cpy.Apply(app)(fn, LiftImpure.liftArgs(defs, fn.tpe, args))
1896-
case Assign(lhs, rhs) =>
1897-
tpd.cpy.Assign(app)(lhs, lift(rhs))
1898-
case Block(stats, expr) =>
1899-
tpd.cpy.Block(app)(stats, lift(expr))
1891+
val result =
1892+
if (ctx.mode.is(Mode.Type))
1893+
typedAppliedTypeTree(cpy.AppliedTypeTree(tree)(op, l :: r :: Nil))
1894+
else if (ctx.mode.is(Mode.Pattern))
1895+
typedUnApply(cpy.Apply(tree)(op, l :: r :: Nil), pt)
1896+
else {
1897+
val app = typedApply(desugar.binop(l, op, r), pt)
1898+
if (untpd.isLeftAssoc(op.name)) app
1899+
else {
1900+
val defs = new mutable.ListBuffer[Tree]
1901+
def lift(app: Tree): Tree = (app: @unchecked) match {
1902+
case Apply(fn, args) =>
1903+
if (app.tpe.isError) app
1904+
else tpd.cpy.Apply(app)(fn, LiftImpure.liftArgs(defs, fn.tpe, args))
1905+
case Assign(lhs, rhs) =>
1906+
tpd.cpy.Assign(app)(lhs, lift(rhs))
1907+
case Block(stats, expr) =>
1908+
tpd.cpy.Block(app)(stats, lift(expr))
1909+
}
1910+
wrapDefs(defs, lift(app))
1911+
}
19001912
}
1901-
wrapDefs(defs, lift(app))
1902-
}
1913+
checkValidInfix(tree, result.symbol)
1914+
result
19031915
}
19041916

19051917
/** Translate tuples of all arities */
@@ -2152,7 +2164,7 @@ class Typer extends Namer
21522164
case tree: untpd.UnApply => typedUnApply(tree, pt)
21532165
case tree: untpd.Tuple => typedTuple(tree, pt)
21542166
case tree: untpd.DependentTypeTree => typed(untpd.TypeTree().withSpan(tree.span), pt)
2155-
case tree: untpd.InfixOp if ctx.mode.isExpr => typedInfixOp(tree, pt)
2167+
case tree: untpd.InfixOp => typedInfixOp(tree, pt)
21562168
case tree @ untpd.PostfixOp(qual, Ident(nme.WILDCARD)) => typedAsFunction(tree, pt)
21572169
case untpd.EmptyTree => tpd.EmptyTree
21582170
case tree: untpd.Quote => typedQuote(tree, pt)

compiler/test/dotty/tools/dotc/CompilationTests.scala

+2-1
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,8 @@ class CompilationTests extends ParallelTesting {
177177
"tests/neg-custom-args/toplevel-samesource/S.scala",
178178
"tests/neg-custom-args/toplevel-samesource/nested/S.scala"),
179179
defaultOptions),
180-
compileFile("tests/neg-custom-args/i6300.scala", allowDeepSubtypes)
180+
compileFile("tests/neg-custom-args/i6300.scala", allowDeepSubtypes),
181+
compileFile("tests/neg-custom-args/infix.scala", defaultOptions.and("-strict", "-deprecation", "-Xfatal-warnings"))
181182
).checkExpectedErrors()
182183
}
183184

0 commit comments

Comments
 (0)