Skip to content

Allow prefix operators on the LHS of assignments #13328

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Aug 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions compiler/src/dotty/tools/dotc/parsing/Parsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1886,6 +1886,7 @@ object Parsers {
* | `return' [Expr]
* | ForExpr
* | [SimpleExpr `.'] id `=' Expr
* | PrefixOperator SimpleExpr `=' Expr
* | SimpleExpr1 ArgumentExprs `=' Expr
* | PostfixExpr [Ascription]
* | ‘inline’ InfixExpr MatchClause
Expand Down Expand Up @@ -2045,7 +2046,7 @@ object Parsers {
def expr1Rest(t: Tree, location: Location): Tree = in.token match
case EQUALS =>
t match
case Ident(_) | Select(_, _) | Apply(_, _) =>
case Ident(_) | Select(_, _) | Apply(_, _) | PrefixOp(_, _) =>
Copy link
Contributor Author

@odersky odersky Aug 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What must have happened in Scala 2: We had similar logic here, but we did not mark prefix operations specially; so a prefix operation was accepted since it was represented by a Select. Similarly for infix. The following expression is parsed in Scala 2, but will get a type error afterwards:

   1 + 1 = 2

In dotc you get instead a parse error:

-- Error: ../../new/test.scala:4:10 --------------------------------------------
4 |    1 + 1 = 2
  |          ^
  |          end of statement expected but '=' found

That's because we represent infix operations as Infix nodes instead of Apply nodes.

atSpan(startOffset(t), in.skipToken()) {
val loc = if location.inArgs then location else Location.ElseWhere
Assign(t, subPart(() => expr(loc)))
Expand Down Expand Up @@ -2206,8 +2207,9 @@ object Parsers {
isOperator = !(location.inArgs && followingIsVararg()),
maybePostfix = true)

/** PrefixExpr ::= [`-' | `+' | `~' | `!'] SimpleExpr
*/
/** PrefixExpr ::= [PrefixOperator'] SimpleExpr
* PrefixOperator ::= ‘-’ | ‘+’ | ‘~’ | ‘!’
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+, - and ~ are not working.

class Foo(var value: Int):
  def `unary_+_=`(value: Int): Unit = this.value = +value
  def `unary_-_=`(value: Int): Unit = this.value = -value
  def `unary_~_=`(value: Int): Unit = this.value = ~value
end Foo

def test =
  val x = Foo(9)
  +x = 10 // error: value unary_+ is not a member of Foo - did you mean x.unary_+_=?
  -x = 10 // error: value unary_- is not a member of Foo, but could be made available as an extension method.
  ~x = 10 // error: value unary_~ is not a member of Foo - did you mean x.unary_~_=?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's because Foo is missing unary_+ and so on. So that's as expected.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does that mean that we cannot write !x = 3 if we have unary_!_= but not unary_!?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, exactly

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? Do we typecheck x.unary_! in x.unary_! = 3 before fully desugaring it into x.unarry_!_=(3)?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like that, yes. Using a setter foo_= as x.foo = y is only allowed/attempted when x.foo exists itself. This is the same in Scala 2, and I think it's mentioned in the spec somewhere.

*/
val prefixExpr: Location => Tree = location =>
if isIdent && nme.raw.isUnary(in.name)
&& in.canStartExprTokens.contains(in.lookahead.token)
Expand Down
10 changes: 8 additions & 2 deletions compiler/src/dotty/tools/dotc/typer/RefChecks.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1039,7 +1039,7 @@ object RefChecks {
val what =
if tpe.paramNames.isEmpty then "empty parameter list.\n\nPossible fix: remove the `()` arguments."
else "parameters"
report.warning(s"Unary method cannot take $what", sym.sourcePos)
report.warning(s"unary_<op> method cannot take $what", sym.sourcePos)
case tpe: PolyType =>
checkParameters(tpe.resType)
case _ =>
Expand All @@ -1057,7 +1057,13 @@ object RefChecks {
case tpe: PolyType =>
checkExtensionParameters(tpe.resType)

if sym.name.startsWith(nme.UNARY_PREFIX.toString) then
def isUnaryPrefixName(name: Name) = name match
case name: SimpleName =>
name.startsWith("unary_") && nme.raw.isUnary(name.drop(6))
case _ =>
false

if isUnaryPrefixName(sym.name) then
if sym.is(Extension) || sym.name.is(ExtMethName) then
// if is method from `extension` or value class
checkExtensionParameters(sym.info)
Expand Down
10 changes: 6 additions & 4 deletions docs/docs/internals/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,8 @@ Expr1 ::= [‘inline’] ‘if’ ‘(’ Expr ‘)’ {nl} Expr [[
| ‘return’ [Expr] Return(expr?)
| ForExpr
| [SimpleExpr ‘.’] id ‘=’ Expr Assign(expr, expr)
| SimpleExpr1 ArgumentExprs ‘=’ Expr Assign(expr, expr)
| PrefixOperator SimpleExpr ‘=’ Expr Assign(expr, expr)
| SimpleExpr ArgumentExprs ‘=’ Expr Assign(expr, expr)
| PostfixExpr [Ascription]
| ‘inline’ InfixExpr MatchClause
Ascription ::= ‘:’ InfixType Typed(expr, tp)
Expand All @@ -235,7 +236,8 @@ InfixExpr ::= PrefixExpr
| InfixExpr id ‘:’ IndentedExpr
| InfixExpr MatchClause
MatchClause ::= ‘match’ <<< CaseClauses >>> Match(expr, cases)
PrefixExpr ::= [‘-’ | ‘+’ | ‘~’ | ‘!’] SimpleExpr PrefixOp(expr, op)
PrefixExpr ::= [PrefixOperator] SimpleExpr PrefixOp(expr, op)
PrefixOperator ::= ‘-’ | ‘+’ | ‘~’ | ‘!’
SimpleExpr ::= SimpleRef
| Literal
| ‘_’
Expand All @@ -250,8 +252,8 @@ SimpleExpr ::= SimpleRef
| SimpleExpr ‘.’ MatchClause
| SimpleExpr TypeArgs TypeApply(expr, args)
| SimpleExpr ArgumentExprs Apply(expr, args)
| SimpleExpr1 ‘:’ IndentedExpr -- under language.experimental.fewerBraces
| SimpleExpr1 FunParams (‘=>’ | ‘?=>’) IndentedExpr -- under language.experimental.fewerBraces
| SimpleExpr ‘:’ IndentedExpr -- under language.experimental.fewerBraces
| SimpleExpr FunParams (‘=>’ | ‘?=>’) IndentedExpr -- under language.experimental.fewerBraces
| SimpleExpr ‘_’ PostfixOp(expr, _) (to be dropped)
| XmlExpr -- to be dropped
IndentedExpr ::= indent CaseClauses | Block outdent
Expand Down
6 changes: 4 additions & 2 deletions docs/docs/reference/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,8 @@ Expr1 ::= [‘inline’] ‘if’ ‘(’ Expr ‘)’ {nl} Expr [[
| ‘return’ [Expr]
| ForExpr
| [SimpleExpr ‘.’] id ‘=’ Expr
| SimpleExpr1 ArgumentExprs ‘=’ Expr
| PrefixOperator SimpleExpr ‘=’ Expr
| SimpleExpr ArgumentExprs ‘=’ Expr
| PostfixExpr [Ascription]
| ‘inline’ InfixExpr MatchClause
Ascription ::= ‘:’ InfixType
Expand All @@ -232,7 +233,8 @@ InfixExpr ::= PrefixExpr
| InfixExpr id [nl] InfixExpr
| InfixExpr MatchClause
MatchClause ::= ‘match’ <<< CaseClauses >>>
PrefixExpr ::= [‘-’ | ‘+’ | ‘~’ | ‘!’] SimpleExpr
PrefixExpr ::= [PrefixOperator] SimpleExpr
PrefixOperator ::= ‘-’ | ‘+’ | ‘~’ | ‘!’
SimpleExpr ::= SimpleRef
| Literal
| ‘_’
Expand Down
9 changes: 9 additions & 0 deletions tests/pos/i13282.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class Ptr[T](var value: T):
def `unary_!` : T = value
def `unary_!_=`(value: T): Unit = this.value = value
end Ptr

def test =
val x = Ptr(9)
!x = 10
println(!x)
6 changes: 6 additions & 0 deletions tests/pos/unary-eq.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
final class Baz private (val x: Int) extends AnyVal {
def `unary_!_=`() : Baz = ??? // parses ok, but will not be usable
def `unary_~_=`() : Baz = ??? // parses ok, but will not be usable
def `unary_+_=`() : Baz = ??? // parses ok, but will not be usable
def `unary_-_=`() : Baz = ??? // parses ok, but will not be usable
}