Skip to content

Parse unary operators as regular identifiers when followed by square brackets #14299

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

Closed
wants to merge 1 commit into from

Conversation

adampauls
Copy link
Contributor

Currently, +(6) parses as a unary + on the expression 6 and +[Int](6) does not parse at all because [Int](6) is not the legal start of an expression. This PR makes +[Int](y) parse as regular apply by removing [ from the set of tokens that can follow a unary operator. I believe this is safe because although [ can begin an expression, it can only begin a typed lambda like [T] => T => Int, and a + could not parse as a unary operator on such an expression in any case. This also happens to be the behavior of Scalameta. I'm not sure which behavior is correct but I believe there is no reason to disallow this behavior.

@som-snytt
Copy link
Contributor

Clever tweak!

Copy link
Contributor

@odersky odersky left a comment

Choose a reason for hiding this comment

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

What is the reason for the change? I guess we all agree that a + operation as in the test is very bad style. Why encourage it with the change to the parsing rules?

I find it also concerning that

+(6)

and

+[Int](6)

now are parsed differently. This goes counter to the intuition that type arguments are optional.

@adampauls
Copy link
Contributor Author

adampauls commented Jan 21, 2022

My team is building a DSL on top of Scala. We are using Scalameta to parse right now and it has this behavior. I thought it was harmless to include, and possibly even intended given Scalameta's choice.

The ambiguity around unary ops in Scala is already pretty surprising -- most on my team are very surprised that +(1,2) parses as unary plus on a tuple. I don't think this change makes things much worse, while at least enabling something that is compilable but unparseable now.

Note that right now, if you define the + function as above, you can't even invoke it with a type param if you put back ticks around +. I.e.

def +[T](x: T): String = "x"
`+`[Int](5)

does not parse.

If such a function can be defined but not invoked, it seems better to disallow them altogether.

In general, I think it would be nice if unary operators were more like .apply() sugar: resolved by the type checker and not the parser. So +(1,2) could compile as a call to a two arg function if no unary plus that can accept a tuple is in scope. As I'm sure you're aware,

class Foo {
  def apply(x: (Int, Int)): String = "5"
}

def foo(x: Int): Int = 5
locally {
  val foo = new Foo
  foo(5, 6): String
}

compiles just fine, but fails to compile if val foo is commented out. Resolving the ambiguity on unary ops this way is of course way outside the scope of this PR (and possibly much more difficult than I think), but I thought this PR was a small step in the right direction.

@som-snytt
Copy link
Contributor

There is an historically very consistent expectation that backticks should do something here. ("Neutralize" unary syntax? Un-unary?) (The ticket for things named * also used backticks IIRC so it's not just unary op names.)

The idea to defer interpretation of +(e) is interesting. But down that path, +m could be postfix +.m which is not a simple expression.

"can be defined but not invoked" says too much, because you can always apply.

@odersky
Copy link
Contributor

odersky commented Jan 21, 2022

scalameta is definitely not the canonical arbiter of Scala syntax. Unary + is simply a prefix operator. To make it compile to a regular method call in some cases is more confusing than helpful. So I am against.

There is one legitimate use case that I can see: To invoke an extension method in direct style. Say you have

object obj:
  extension (x: T) def + (y: T)

Then you want to be able to call + directly. But you can already do that, like this:

obj.+(a)(b)

@adampauls
Copy link
Contributor Author

adampauls commented Jan 21, 2022

I did not mean to imply that scalameta is authoritative, just that it was indication that this area was at least not clearly specified. I think you'll agree that that the situation with + is not so clear as you state: + is not simply a prefix operator because it can also be an infix operator, just like all dot-callable functions can be. And if it were just a unary operator, why would I have to declare def unary_+ and not just def +? And why would one even permit a top-level def +[T](x: T, y: T) if it's (almost) impossible to invoke?

Happy to close the PR since this is a very minor point for us. IMHO it would be more inline with my expectations that backticked + would act as a syntactically unprivileged identifier (agreeing with @som-snytt), and that would remove any concerns about def + being definable but (nearly) uninvocable (thanks to @som-snytt for pointing out you could still invoke it with apply).

@som-snytt
Copy link
Contributor

worth adding that scala 2 is also not authoritative, because it accepts infix type args out of spec, but it does accept the syntax under discussion, which is within spec, or "within tolerance" as the engineers say, which for us means our tolerance for the weird syntax:

scala 2.13.8> object + { def apply[A](n: Int) = 42 }
object $plus

scala 2.13.8> +(1)
val res0: Int = 1

scala 2.13.8> +[String](1)
val res1: Int = 42

scala 2.13.8> object X { def unary_+[A] = 42 }
object X

scala 2.13.8> +X
val res2: Int = 42

scala 2.13.8> +[String]X    // just checking
               ^
              error: missing argument list for method apply in object +
              Unapplied methods are only converted to functions when a function type is expected.
              You can make this conversion explicit by writing `apply _` or `apply(_)` instead of `apply`.
                       ^
              error: postfix operator X needs to be enabled

@adampauls
Copy link
Contributor Author

Maintaining backwards compatibility where possible is a reasonable argument in favor?

@odersky
Copy link
Contributor

odersky commented Jan 22, 2022

Maintaining backwards compatibility where possible is a reasonable argument in favor?

Yes, but it needs to be weighed against the argument that Scala 3 is about cleaning up the language. So backwards compatibility is not an absolute here. I still think that accepting the syntax was a bug, and Scala 3 fixed it. I can already see the puzzler in my head that would exploit the Scala 2 behavior.

@SethTisue SethTisue changed the title Parse unary operators as regular identifiers when followed by square backets Parse unary operators as regular identifiers when followed by square brackets Jan 22, 2022
@adampauls adampauls closed this Jan 22, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants