Exponentiation operator and precedence group

I do buy the idea that binary operators should uniformly precede unary operators, because it respects a clear separation between syntactic analysis and semantic analysis. (It might even be possible to convince me that independent precedence weighting of different infix operators should be removed from the language, but I don’t think about it because something that breaking is obviously so far from ever being on the table.)

The direction of this thread seemed to be about a perceived need to break that axiom. Do I want to break it? Not really. But if we have do, then I would rather do it in a consistent and useful way.

My real preference would be to just let -x**y play like Swift and not try to make it play like math. It is already both bad math (−xy) and bad Swift (-x ** y according to any style guide I have ever seen). Order of operations isn’t a property of the operations, it’s a property of the notation. Despite the way we often say it, exponentiation does not precede multiplication in any inherent way. Rather, in western mathematical notation, superscripts precede juxtaposition. If I said, “I got two apples and four bananas twice,” you probably wouldn’t assume I received a total of two apples and eight bananas, because even though it is math, it is expressed in English, not mathematical notation, and so the multiplication operation doesn’t come first. Since Swift owns the notation here, not math, I am perfectly fine with -x**y being governed by prefixes preceding infixes.

7 Likes

What you are proposing is, I think, an appropriate level of abstraction. I can imagine the possibility of making this more abstract and more intricate, but I can't see that the higher complexity would solve any problem we actually have.

If that were the only distinction, I'd agree 100%. However, there's also a distinction about making it "play like" Python (for example) — even allowing that Swift isn't really responsible for Python's choices.

What I mean is that most modern languages have basically the same arithmetic operator grammar. Having something like -a + b change meaning across language boundaries would be pretty bad. Having something like -a ** b change meaning across language boundaries isn't as bad, but still seems like it's worth avoiding.

FWIW, the math notation –xy might be "bad" stylistically, if you choose to think so, but it's not ambiguous in any way. It cannot mean (–x)y.

I meant that is what math dictates it should be instead. Math doesn’t use **, and Swift prefers spacing for infix operators. -x**y isn’t “rightest” for either system.

Most of the computer languages mentioned have a small, finite set of operators, so each operator can play by its own rules. It’s easy for syntax tree parsers and easy for users. Swift on the other hand has an infinite amount of definable operators. For the sake of both syntax tree parsers and users, it seems better to me to have a consistent, extensible pattern that they all play by. We make an effort to distinguish = from the term operator precisely because it doesn’t follow that pattern.

But as long the solution doesn’t interfere with the rest of the operator repertoire, it doesn’t matter all that much to me what the decision ends up being. We are talking about an operator I will never use and a diagnostic I will never see.

I've created a toolchain to test out the proposed behavior:

I can't figure out how to get Xcode to stop displaying (without enforcing) diagnostics from the unrelated shipping toolchain, which causes lots of noisy errors that don't actually reflect the toolchain in the PR.

One issue which presents another wrinkle into the discussion is that Swift actually lexes the minus sign in front of a literal value as part of the literal, not as a prefix operator. So, with all of our discussions on @Joe_Groff's -2**4 example, Swift literally sees only one operator. (This is not an insurmountable problem, but it does require a bit of a fudge.)

3 Likes

I see the proposed associativity is right. That’s certainly better than left, but have we considered none?

From a clarity-when-reading standpoint, sequential exponentials are non-obvious without parentheses.

This expression is immediately clear and unambiguous:

4 ** (3 ** 2)

Whereas this one is easily misunderstood:

4 ** 3 ** 2

A right-associative precedence group accords with behavior in other commonly used languages; more likely is that people won’t be sure of the order of operations than that they would confidently misunderstand.

(Indeed, if even an operator with historical precedent as this does shouldn’t be right-associative, then it argues against any new or custom operator having right associativity.)

The issue with a non-associative operator with the additional rules above about prefix operators is that, if you’re chaining operators (which is where the non-associativity part comes in), things become quickly ridiculous:

((-x) ** ((-y) ** ((-z) ** a)))
// All mandatory parens

The ergonomics of this would be poorer than just pow, at which point, why bother?

1 Like

I guess you’re right, forcing people to use extra parentheses is not a good thing. We have to trust programmers to write code in a readable way.

That means associativity should be right, and it also means the rules for unary operators should stay exactly as they are.

We’re not going to stop people from writing ugly code, and we shouldn’t penalize people who are trying to write good code, in a futile attempt to prevent ugly code.

Heck, even if the code looks nice, that still doesn’t mean it behaves as it appears to. Sure, you and I both know what the caret operator ^ does, but someone new to programming could easily write things like this:

5^3 + 6^3 == 12^3       // true
(5^3) + (6^3) == (8^3)  // true

• • •

If we had some way for IDEs to display exponents as superscripts, then it would make sense to use postfix precedence for, eg, -xy.

But we don’t, and that’s not being proposed here. So let’s leave the rules for operators alone, and consider ** on its own merits within the existing design for Swift operators.

2 Likes

In practice, though, nested exponentiation is extremely uncommon. There's a few common integer sequences (Fermat numbers, Double Mersenne numbers, number of k-ary boolean functions, uh, probably a few more?) but in practice all of those are better expressed as a shift (or some other special mechanism). In 20+ years of working on numerical computation, I can count the number of times I've seen a nested exponential in code on one hand, and some parentheses really wouldn't have caused any problem.

7 Likes

+1 from me regardless of associativity. I've defined this a couple times myself so it would be nice to no longer have the need.

I don't doubt it! But since associativity only comes into play here when exponentiation is nested, only the nested exponentiation scenario comes into play within the context of discussing what associativity the operator should have; its relative rarity doesn't change the calculus.

My point here is, if exponentiation--which is right associative in many (all?) other programming languages in accordance with mathematical custom--can't be made right associative without being confusing in Swift, could there be any new operator that would pass that bar? Or is the argument essentially that we shouldn't allow right associative operators anymore, basically?

I'd be totally fine with that set of rules.

I think you misconstrue the issue about unary operators though: it isn't at all to do with "preventing ugly code" but preventing inadvertently incorrect code. The argument against allowing -2 ** 4 isn't that people will find it ugly (although you may), it's that people who find it perfectly beautiful will also vehemently disagree as to what this beautiful (to their eyes) expression means.

And they would be encountering this issue not out of general inexperience with programming (as may be the case for someone who misuses ^ as you write) but precisely because they are an experienced and polyglot programmer. That would be...unfortunate not to prevent if we could help it, all else being equal.

I do suggest you give the toolchain a try to get a sense of the user experience (to get a true sense of things as they will be once exponentiation is fully implemented, add your own func ** ... { return pow ... } with that toolchain): I think it's a workable model (particularly since it's been proven out in JavaScript) and preserves Swift's general design for operators.

1 Like

After really digging into Swift's (sometimes unstated) operator precedence rules, I think we can really put to rest any possibility of parsing -x**y as -(x**y) in Swift. There are several reasons, which I will collate together:

  1. Swift's hierarchy places unary operators above all binary operators; this is a considered decision based on precedent in other C family languages and users have voiced that they intuitively expect this behavior (already discussed extensively on this list).

  2. Even if we break with (1), Swift lexes a negative number literal as a single literal, not as an operator in front of a positive number literal. That is to say, making ** have higher precedence than prefix - would cause let x = 2; let y = -x**4 to behave differently from let y = -2**4, because no precedence rule expressed in Swift would change that -2 is lexed as a literal value. So, we would have to "fudge" how Swift lexes literal values for ** to make -2**4 equivalent to -(2**4), which seems...unprincipled.

  3. Even if we break with (1) and (2), Swift has special rules for operators such as as. For instance, 2 as Int ** 2 means (2 as Int) ** 2 and never 2 as (Int ** 2). Therefore, -2.0 ** 4 and -2.0 as Double ** 4 would behave differently from each other if we fix (1) and (2), which is a delightfully inexplicable result (because - is higher than as, which is higher than **, but - is lower than **). I don't think this has been brought up before here.

All of this is to say that it's not only the users, but also Swift's grammar itself in various ways, which assume that no binary operator has higher precedence than a unary operator. Actually implementing an operator that breaks those rules would have all sorts of knock-on effects that are hard to explain.

I do think that warning about such use though (as in JavaScript's model and as I've prototyped in the PR toolchain) is a workable model.

7 Likes

If the warning is restricted to only the case where there is no whitespace around the exponentiation operator, then I suppose this is tolerable.

-x ** y” should never warn. It parses exactly as it looks.

-x**y” could warn, I guess, but I’m not convinced it’s really worthwhile.

The point here is that what constitutes "parses exactly as it looks" means diametrically different things to you and to other people (including anyone coming from a great number of languages more commonly used than Swift): it's precisely this case that needs diagnostics.

Where the notation is ambiguous, people wouldn't be so certain of how they expect it to parse. Again, the purpose of this is to eliminate the "parses exactly as it looks" scenario. It's not about preventing people from writing ugly code, but from writing pretty code that means different things to different people.

Again, to make it plain, all of this design work is to address the following premise: There will be people who will see -x ** y and confidently know it behaves as it does in Python and other languages, parsing the expression as -(x ** y); this represents a problem that must be addressed. If you accept this premise, then there are only three possibilities:

(1) do not introduce ** into the language at all;
(2) introduce ** but forbid the scenario in question from arising with diagnostics; or
(3) introduce ** but eliminate the mismatched expectations by aligning behavior with that of Python and other languages.

Option 1 is the status quo; option 2 is the design I've worked on in the PR toolchain; option 3 is what I've just described as impossible given the direction of Swift. That is an exhaustive list of options.

Alternatively, you can reject the premise, which is fine :man_shrugging:

In that case, I’m opposed to any warning at all here.

Swift already has rules for operator precedences. People already must learn those rules. The rules are simple, clear, and consistent.

Let’s not muddy things up.

2 Likes

Well, yes, Swift has rules. They are definitely not simple, clear, and consistent rules, but they're rules. The design proposed for adding the diagnostics I describe is not ad hoc (nowhere in the compiler does it look for a hardcoded **), but based on the addition of another rule that can be reused for custom operators, so fortunately it doesn't "muddy things up." :slight_smile:

We can debate whether it's the best rules-based solution to make the diagnostic a reality, and we can debate whether the problem needs to be addressed. It sounds like you're of the opinion that we don't need to deal with the problem in the first place--again, fine, but there are others here who want it dealt with, so there needs to be an exploration of how that might be done.

Postfix, then prefix, then infix in decreasing order of precedence, seems pretty straightforward to me.

If only it were that simple:

  // E.g. 'fn() as Int << 2'.
  // In this case '<<' has higher precedence than 'as', but the LHS should
  // be 'fn() as Int' instead of 'Int'.
  // If the left-hand-side is a 'try' or 'await', hoist it up turning
  // "(try x) + y" into try (x + y).
  // If this is an assignment operator, and the left operand is an optional
  // evaluation, pull the operator into the chain.
  // If the right operand is a try or await, it's an error unless the operator
  // is an assignment or conditional operator and there's nothing to
  // the right that didn't parse as part of the right operand.
  //
  // Generally, nothing to the right will fail to parse as part of the
  // right operand because there are no standard operators that have
  // lower precedence than assignment operators or the conditional
  // operator.
  //
  // We allow the right operand of the conditional operator to begin
  // with 'try' for consistency with the middle operand.  This allows:
  //   x ? try foo() : try bar()
  // but not:
  //   x ? try foo() : try bar() $#! 1
  // assuming $#! is some crazy operator with lower precedence
  // than the conditional operator.
  // If the operator is a cast operator, the RHS can't extend past the type
  // that's part of the cast production.
6 Likes

I’ve been thinking about this further, and I still feel that this pitch should not introduce the warning we’ve been discussing.

However, it might be worthwhile to consider a more general proposal for warnings on confusing uses of operators.

The example I posted earlier is illustrative.

This expression:

5^3 + 6^3

Parses like this:

5^(3 + 6)^3

And not like this:

(5^3) + (6^3)

So perhaps there could be a warning when an operator surrounded by whitespace has higher precedence than an adjacent operator with no whitespace.

6 Likes

I like this idea! I also agree with you that it's orthogonal to this topic, as it does not solve the issue regarding -2 ** 4 meaning different things to different people, which does not involve an operator of higher precedence. (In fact, what you mention could probably reasonably be implemented as a QoI measure without going through Evolution.)

2 Likes