Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Bits

Status: settled and built.

defined-behaviour.md §8 listed “shifts and bitwise operators” as absent rather than undefined, with two questions attached: what happens at a shift amount ≥ 64, and whether >> is arithmetic. §8’s rule is that each gets an answer in the slice that adds it, before the code that needs one — so this document exists because a port needed them (docs/porting.md), and it answers both plus the two §8 did not think to ask.


1. The set

a & b      a | b      a ^ b      ~a       a << n      a >> n

Six operators on int, and nothing on byte. strings.md §2 refuses arithmetic on byte so that unsigned widths can stay deferred, and a bitwise operator is arithmetic for that purpose: byte_of(int_of(b) & 15) is what you write, and it says where the range check happens.

That is the same shape strings.md §4 predicted — “a caller that wants the low eight bits writes the mask explicitly, once there are bitwise operators to write it with” — so the sentence that was a promise is now a program.

1.1 And hexadecimal literals, which the same code demanded

Writing 0xff was not possible until this slice either, and a mask in decimal is a mask nobody can read:

byte_of(int_of(b) & 15)        // fine, because 15 is small
value & 4294967295             // and this is not
value & 0xffff_ffff            // this is

0x is the one prefix. Base two and base eight were left out on purpose: a mask is written in hex by everyone who writes masks, 0b earns its place only where a bit pattern is the documentation (a register map, which this language cannot yet reach), and 0o’s only customer is a Unix file mode, which it cannot set.

A hexadecimal literal is a spelling, not a type. canonical-ast.md §3 keeps values rather than spellings, so 0xff and 255 are the same node and hash identically — which is why §6 below is true of this too.


2. >> is arithmetic, and that is forced

int is signed, and a signed right shift that filled with zeros would turn a negative number positive. §8 had already called this: “it will be arithmetic; int is signed.”

(0 - 8) >> 1   ==   0 - 4

A logical shift is a different operation on a different type, and the type it belongs to — an unsigned width — is still deferred (defined-behaviour.md §8). When unsigned integers arrive, >> on them will be logical, and it will be the type that decides rather than a second operator.


3. A shift amount out of range traps

x << 64        // traps
x << -1        // traps
x >> 64        // traps

C leaves this undefined. Rust panics in debug and masks in release, so the same program means two things. This language has one answer for the whole family, and §2.1 of defined-behaviour.md already gave it: a silently wrong answer propagates and a stopped process does not.

The range is 0 <= n < 64, checked against the shift amount and not against the value. 64 is the width of int and the only width there is.


4. But a shift does not trap on the value it produces

This is the exception, and it is deliberate:

1 << 62        //  4611686018427387904
1 << 63        // -9223372036854775808, and this is fine

1 << 63 sets the sign bit. Read as arithmetic that is an overflow, and §2.1 traps on overflow. Read as bits it is exactly what was asked for, and refusing it would make << useless for the only thing it is for.

The rule: + is arithmetic and traps; << is bits and does not.

Which is the same line defined-behaviour.md §2.2 already draws between + and wrapping_add — “+ is what you write when you mean arithmetic, wrapping_add is what you write when you mean the bits” — applied to an operator that has no arithmetic reading at all. A bitwise operator is a wrapping_ operator that does not need the prefix, because nobody reaches for & by accident.

&, |, ^ and ~ cannot overflow, so the question does not arise for them.


5. Precedence is Rust’s, not C’s

Loosest to tightest:

||
&&
==  !=
<  <=  >  >=
|
^
&
<<  >>
+  -
*  /  %

Bitwise binds tighter than comparison. In C it does not, so

if (flags & MASK == 0)      /* C: flags & (MASK == 0) */

is a bug that every C style guide warns about and every C programmer has written. It is a famous defect, C keeps it for compatibility with code written before && existed, and there is no compatibility to keep here.

So flags & mask == 0 means (flags & mask) == 0, which is what it looks like.

Shifts bind tighter than & and looser than +, which is Rust’s ordering and reads correctly for the thing shifts are mostly used for: base + n << 2 would be surprising either way, so §7’s fixture pins it rather than leaving it to be discovered.

5.1 & in expression position is unambiguous

& is the reference constructor — &r T, &!r T, and a region binder fn f[&r]. All three are type positions, and cancho has never let a type and an expression meet: a type appears after :, after ->, or inside [...] on a declaration, and nowhere else.

There is also no address-of operator to collide with, because a reference here comes from borrow and from nothing else (linearity-and-effects.md §5). So a & between two expressions can only be the binary operator, and the parser needs no lookahead to know it.

>> is free for the same structural reason: generic arguments are written in brackets, Pair[int, Vec[int]], so the closing-angle-bracket problem that C++ spent a decade on does not exist here.


6. Adding these moved no hash

An operator is hashed by a code inside Binary/Unary (crates/cancho-id), not by a node tag of its own, and the six new codes are appended after the thirteen that existed. So every SigId and body_hash in the repository is byte-identical to what it was before this slice, and docs/INVARIANTS.md needed no entry.

That is worth one line because it is the property the canonical AST was designed for and this is the first slice that got to check it rather than assert it: ids_are_stable_across_the_operator_set compares the hashes of a program written before the operators existed.


7. The suite

FixtureRule§
bitwise_on_a_byte.chobyte still has no arithmetic, so a mask says where the range check is1
bitwise_on_a_bool.cho& is not &&, and a bool is not an int1

An out-of-range shift is not a reject fixture, for the reason slicing.md §8 gives: the reject harness runs check, and a program that traps is one that compiled. Those are conformance tests.

TestRule§
a_shift_past_the_width_trapsn >= 64 and n < 0 both trap, and 1 << 63 does not3, 4
ids_are_stable_across_the_operator_setAdding five binary codes and one unary code moved no hash6
AcceptingShows
bitwise.choAll six, the precedence table, arithmetic >>, 1 << 63, and a hex mask
examples/base64/§1’s promise as a program: the port that needed them