Precedence Rules And De-Facto Naming
In our language we allow programmers to define whatever operators they like, and we allow operator overloading. This lead us to an interesting situation where we noticed that if the precedence is defined per overload, then parsing expressions is either undecidable, or you need some disambiguating strategy that's almost certainly not something the programmer can predict. Our solution was to try mostly flat precedence, and we found that worked very well for the most part. We had to make some exceptions to make the language's notation read properly. For example, we noticed that indexing into structs or arrays was awkward because of situations like this:
a + foo.bar@2
. and @ are our de-facto struct and array indexing operators, and we found that we intuitively wanted those to parse in such a way that you can treat things like foo.bar@2 almost like name refs. Like this:
(a + ((foo.bar)@2))
But with flat precedence rules you get this instead:
(((a + foo).bar)@2)
This lead us to the insight that precedence rules aren't about the order of operations as much as they are about having de-facto names bubble up out of the notation. Talking about precedence rules in terms of operators or eliding parens instead of de-facto naming effects seems like missing the forest for the trees.
You can see the de-facto naming effect in PEMDAS rules in the following examples:
x^2 + 1
a + bcd * e
It's common to use superscripts for exponentiation, which makes it immediately obvious that 2 in x^2 + 1 is part of a different concept in the expression, and thus should not be added to 1. Implicit multiplication does the same thing in the second example, where bcd is a tightly bound concept composed of b, c, and d, and should be treated that way regardless of whatever a and e are doing. Notably you can accomplish this same thing by controlling your whitespace usage, for example:
a + b*c*d * e
But this isn't nearly as strong as having superscripts or implicit multiplication, both of which are fairly awkward to do in most programming languages. Some artifact of how our editors work? Something to do with how precisely we need to manipulate code? Correctly parsing identifiers is a big issue with implicit multiplication. I'm unsure all of the reasons for why this seems so hard to do, but the de-facto naming effects you can get from PEMDAS don't seem nearly as useful as they could be. Also worth noting is that in many languages you're rarely doing algebra or calculus in code, but instead you're importing the results of algebra and calculus into your code. This domain mismatch also seems relevant. Regardless, my hypothesis is that, modulo any competing concerns, the precedence rules you use should be optimized, by whatever metric is important for your domain, to maximize the de-facto naming effect. I don't have any reasonable way to test my hypothesis, of course, but I'd be interested to hear if any of you have any insights on this line of thinking.
Posted Jun 14, 2022, 6:26 PM UTC · edited
I'll never understand the obsession against operator precedence this sub seems to have. Pick something that makes doing common things ergonomic. Such as giving
.a higher precedence than other operators. Yes, you can do that. No, it's not a crime.We don't object to precedence, which we noted from the start when we decided on mostly-flat precedence. Our rule is that, with the exception of classic comparison operators, like
==, any operator that ends in=has lowest precedence and binds right-to-left. That way things like+=or>>|>>=just work like they should.My point in talking about this is the same reason why people talk about love rather than talking about chemical processes in the brain: Knowing how a mechanism works doesn't mean you understand its consequences.
It's because parsing with operator precedence is kind of a pain in the neck to implement and programming language designers love simplicity and flexibility of design. It's this mindset of "if I just make users do <X>, then it makes my job much easier and it would let users define their own syntax, and everyone likes defining new syntax, right?" I've been guilty of this myself in the past, but now I try to focus more on "what is the most intuitive/convenient thing for the user?" and bend over backwards to make the compiler do that, even if it's inelegant from an implementation perspective.
There's also a totally fair and legitimate complaint that some operator precedence is confusing and leads to bugs when people have no preconceptions about relative precedence between operators. For example, bitwise OR vs. arithmetic add vs. modulus (
a | b + c % d). And in those cases (i.e. operators other than PEMDAS, comparisons, and boolean logic) it does make sense to use a simpler set of rules like "the compiler will tell you to use more parentheses."If you manually write the recursive descent layer for each precedence you will face an incredibly bad time yes. But pratt parsing to the rescue, it really doesn't need to be elaborate:
https://gitlab.com/xmdr/ananke/-/blob/master/prototypes/pratt.py