cint

cint university / 201 / ced, the cint editor

201-03: operators at the edge

This is lesson 201-03 of CU-201.2, checked against cint 0.2.0.

You will see that the plain operators are checked, and write the wrapping or saturating spelling only where that is what the program means.

the program

This program uses the 32-bit FNV-1a hash. Its multiply is meant to wrap, so the program writes *%:

// FNV-1a, a 32-bit hash. Hashes wrap on purpose, so the
// multiply is written *% and says so.
const U32 FNV_OFFSET = 2_166_136_261;
const U32 FNV_PRIME = 16_777_619;

U32 fnv1a[n](in U8[n] bytes) {
    U32 h = FNV_OFFSET;
    for i in 0..n {
        h = h ^ (bytes[i] as U32);
        h = h *% FNV_PRIME;
    }
    return h;
}

void main() {
    U32 a = fnv1a("hello");
    U32 b = fnv1a("hellp");
    "hello -> {a:x}\n";
    "hellp -> {b:x}\n";
}

Run this program in ced

run it

$ cint run fnv.ci
hello -> 4f9f2cab
hellp -> 5c9f4122

Both values match a Python implementation of the published algorithm, which masks each product with & 0xffffffff.

the same hash with a plain *

Change *% to * and the first multiply faults:

fault[E_OVERFLOW]: the exact result is outside the type's range
  --> fnv_checked.ci:10:15
   |
10 |         h = h * FNV_PRIME;
   |               ^ mul.checked.u32
   |
   = operation: mul.checked.u32
   = operand-count: 2
   = operand: U32 2166136237
   = operand: U32 16777619
   = exact:   36342608486479703
   = limit:   U32 4294967295
   ...
   = stack-depth: 1
   = called from: fnv_checked.ci:16:13

called from gives the line in main that called fnv1a. In C, uint32_t arithmetic wraps by definition, so C versions of the two programs would read the same. In cint, the wrap is written where it is wanted, and every other multiply in a program stays checked.

three families of operators

Family Spelling On overflow
checked + - * / << faults
wrapping +% -% *% keeps the low bits, never faults
saturating +| -| *| stops at T.min or T.max, never faults

This program prints four results:

U8 a = 10;
U8 b = 20;
U8 w = a -% b;
U8 s = a -| b;
I8 m = 100;
I8 t = m *| 3;
I8 u = m *% 3;
"{w} {s} {t} {u}\n";

Run this program in ced

246 0 127 44

10 - 20 wraps to 246 in U8 and saturates to 0; 100 * 3 saturates to 127 in I8 and wraps to 44 (300 - 256).

saturating pixels

Brightening an 8-bit pixel should stop at white, 255. That is what +| says:

// Brighten 8-bit pixels. A pixel past 255 should stop at 255,
// so the add is written +| and saturates.
void brighten[n](inout U8[n] px, U8 amount) {
    for i in 0..n { px[i] = px[i] +| amount; }
}
void main() {
    U8[6] px = [0, 64, 128, 192, 230, 255];
    brighten(px, 40);
    for i in 0..6 { "{px[i]} "; }
    "\n";
}

Run this program in ced

40 104 168 232 255 255 

elsewhere

On 2026-10-10, the same six pixels were brightened by 40 in other languages:

Language Result
cint, +| 40 104 168 232 255 255
NumPy 2.5.3, uint8 array + np.uint8(40) [ 40 104 168 232 14 39], with no warning
C, GCC 13.3 -O2, uint8_t with += 40 104 168 232 14 39

The two bright pixels turn dark. A plain + in cint would fault on 230 + 40 instead; +| gives the answer the program wants.

shifts are checked too

I32 one = 1;
I32 x = one << 31;
"x={x}\n";

Run this program in ced

fault[E_OVERFLOW]: the exact result is outside the type's range
  --> p29_shift_overflow.ci:2:13
   |
 2 | I32 x = one << 31;
   |             ^ shl.checked.i32
   |
   = operation: shl.checked.i32
   = operand-count: 2
   = operand: I32 1
   = operand: I64 31
   = exact:   2147483648
   = limit:   I32 2147483647
   ...

A left shift is a multiply by a power of two, and it is checked the same way. In C, 1 << 31 on a 32-bit int is undefined behavior.

walk-through

Why checked is the default. Most arithmetic in a program is not meant to wrap. Making the plain operators checked turns every unintended overflow in a plain operator into a fault at the operation that caused it. Intended wrapping and saturating stay visible as the % of +% and the | of +|.

Bitwise operators &, | and ^ take two integers of one type, and ~ takes one. Unlike the operands of these, a shift's count can be of another integer type. h ^ (bytes[i] as U32) converts the byte first, because cint does not convert implicitly.

in U8[n] bytes is a read-only view of n bytes, and n is a size parameter filled in at the call. A string literal passed to it is viewed as its bytes.

try this

  1. Write a 64-bit FNV-1a with offset 14695981039346656037 and prime 1099511628211. What does it give for "hello"?
  2. Darken the pixels by 40 with -|. What do the first two become?

reference

checked operators, wrapping and saturating operators, shifts, views

Next: 201-04, division and rounding.