Readit News logoReadit News
teo_zero · 2 years ago
Please note that, despite the misleading title, TFA is about the precedence of bitwise operators & and | with respect to the comparison operators (== and all the others). In particular it was asked to Ritchie why the former are weaker than the latter in C, a rule generally considered a mistake because it makes it impossible to write common expressions like

  if (addr & mask == 0)
without adding extra parentheses:

  if ((addr & mask) == 0)
TFA is not about the relative precedence between & and | (which is well understood, although disputed, and has mathematical basis), or between && and || (which mimics & and |). And TFA is not even about the precedence of the logical operators && and || vs. the comparison operators.

I'm writing this because I've seen a lot of comments that, while interesting in themselves, completely misunderstood the premise.

goto11 · 2 years ago
Thank you for this additional context.

So in short, bitwise operators have lower precedence than comparisons to allow you to write:

    if (a==b & c==d) ...
but of course, this means you can't write bitwise checks like this:

   if (addr & mask == 0) ...
The problem could theoretically have been solved when the shortcut operators were introduced, by increasing the precedence of & and | to be higher than comparisons, but have the shortcut operators be lower. So you would be able to write both:

   if (a==b && b==c) ...

   if (addr & mask == 0) ...
But this was not done due to concerns of backward compatibility with existing code, since now every expression using the old pattern would subtly change semantics. E.g. the first example would now be parsed as:

  if ((a==(b & c))==d) ...

MaxBarraclough · 2 years ago
> you could write [...] but of course, this means you can't write

I found this rather difficult to read. You could write those expressions. They're legal C code. Whether they will have the expected semantics will depend on, well, what you expected.

The more general problem is code that relies too heavily on precedence rules in the first place. Precedence-related bugs and readability issues are easily avoided, just use parentheses. As I mentioned in another comment in this thread, some languages force the programmer to do this.

leereeves · 2 years ago
Why was it important to allow you to write:

    if (a==b & c==d)
I always thought using the bitwise operator as if it were a logical operator was simply a mistake, even though it works because false is 0 and true is 1.

Edit: Mea culpa for reading and responding to the comments before the article.

smcameron · 2 years ago
> && and || (which mimics & and |)

I've always thought of it as mimicking * (multiplication) and + (addition), and you can actually use * and + in place of && and || if you wanted to (not advisable, though the idea can be used in expressions to produce branchless code, common technique in GPU shader code). I used to use this trick in TI99/4a BASIC which lacked "AND" and "OR" in "IF" statements, but because boolean expressions evaluated to integer 1 and 0 for true and false, multiplication and addition could serve as AND and OR.

teo_zero · 2 years ago
But remember that * and + don't short-circuit. This won't work:

  found = (p != NULL) * (*p ! = 0);

nathan_douglas · 2 years ago
To be fair, Dennis says:

>The priorities of && || vs. == etc. came about in the following way.

I think it takes some serious concentration to tease out that he is indeed trying to explain what you indicated. Either that, or my brain is just thoroughly cooked.

I'm not arguing with you -- I think you're absolutely right -- but man, Dennis made it hard on us here

Sohcahtoa82 · 2 years ago
I don't even know the priority between && and ||. If I'm using both, I use parentheses just so it's more explicit. "a && b || c" should flag a linter, IMO, with "(a && b) || c" or "a && (b || c)" being required.
o11c · 2 years ago
Convert them to arithmetic. If you ignore the casting,

  a && b   is just   a * b
  a || b   is just   a + b
Now you remember the precedence between them (except in broken languages, of which the only notable one is shell).

anonnon · 2 years ago
> except in broken languages, of which the only notable one is shell

All binary messages in Smalltalk (messages with selectors consisting of punctuation, like !@+-, including punctuation sequences like "+-&|", if you want) have the same precedence, and the keyword variants (which short-circuit, using block arguments) and: and or: also have the same precedence (one level lower than the binary/punctuation messages), but because they take block arguments, are usually disambiguated:

    a | (b & c) "parens needed to ensure b & c is evaluated first"
        ifTrue: [self doSomething]
        ifFalse: [
                "because the and: is sent in a block arg to or:, it won't be evaluated unless or:'s receiver is false"
                (self hasPendingTask or: [self updateTaskQueue and: [self hasPendingTask]])
                        ifTrue: [self processTask]]
Smalltalk is extremely elegant, powerful, and simple. 6 reserved words, 3 levels of operator precedence, and not much syntax to learn. It's what r5rs Scheme should have been.

dietrichepp · 2 years ago
My problem here is that I distinguish between multiplication and addition by seeing which one distributes over the other.

  a * (x + y) = (a * x) + (a * y)
However, the reverse doesn’t work…

  a + (x * y) =? (a + x) * (a + y)
Why is this a problem?

  a ∧ (x ∨ y) = (a ∧ x) ∨ (a ∧ y)
  a ∨ (x ∧ y) = (a ∨ x) ∧ (a ∨ y)
Both are true. So, who are we to say that one corresponds to multiplication, and the other corresponds to addition? The two operations are too similar to each other.

dataflow · 2 years ago
Also if you forget which is multiplication and which is addition, remember false is 0 and true is 1, then work it out from there: a * b is only nonzero if both of them are nonzero, so it's AND.
threatofrain · 2 years ago
Isn't it x ^ y which is like x + y?
dananabread · 2 years ago
Is there an intuition for this correspondence otherwise I don't think it's very helpful
CamperBob2 · 2 years ago
No. Then I have to think about it.

I have (gently) jumped on peoples' cases for not using parentheses in expressions involving these operators, and will continue to do so, thanks.

hnlmorg · 2 years ago
Depends on the shell. Murex, for example, follows order of precedence correctly

Deleted Comment

ksherlock · 2 years ago
0x8000 && 2 != 0x8000 * 2

-1 || 1 != -1 + 1

0x8000 || 0x8000 != 0x8000 + 0x8000

(with a 16-bit integer, adjust accordingly for larger word sizes)

msla · 2 years ago
> except in broken languages, of which the only notable one is shell

The array language people (APL, J, K) are going to come in and protest, but you aren't going to be able to understand them.

/s

tom_ · 2 years ago
1+1=1? My maths education was a long time ago, but I triple checked with my calculator, and I'm uncertain this is quite right.

My own mnemonic: SAXO. Shift, And, Xor, Or. (Like a real Saxo, it's fun to go a bit faster like this, but you do have to trust everybody involved, because any accident is probably going to end up badly for you.)

And now you know the bitwise precedence as well! And this ordering actually works out tidily for common operations: "x=a<<sa|b<<sb|c<<sb"; "x=p>>n&m"; "x=x&~ma|a<<sa"; and so on. You do need brackets sometimes, but fewer than you'd think, and it helps the unusual cases stand out.

(Main annoying thing: a lot of compilers, even popular ones such as clang and gcc, don't actually seem to know what the precedence rules actually are, and generate warnings asking you to clarify. Presumably the authors didn't realise that C has an ISO standard, that can be consulted to answer this question? Very surprising.)

MaxBarraclough · 2 years ago
The Ada language requires explicit parentheses in the case of a succession of different logical operators precisely to avoid precedence-related bugs. [0]

In the Pony language, any expression where more than one infix operator is used must use parentheses to remove the ambiguity. [1]

The Zig language considered following Pony, but didn't. [2]

[0] http://archive.adaic.com/standards/83rat/html/ratl-03-06.htm...

[1] https://tutorial.ponylang.io/expressions/ops#precedence

[2] https://github.com/ziglang/zig/issues/114

pi-e-sigma · 2 years ago
Ada is such an underrated language. The lack of its adoption is a testament to the old adage that technologies don't thrive or die on their technical merits.
MrBuddyCasino · 2 years ago
If I read this right, Zig follows Pony partially. The very basic stuff like addition and multiplication follows the well known precedence rules („chainable“), but eg bitwise operators whose precedence nobody can remember must be put into parentheses.

I find this a good compromise between being explicit and readability (Lisp syndrome).

amelius · 2 years ago
What about using the same associative operator multiple times?
User23 · 2 years ago
You’re in good company[1]:

  Do not introduce priority rules that destroy symmetry. I remember how much more pleasant the predicate calculus became to work with after we had decided to give con- and disjunction the same binding power and thus to consider p ∧ q ∨ r an ill-formed formula.
[1] https://www.cs.utexas.edu/users/EWD/transcriptions/EWD13xx/E...

Terr_ · 2 years ago
> We don’t want to baffle or puzzle our readers, in particular it should be clear what has to be done to check our argument and it should be possible to do so without pencil and paper. This dictates small, explicit steps.

I feel this justifies why I sometimes write code with lots of temporary variables on their own lines: It's a way to break down the problem, to name each thing (especially when it's not a simple method-call to a correctly-named method) and it also makes it easier to verify the behavior with the average line-by-line debugger or line-by-line debugging easier.

In many cases, such temporary local variables have no negative performance impact, being optimized away.

lifthrasiir · 2 years ago
It should be noted that this is not same to using a default order (typically, but not necessarily, left-to-right) in such case. Some languages use a term "non-associative" instead.
thomasahle · 2 years ago
This article is a joy to read.
JohnFen · 2 years ago
I know the priorities of these, but I still use parentheses to make the precedence explicit. I think it's just great practice -- and it makes things easier for the next dev who might have to work with the code.
JonChesterfield · 2 years ago
Consider (&& a b) and (|| a b) for no uncertainty over operator precedence and easy variadic representation (&& a b c) at the cost of zero additional parentheses.
tasty_freeze · 2 years ago
... at the cost of having parentheses around every subexpression.
namrog84 · 2 years ago
While true and might be easy to read in the example context of a b c I'd imagine having the && separator in real world conditions to be far more readable and clear for vast majority if people
NegativeLatency · 2 years ago
Most, and possibly all languages I've used day to day do not have prefix operators like that.
torstenvl · 2 years ago
I hate Excel for this very reason.
ant6n · 2 years ago
Prefix operators considered harmful for readability.
moritzwarhier · 2 years ago
It bothered me a while for "short" boolean expressions to have a linter expect parens, where I'd simultaneously expect everyone with a high school degree to understand the implicit operator precedence.

Then, since my first bug having been merged that was caused by a && vs || operator precedence mistake, I like my parens-always ESLint rule (or whatever exactly it was called).

Even redundant parens can start to look as pleasent as indents, after getting used to them.

In short, I think expressions like (a && b || c && d || e) are a footgun and linters should forbid them. Parens fit well with logical thinking, similar to relative clauses in language.

userbinator · 2 years ago
Sorry, but I'm of the exact opposite opinion. If you don't know, learn. It's not hard (as other sibling comments have noted). Code with superfluous parentheses is even more confusing, since I expect them to be present only when overriding precedence.

...and I just realised your username adds some additional irony.

sublinear · 2 years ago
Explicit is better than implicit. I think the real problem is not breaking up complicated expressions. If it's more than just a few pairs of parentheses making it hard to read there's something more wrong there.
mdbauman · 2 years ago
Although I agree, most times where I mix any of these without parentheses I end up having to explain it either in code review or when somebody does a `git blame` a couple months later. Rather than waste everyone's time explaining it, it's easier to just use the parentheses since it's what many/most people expect and everyone else can read it well enough.

Deleted Comment

Terr_ · 2 years ago
Agreed, it's quite justified: If their implicit combined behavior isn't easy-to-read-obvious to the author composing the code, then it's also problematic for any poor future-person (perhaps even the original author) tasked with reading and verifying the code. Checking parens is faster than remembering and applying the knowledge.

Plus the rules may be just different enough in different languages to trip up even those who try to remember them.

asimeqi · 2 years ago
I once implemented a logical expression evaluator to be used as filters on a search page. My boss tested it and filed a bug because he expected OR to have higher precedence than AND. I pointed him to a page with the rules and told him that he could use parenthesis if he wanted to modify the order of precedence. He was sort of convinced but never closed the ticket. Every release he would move it to the next release.
cm2187 · 2 years ago
I just asked myself the question for SQL's AND and OR this week. Apparently AND takes precedence but I don't know if it is consistent accross all SQL engines. In any case it seems to me that few programers will know anyway, so it is bad practice to not have parenthesis.
zzo38computer · 2 years ago
I also do (it also makes it more clearly, in my opinion), as well as with the bitwise operators & and | but sometimes with the bitwise operators the code is written in such a way that the precedence doesn't matter anyways and so I will not need to add parentheses anyways.
paulddraper · 2 years ago
|| is lower precedence than &&. I memorized that, just like I memorized that + is lower precedence than *.

There are some languages (e.g. shell) where they have the same precedence.

Prettier (JavaScript) adds parens like you suggest automatically.

buserror · 2 years ago
I do the same, not because I don't know the priorities, but because in my 35+ years career, I've seen so many times when cut+paste of partial expression leads to disaster just because it wasn't grouped properly with parenthesis..

Early in my career I erased several megabytes of shared memory on a mini computer, ie DOZENS of users worth of memory just because I was being 'clever' and cut+pasted bits of an expression the wrong way.

Since then, as a rule, sod the priorities, parenthesis it is...

torstenvl · 2 years ago
Do you know the priority in this?

[ -e "${target}" ] && echo "Getting information about ${target}" && stat "${target}" || echo "Failed to stat file."

EDIT: What about the || die("...") pattern in Perl?

Or the fact that the glyphs in || are visually lower than the glyphs in &&?

Or that "and" comes before "or" in the expression "and/or"?

pavlov · 2 years ago
Shell scripting sucks, but that’s hardly news.

If I really have to touch a bash script, I just assume nothing works like I might expect from proper programming languages despite any surface similarity, and google every construct with sweat drops dripping down my face. ChatGPT has made the process somewhat more tolerable.

PhilipRoman · 2 years ago
The shell command is very readable.

a && b && c is a common idiom for "stop if any step fails". In the shell language && and || are more like control flow concepts, rather than binary operators.

And a && b || c is a universal "ternary operator" (with one minor deviation) idiom across many languages, not just the shell

dehrmann · 2 years ago
I'm always surprised when people don't know this one because they're like multiplication and clamped addition.
nealabq · 2 years ago
Smalltalk doesn't have operator precedence rules. They just apply left-to-right. The infix operators aren't part of the language - they're messages (aka methods), and precedence would be inconsistent with the language's simple syntax.
dox_shepherd · 2 years ago
This is the best way in my opinion. Sacrifice a second of typing for clarity. I try to do this with expressions in general.
tialaramex · 2 years ago
One of the most intriguing novel ideas in Carbon (One of the "C++ Replacement languages" announced in 2022) is the idea that operator precedence should not be a total order.

Lots of prior languages have tried either:

1. No operator precedence, expressions must use parentheses so that it's very clear

2. No operator precedence, everything just happens left to right regardless

But Carbon says what if we do have precedence, but only between operators which programmers expect to have precedence - whenever it's unclear what should happen the compiler instead rejects that, the same way many languages won't let you ask whether 5 == "Five" because you need to explain WTF you intended as most likely you've screwed up and didn't realise they're completely different types.

djmcnab · 2 years ago
WGSL (the shading language for WebGPU) has something similar[1].

For example, `a + b << c` will fail to parse in a conforming implementation, as will `a << b << c` and `a && b || c`. Note however that `a && b && c` does parse. I find these rules to be well-thought through.

[1]: https://www.w3.org/TR/WGSL/#operator-precedence-associativit...

Jenk · 2 years ago
SmallTalk has the left-to-right for everything principle because everything is message passing and "operators" are just binary (as in two argument) messages which catches many out.

    2 - 1 * 5
will return 5, while many would expect -3. You must apply parentheses if you want a precedence other than ltr.

borrow · 2 years ago
Novel among mainstream languages, but not unseen before: https://blog.adamant-lang.org/2019/operator-precedence/
not2b · 2 years ago
The relative priorities of && vs ||, or & vs |, match the traditional precedence in logical expressions: "and" binds more tightly than "or", just as * binds more tightly than + ("and" is equivalent to * for one-bit values, and "or" is addition modulo 2). So I think that they got this correct.

However, the precedence of & vs &&, or & vs ||, etc is a source of problems.

ynik · 2 years ago
& vs && doesn't feel like a problem to me.

& vs == is the real problem: `(val & MASK) == CONSTANT` needs parentheses due to this mistake.

`a & (b == c)` essentially almost never makes sense (== gives you a boolean, and when dealing with booleans you can use && instead), yet that it what you get by default if omitting the parentheses.

nneonneo · 2 years ago
The example given in the post is that & used to be the only conjunction operator, before && was added. Therefore, it was normal to write “if (a==b & c==d)”. While this is definitely not used anymore, the historical context is useful for explaining why the precedence of & is so low.
User23 · 2 years ago
Off the top of my head I can’t think of anything that should bind more weakly than the equivalence.
not2b · 2 years ago
Yes, the latter issue comes up when the user meant to say && but types & instead.
Denvercoder9 · 2 years ago
> However, the precedence of & vs &&, or & vs ||, etc is a source of problems.

Do you have any examples of this? In my experience you almost always want the bitwise operators to have a higher precedence than the logical operators, as using the result of logical operators as a bitmask makes little sense. Consider e.g. `A & B && C & D`, which currently is equivalent to `(A & B) && (C & D)`, but with reversed precedence would be equivalent to the almost nonsensical `A & (B && C) & D`.

Now, the precedence of == with respect to & and | is actually problematic (as Ritchie admits as well). Having `A == B | C` interpreted as `(A == B) | C` is almost never desirable. For extra annoyance, the shift operators do have higher precedence than comparison, so `A == B << C` does what you usually want (`A == (B << C)`).

not2b · 2 years ago
It usually comes up if the user intended to write && and writes & at one point in an expression, the different precedence can produce an unexpected result. But gcc and clang have warnings for that.
c2occnw · 2 years ago
> match the traditional precedence in logical expressions

arithmetic expressions?

ynik · 2 years ago
Unfortunately even though this was called out as a mistake in 1982, much more modern languages (e.g. C#) are still copying it.

As painful as breaking changes might be, they beat the alternative of dealing with a bad design indefinitely. At least in more modern languages, the type checker usually catches the mistakes caused by this design mistake.

kibwen · 2 years ago
> much more modern languages (e.g. C#) are still copying it

Fortunately Go, Rust, and Swift all chose to defy C and fix the precedence of &, which I expect sets enough of a precedence (heh) for every language for the rest of human history to get it right.

wiseowise · 2 years ago
Python also fixed it.
o11c · 2 years ago
Somewhere, I sat down and wrote a combined precedence table across many languages. It was quite hairy; sometimes there's even variation across language versions.

Besides `not` (which often has different precedence depending on whether it's a keyword or a punctuator, and is a great reminder that multiple levels of pratt parsing is meaningful), `await` is the one with the most variation - in most languages, it binds tighter than binary operators, whereas in C++ it's just barely tighter than assignment.

Denvercoder9 · 2 years ago
> `await` is the one with the most variation [..] in C++ it's just barely tighter than assignment.

That doesn't seem right to me. According to cppreference[1] it's the operator with the third-highest precedence, well above assignment, which is the second-lowest.

[1] https://en.cppreference.com/w/cpp/language/operator_preceden...

o11c · 2 years ago
Hm, I looked at Wikipedia, and I'm pretty sure cppreference used to agree with it? Did it ever differ between drafts?

I've gotten surprisingly out of touch with the C++ world, even though it was my first serious language and I used to have the draft numbers memorized ...

naitgacem · 2 years ago
quite interesting. in VHDL we have a similar thing. `<=` is the assignment operator, however after an if, it means "less than or equal". I'll have to check it this was inherited from Ada.

Edit: on a quick glance, Ada uses the := as the assignment operator. However there seems to be only one version of the and, or, xor operators, used for both logical and bitwise.

zajio1am · 2 years ago
For languages that have strict distinction between booleans and integers (or other non-logical types) there is the natural priority of operations:

1) algebraic

2) relational

3) logical

From this, bitwise & and | are algebraic, so they should have higher priority than relationals.

One problem is !, which as a logical operator should have much lower priority.

Another problem is ==, which could be interpreted as both relational (when used on integers) or logical when used on booleans).