Operator priority bug in I6 compiler?

There might be a bug in operator priority in Inform 6 compiler, but I’d rather post about it here than on github/gitlab, in case it’s just my tired brain… (It seems odd to me that a bug like this, on operator priority / logical expression parsing, would have stayed in there for that long…)

These expressions should be, I think, logically equivalent :

(~~(location == Cove or Centralplaza) && lookmode == 1)
(~~(location == Cove || location == Centralplaza) && lookmode == 1)
((location ~= Cove && location ~= Centralplaza) && lookmode == 1)

But when I’m at the Beach location with lookmode = 2, the first two expressions are true, when they really shouldn’t. (The DM4 seems to endorse that usage, since page 484 there is “if (~~stop) && digits <= 3)”).

And then when I try permuting the order:

(lookmode == 1 && ~~(location == Cove or Centralplaza))
(lookmode == 1 && ~~(location == Cove || location == Centralplaza))
(lookmode == 1 && (location ~= Cove && location ~= Centralplaza))

They’re all false, as they should be.

What’s going on?

The operator precedence table says that ~~, ||, and && all have equal precedence. This means that (~~X && Y) is parsed the same as ~~(X && Y), not ((~~X) && Y).

Most languages think logical-not should be tighter than logical-and/or, but that’s not what we got here,

Ah, I see! If it’s the way it is and it’s documented, there’s nothing to do then. I must have missed it in the DM4; it’s rather disconcerting but I can just reword my conditional. Thank you!

It is a bit peculiar.

&& is left-associative and short-circuiting, and ~~ is unary, which would normally make you think that (~~X && Y) would be equivalent to ((~~X) && Y), even though they have the same precedence (since that’s what left-associativity means).

Possibly the tipping point is that ~~ doesn’t specify associativity (since it’s unary, I guess) but since it requires an expression placed after it to negate (and thus is effectively right-associative), that might be why it’s parsing as (~~(X && Y)), since && does not have lower precedence than ~~. Still seems like a bit of a grey area though.

Just add brackets to remove the ambiguity:
print ((~~(location == Cove or Centralplaza)) && lookmode == 1);

I concur with @mirality. If the compiler does indeed interpret the expression as @zarf describes it, then the documentation is wrong (or at least misleading).

First, from DM4 1.6:

When two operators have the same precedence level (for example, + and - are of equal precedence) calculation is (almost always) “left associative”, that is, carried out left to right. So the notation a-b-c means (a-b)-c and not a-(b-c).

If ~~ and && are left-associative, then the expression ~~ X && Y should be evaluated from left to right, so ~~ should be evaluated before &&.

However! In DM4 Table 1b, the associativity field of ~~ is blank. Blank is clearly not the same as left. Further down in the table, the associativity of several operators is listed as “none”. Could a blank field mean “none”? Well, no, because (quoting the notes after Table 1b):

Conditions have no associativity and if you type a==b==c then Inform will ask you to add brackets for clarity.

Meanwhile, unary minus is described like this (in 1.6):

The operator - is different from all those mentioned so far because it operates only on one value. It has higher precedence than any of the five other arithmetic operators.

Most languages consistently bind unary operators tighter than binary operators, and it’s unfortunate that I6 treats arithmetic and conditional logic differently. It’s not possible to change it, as doing so would break existing code in subtle and horrible ways. But it would be possible to make the compiler warn about counter-intuitive expressions and insist on clarifying parentheses, in the way C compilers often warn about if(a & b == c) (because, surprisingly, == binds tighter than bitwise & in C).

I think DM4 could be fixed (made to match reality) by adjusting the precedence of ~~ to 1.5. I suspect a similar case could be made for ~ (bitwise not), which would get a new precedence of 5.5.

1 Like

Thanks, that’s a good way of looking at it. You’re correct about ~.

I am going to add a section to the I6 Addendum doc:

3 Likes

I like this idea in theory. (As a warning, not a mandatory requirement like a==b==c!) Mind you, the expression parser is not my favorite part of the I6 source, so I’m not eager to jump in and work on this…

I’d also want to check if the I6 library contains any lines like this, as far back as 6/11. It wouldn’t be great if the library generated warnings. I suppose we could skip them on System_file files.

Yeah, if anyone wants to point out goofs in the Library, I’d very much appreciate it. There’s 20+ years of cruft still floating around in there that I’m still fixing despite the 6.12.x releases.

I saw nothing suspicious with the logical and binary operator in the library.

I see nothing suspicious in the “veneer” routines. It’s all on the form “if (~~(obj ofclass cla))”.

If “~~obj ofclass Object” is a problem, then there is one in the library e.g.:
if (~~obj ofclass Object) "[Not an object.]";

That actually should work ok, since ofclass has strictly higher precedence than ~~.

But adding the parentheses anyway may be a good idea for the sake of clarity with the other cases.

1 Like