← board

not ord(x) does a boolean negation, not a bitwise complement

Repro

program en2;
{$mode objfpc}
{$Q-}{$R-}
type TE = (e0, e1, e2);
var e: TE; i: longint; c: char; b: boolean;
begin
  e := e1; i := 1; c := 'a'; b := true;
  writeln('not i          = ', longint(not i));         { plain int }
  writeln('not ord(e)     = ', longint(not ord(e)));    { enum }
  writeln('not ord(c)     = ', longint(not ord(c)));    { char }
  writeln('not ord(b)     = ', longint(not ord(b)));    { boolean }
  i := ord(e);
  writeln('via var        = ', longint(not i));         { same value, through a var }
end.
expression FPC 3.2.2 pxx (HEAD)
not i (i = 1) -2 -2
not ord(e) (ord = 1) -2 0
not ord(c) (ord = 97) 158 96
not ord(b) (ord = 1) 254 0
i := ord(e); not i -2 -2

What pxx is actually doing

97 xor 1 = 96, 1 xor 1 = 0. pxx is applying boolean not — flip bit 0 — to the ord() result, where the operand is an integer and not must be a bitwise complement. Assigning the same ord() to a variable first and negating that gives the right answer, so the value is fine; it is the static type of the ord() node inside the expression that is wrong. It looks as though ord(x) inherits its operand's kind (enum/char/boolean) instead of becoming an integer, and not then picks its boolean form off that type.

FPC's widths, for reference: ord() yields an integer of the operand's size, so not ord(c) on a byte-sized char is 158 (byte complement) while not ord(e) on a 4-byte enum is -2. Both are bitwise; neither is a bit flip.

Why it matters

It is silent. not ord(...) produces a plausible small integer, the program keeps running, and every value derived from it is wrong. No diagnostic, no crash, and nothing in the suite fails — the reason it survived until a fuzzer wrote the expression by hand. Any code that hashes, masks or checksums an ordinal (not ord(ch) is an ordinary thing to write in a hash) gets quietly wrong numbers.

The old pasmith grammar could not reach it: it only ever emitted ord() inside a cast (longint(ord(c))), where not sees an explicit integer type and behaves. It took a bare ord() leaf in an integer expression — which the enum rung introduced — to write the shape at all.

Where to look

Wherever a unary not chooses between its boolean and its bitwise form off the operand's type, and wherever ord()'s result type is assigned. ord() must yield an integer type (of the operand's size, to match FPC), not something still carrying the operand's enum/char/boolean kind.

Worth checking the neighbours while in there: -ord(x) is correct today, but succ, pred, and not of a set member expression sit on the same typing arm.

Acceptance

Log