[Pitch] Enforce space between two different operators

Inspired by this comment:

This pitch does not introduce any new operators, but proposes a new compiler diagnostic.

Motivation

The following is valid syntax. It will check if the value exists, and if it does, assign a new value to it.

var x: Int? = nil
x? = 1 // Will be parsed as 'x' '?' '=' '1'
print(x) // nil
x = 2
x? = 3
print(x) // 3

However, it's also valid to omit all whitespace, which will cause the same thing to happen.

x?=1 // Will be parsed as 'x' '?' '=' '1'

Since it's possible to make a custom infix operator named ?=, it is surprising that the custom operator is not called in this case.

Proposal

Omitting whitespace between two operators should be disallowed.

x?=1 // Error: Operators must be separated with whitespace

A fix can be proposed, that inserts a space between the operators:

x? = 1 // OK

Existing operators will still work as before:

x ?= 1 // Currently parsed as 'x' '?=' '1', and is OK if this infix operator exists

Note that this diagnostic does not change behavior if a custom operator exists, and will not allow x?=1 in any case.

Currently, Swift already enforces consistent whitespace between operators, and this new error is similar.

x= 1 // Error: '=' must have consistent whitespace on both sides
x = 1 // OK
x=1 // OK
4 Likes

to take the optional wrapping example a bit further, consider additional levels of optionality:

var x: Int?? = nil

today each ? unwraps a 'single level' of optionality, so you can apply them repeatedly like:

x?? = 1
// or to use the motivating example:
x??=1

would the proposed rule apply space between each of those ? operators (aside: is 'operator' even the right term for them?)? doing so naively results in invalid syntax:

3 | x? ? = 1
  |      `- error: expected expression after '?' in ternary expression
4 | 

how would you expect that case to be handled? if the rule is 'space should be applied between different operators' then i think there's still a possible issue with these sorts of things:

x?!=1
x!?=2

are the optional unwrapping operators the only ones that have this issue (?, !), or are there others that exist or could be constructed?

2 Likes

The specific operator types that allow or disallow whitespace should be refined, I agree. x?? = 2 should remain possible.

I don't know which operators specifically allow to be combined without whitespace. It's possible that only unwrapping operators have this problem in the first place, in which case the diagnostic only applies to combining an unwrapping operator with a different type of operator.

I found x? = 1 syntax confusing and magical. Is it used often?

  • It does work with x? += 1
  • but not with x? + 1
  • but it does work with my own + operator:
infix operator +++ : AssignmentPrecedence

func +++ (lhs: Int, rhs: Int) -> Int { lhs + rhs }
var x: Int? = nil
let y = x? +++ 1 // ok, yields nil
  • but that's only if I use AssignmentPrecedence, otherwise it doesn't work.

If we didn't have it in the language already would we add it now?

It is the same behavior as found in optional chaining: foo?.bar = baz

The assignment is pulled inside the optional chain, so that expressions like foo?.bar += 2 will parse as foo?(.bar += 2) instead of failing to parse as (foo?.bar) += 2.

This behavior is controlled by the assignment property of an operator precedence group. When it is true the operator gets pulled into optional chains.

• • •

The fact that it works with “simple” optional chains (with nothing after the question mark) simply falls out of the definition: the assignment gets pulled into the chain regardless of how long the chain happens to be.

The “canonical” use-case for the simple version involves updating a dictionary value if the key is already present:

dict[key]? += 2
5 Likes

On the topic of diagnostics related to operator spacing, one thing which has been discussed before is the possibility of warning when a higher-precedence operator is surrounded by whitespace, while a nearby lower-precedence operator is not.

For example, the expression:

5^3 + 6^3

is parsed as

5^(3 + 6)^3

and not as

(5^3) + (6^3)

which is potentially confusing.

6 Likes

Yeah.. although . is not an operator..

Great example, thank you.

Nice. I was beaten by this very one whilst attempting introducing my ^ power operator. (Next thing I tried was using superscript ² and ³ as postfix operators – alas, these characters are only valid for identifiers.)

Having a warning (or even an error) in this case makes total sense. Any of existing linters already do this?

The rule could be something along the lines that "lower precedence operator should have at least the same number of whitespace characters (or higher) than the higher precedence one".