Operators
Arithmetic, bit operations, assignment, comparisons and logic, ktm's chained comparisons, sizeof and typeof, casts, and the order operators apply in.
Arithmetic
+, -, *, / and % work as in C. Integer division drops the fraction; with a float on either side the result is a float.
void main() {
i64 a = 7, b = 2;
printf("%d %d %d %d %d\n", a + b, a - b, a * b, a / b, a % b);
printf("%.2f %.2f\n", 7.0 / 2, a / 2.0);
}
9 5 14 3 1
3.50 3.50
There is no exponent operator: use math.power(x, y), which takes and returns an f64.
import math;
void main() {
printf("%.0f %.3f\n", math.power(2, 10), math.power(2, 0.5));
}
1024 1.414
Unary minus and plus
Unary - and + take a number. A narrower integer is widened to i64 first, so negating a u8 gives a negative number, not a wrapped byte.
void main() {
u8 small = 200;
i64 n = 5;
printf("%d %d %d\n", -small, -n, +n);
}
-200 -5 5
Bit operations
& (and), | (or), ^ (exclusive or), ~ (not), << and >> (shifts).
void main() {
i64 a = 0b1100, b = 0b1010;
printf("%d %d %d %d\n", a & b, a | b, a ^ b, ~a);
printf("%d %d\n", 1 << 10, 1024 >> 3);
}
8 14 6 -13
1024 128
Flags are the usual use: one bit per option, | to combine, & to test, & ~ to clear.
const i64 READ = 1 << 0;
const i64 WRITE = 1 << 1;
const i64 EXEC = 1 << 2;
void show(i64 mode) {
printf("%c%c%c\n", mode & READ ? 'r' : '-', mode & WRITE ? 'w' : '-', mode & EXEC ? 'x' : '-');
}
void main() {
i64 mode = READ | WRITE;
show(mode);
mode |= EXEC;
show(mode);
mode &= ~WRITE;
show(mode);
}
rw-
rwx
r-x
>> on a signed value keeps the sign (an arithmetic shift); on an unsigned value it shifts in zeros.
void main() {
i64 neg = -16;
u64 big = 0xFFFFFFFFFFFFFFFF;
printf("%d %u\n", neg >> 2, big >> 60);
}
-4 15
~ takes an integer only: ~ on an f64 is an error.
Assignment
= stores a value; +=, -=, *=, /=, %=, &=, |=, ^=, <<= and >>= combine an operation with it. ++ and -- add or subtract one, before (++n) or after (n++) the value is used.
void main() {
i64 n = 10;
n += 5;
n *= 2;
n -= 1;
n /= 3;
n %= 4;
printf("%d\n", n);
i64 i = 5;
i64 before = i++;
i64 after = ++i;
printf("%d %d %d\n", before, after, i);
}
1
5 7 7
An assignment is also an expression: its value is what was stored. That value has the type of the left side, so it is cut to fit:
void main() {
i64 a, b, c;
a = b = c = 7;
printf("%d %d %d\n", a, b, c);
u8 byte;
i64 v = (byte = 300);
printf("%d\n", v);
}
7 7 7
44
Comparisons
==, !=, <, <=, > and >= give 1 or 0.
void main() {
i64 a = 3, b = 5;
printf("%d %d %d %d %d %d\n", a == b, a != b, a < b, a <= b, a > b, a >= b);
}
0 1 1 1 0 0
On u64 values, comparisons, /, % and >> are unsigned:
void main() {
u64 big = 0xFFFFFFFFFFFFFFFF;
i64 neg = -1;
printf("%d %d\n", big > 1, neg > 1);
}
1 0
Chained comparisons
As in mathematics, lo < x < hi means lo < x && x < hi, and the middle value is computed once.
bool inRange(i64 x) { return 1 <= x <= 10; }
void main() {
printf("%d %d %d\n", inRange(0), inRange(5), inRange(10));
i64 a = 1, b = 2, c = 3;
printf("%d %d\n", a < b < c, a < c < b);
}
0 1 1
1 0
Logic
&& (and), || (or) and ! (not) take any number or pointer: zero and null are false, everything else is true. They give 1 or 0.
void main() {
f64 half = 0.5;
u8 *none = null;
printf("%d %d %d %d\n", half && 1, !half, none || 0, !none);
}
1 0 0 1
&& and || stop as soon as the answer is known: the right side runs only when it is needed. That makes "check, then use" safe in one expression.
bool loud(u8 *name, bool v) {
printf("%s ", name);
return v;
}
void main() {
bool r1 = loud("a", false) && loud("b", true);
printf("\n");
bool r2 = loud("c", true) || loud("d", true);
printf("\n%d %d\n", r1, r2);
u8 *name = null;
if (name && name[0] == 'x') {
printf("starts with x\n");
} else {
printf("no name, and no crash\n");
}
}
a
c
0 1
no name, and no crash
The conditional operator
cond ? a : b is a when cond is true, else b. Only the chosen side runs.
i64 abs(i64 n) { return n < 0 ? -n : n; }
void main() {
i64 count = 1;
printf("%d %d\n", abs(-4), abs(4));
printf("%d item%s\n", count, count == 1 ? "" : "s");
count = 3;
printf("%d item%s\n", count, count == 1 ? "" : "s");
}
4 4
1 item
3 items
sizeof, alignof and typeof
sizeof is the size of a type or an expression, alignof its alignment, in bytes. typeof is the name of an expression's type, as a string.
struct Pair { i32 a; i64 b; };
void main() {
i64 arr[10];
printf("%d %d\n", sizeof(arr), sizeof(arr) / sizeof(arr[0]));
printf("%d %d\n", sizeof(Pair), alignof(Pair));
printf("%s | %s | %s\n", typeof(1 + 1), typeof(&arr[0]), typeof(arr));
}
80 10
16 8
i64 | i64 * | i64[10]
All three are worked out at compile time. Their operand never runs:
i64 noisy() {
printf("called!\n");
return 1;
}
void main() {
printf("%s %d\n", typeof(noisy()), sizeof(noisy()));
}
i64 8
Casts
(type)value converts a value, as in C. A cast binds tighter than any binary operator: (f64)a / b converts a, then divides.
void main() {
f64 f = 3.9;
i64 i = (i64)f;
printf("%d\n", i);
printf("%.1f %.1f\n", (f64)(7 / 2), (f64)7 / 2);
printf("%.1f\n", (f64)-i / 2);
u8 *s = "hey";
i64 address = (i64)s;
printf("%c\n", *(u8 *)address);
}
3
3.0 3.5
-1.5
h
- A cast applies to the whole postfix expression after it:
(i64)p.ncastsp.n. - Any type works, function types included:
(fn(i64) i64)ptr. - Types and values covers what each conversion does to the value.
Precedence
The same as C, from the tightest to the loosest:
| Operators | Kind |
|---|---|
a[i] f() a.b a->b x++ x-- | Postfix |
- ! ~ *p &x ++x --x (T)x sizeof | Unary |
* / % | Multiplicative |
+ - | Additive |
<< >> | Shifts |
< <= > >= | Relational (they chain) |
== != | Equality |
&, then ^, then | | Bitwise |
&&, then || | Logical |
?: | Conditional |
= += -= ... | Assignment |
When in doubt, add parentheses: they cost nothing.
void main() {
i64 a = 2, b = 3, c = 4;
printf("%d %d\n", a + b * c, (a + b) * c);
printf("%d %d\n", 1 << 2 + 1, (1 << 2) + 1);
printf("%d %d\n", 6 & 3 == 3, (6 & 3) == 2);
}
14 20
8 5
0 1
The middle line shows a classic trap: + binds tighter than <<. So does the last one: == binds tighter than &.